Samad14 commited on
Commit
5a0e691
·
0 Parent(s):

Initial commit: Bio Nexus pipeline engine — BLAST + UniProt + AlphaFold + AI interpretation (Phase 1)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +32 -0
  2. MASTER_PLAN.md +205 -0
  3. bio-nexus/phase0/OUTREACH.md +110 -0
  4. bio-nexus/phase0/TRACKER.md +97 -0
  5. bioai-platform/backend/.env.example +4 -0
  6. bioai-platform/backend/__init__.py +0 -0
  7. bioai-platform/backend/app/__init__.py +0 -0
  8. bioai-platform/backend/app/ai/__init__.py +0 -0
  9. bioai-platform/backend/app/ai/interpreter.py +45 -0
  10. bioai-platform/backend/app/ai/llm_client.py +44 -0
  11. bioai-platform/backend/app/ai/prompts.py +46 -0
  12. bioai-platform/backend/app/config.py +22 -0
  13. bioai-platform/backend/app/main.py +54 -0
  14. bioai-platform/backend/app/pipeline/__init__.py +0 -0
  15. bioai-platform/backend/app/pipeline/assembler.py +68 -0
  16. bioai-platform/backend/app/pipeline/definitions/__init__.py +0 -0
  17. bioai-platform/backend/app/pipeline/definitions/protein_analysis.py +16 -0
  18. bioai-platform/backend/app/pipeline/engine.py +46 -0
  19. bioai-platform/backend/app/pipeline/registry.py +19 -0
  20. bioai-platform/backend/app/routers/__init__.py +0 -0
  21. bioai-platform/backend/app/routers/ai.py +32 -0
  22. bioai-platform/backend/app/routers/export.py +42 -0
  23. bioai-platform/backend/app/routers/jobs.py +38 -0
  24. bioai-platform/backend/app/routers/pipelines.py +65 -0
  25. bioai-platform/backend/app/routers/profile.py +28 -0
  26. bioai-platform/backend/app/routers/results.py +35 -0
  27. bioai-platform/backend/app/routers/share.py +13 -0
  28. bioai-platform/backend/app/routers/waitlist.py +28 -0
  29. bioai-platform/backend/app/services/__init__.py +0 -0
  30. bioai-platform/backend/app/services/cache.py +61 -0
  31. bioai-platform/backend/app/services/export.py +130 -0
  32. bioai-platform/backend/app/services/rate_limit.py +27 -0
  33. bioai-platform/backend/app/services/redis.py +15 -0
  34. bioai-platform/backend/app/services/supabase.py +11 -0
  35. bioai-platform/backend/app/services/validators.py +43 -0
  36. bioai-platform/backend/app/tools/__init__.py +0 -0
  37. bioai-platform/backend/app/tools/alphafold.py +44 -0
  38. bioai-platform/backend/app/tools/base.py +16 -0
  39. bioai-platform/backend/app/tools/blast.py +80 -0
  40. bioai-platform/backend/app/tools/registration.py +10 -0
  41. bioai-platform/backend/app/tools/uniprot.py +102 -0
  42. bioai-platform/backend/app/workers/__init__.py +0 -0
  43. bioai-platform/backend/app/workers/celery_app.py +22 -0
  44. bioai-platform/backend/app/workers/pipeline_worker.py +57 -0
  45. bioai-platform/backend/requirements.txt +12 -0
  46. bioai-platform/frontend/.env.local.example +3 -0
  47. bioai-platform/frontend/next-env.d.ts +5 -0
  48. bioai-platform/frontend/next.config.js +13 -0
  49. bioai-platform/frontend/package-lock.json +0 -0
  50. bioai-platform/frontend/package.json +32 -0
.gitignore ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Python ---
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ .env
7
+ venv/
8
+ .venv/
9
+
10
+ # --- Node ---
11
+ node_modules/
12
+ .next/
13
+ npm-debug.log*
14
+
15
+ # --- IDE ---
16
+ .idea/
17
+ .vscode/
18
+ *.swp
19
+ *.swo
20
+
21
+ # --- OS ---
22
+ .DS_Store
23
+ Thumbs.db
24
+
25
+ # --- Env files (keep .example, ignore actual keys) ---
26
+ .env.local
27
+ .env.production
28
+ backend/.env
29
+
30
+ # --- Runtime artifacts ---
31
+ dump.rdb
32
+ *.rdb
MASTER_PLAN.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Bio Nexus Platform — Master Plan v3.0
2
+
3
+ > A bioinformatics pipeline engine that removes the need for expertise to get expert results.
4
+ > User arrives with a biological question and raw data. Leaves with a complete, interpreted answer — having touched nothing in between.
5
+ >
6
+ > Solo founder · Indian M.Sc. bioinformatics grad · Product-first
7
+
8
+ ---
9
+
10
+ ## 1. What the Product Actually Is
11
+
12
+ Not a tool aggregator. Not a BLAST wrapper with AI on top.
13
+
14
+ The current experience:
15
+
16
+ ```
17
+ NCBI → paste sequence → BLAST → confusing output → copy accession →
18
+ open UniProt → new tab → scroll entry → open AlphaFold → new tab →
19
+ download PDB → open PyMOL → read 3 papers → give up or ask a senior
20
+ ```
21
+
22
+ Bio Nexus:
23
+
24
+ ```
25
+ Paste sequence → press Run → read one comprehensive page that explains everything
26
+ ```
27
+
28
+ This is a **pipeline engine**. Every tool output flows into a results aggregator that normalizes, cross-references, and passes the complete picture to both the visualization layer and the AI interpretation layer simultaneously. The AI doesn't just see BLAST output — it sees what the sequence is, what homologs exist with statistical confidence, what the function annotation says, and what the structure looks like.
29
+
30
+ **New bioinformatics tools become new modules. The architecture never changes — only the pipeline library grows.**
31
+
32
+ ---
33
+
34
+ ## 2. Target User
35
+
36
+ **NOT** IIT computational biology faculty (they already have workflows).
37
+
38
+ The real users:
39
+ - **M.Sc. bioinformatics student at JMI or central university** — learning but doesn't know the toolchain
40
+ - **PhD student in biochemistry** — suddenly needs to analyze a protein sequence, uncomfortable with CLI
41
+ - **MBBS researcher at AIIMS** — found an interesting variant, doesn't know what to do
42
+ - **M.Sc. biotechnology final-year project student** — needs results, not tool expertise
43
+
44
+ These people exist in enormous numbers. They have severe unmet need. You can reach them directly because you **are** one of them.
45
+
46
+ Pricing reflects this reality:
47
+
48
+ | Tier | Price | Verification | Users |
49
+ |------|-------|--------------|-------|
50
+ | Student | ₹0 | `.edu.in` email | Current students |
51
+ | Individual | ₹299/mo | — | Early-career researchers |
52
+ | Lab | ₹999/mo (3–5 seats) | — | Small research groups |
53
+ | Institution | Custom | — | University-wide license |
54
+
55
+ The free student tier is not a loss — it's the growth strategy. Students graduate, become postdocs, become faculty, become the people who approve institutional licenses.
56
+
57
+ ---
58
+
59
+ ## 3. Competitive Moat
60
+
61
+ Galaxy Project: free, open-source, 15 years old, hundreds of tools, trusted by reviewers.
62
+
63
+ You cannot beat Galaxy on **breadth** or **reputation**.
64
+
65
+ You beat them on:
66
+
67
+ 1. **AI interpretation** — Galaxy has none. Results come back raw. Bio Nexus explains everything in plain language.
68
+ 2. **Zero setup** — Galaxy requires an account, a server, or an institutional deployment. Bio Nexus requires a Google login.
69
+ 3. **Plain language output** — designed explicitly for people who aren't bioinformatics experts. This is the real differentiation.
70
+
71
+ ---
72
+
73
+ ## 4. Architecture
74
+
75
+ ```
76
+ User input
77
+ ├── Sequence (FASTA)
78
+ ├── Gene list
79
+ ├── FASTQ files (Phase 3)
80
+ └── Structure (PDB ID)
81
+ │
82
+ ▼
83
+ Pipeline selector
84
+ "What do you have?" → "What do you want to know?" → pipeline chosen
85
+ │
86
+ ▼
87
+ Pipeline engine
88
+ Task DAG · async workers · Redis queue · Supabase Realtime progress
89
+ │
90
+ ├── BLAST (EBI REST API: nr, swissprot, pdb)
91
+ │ E-value · bit score · % identity · coverage
92
+ │
93
+ ├── UniProt (REST lookup)
94
+ │ Function · domains · disease associations · 24h Redis cache
95
+ │
96
+ ├── AlphaFold DB (Phase 1)
97
+ │ 3Dmol.js render · in-browser only · no downloads
98
+ │
99
+ ├── Phase 2 modules
100
+ │ MSA · Phylogenetic tree · Pfam domains
101
+ │ KEGG pathways · Primer3 · DiffDock (via Replicate)
102
+ │
103
+ └── Phase 3 modules
104
+ RNA-seq · Variant calling · ChIP-seq
105
+ │
106
+ ▼
107
+ Results aggregator ◄── THE KEY PIECE YOUR CURRENT CODEBASE MISSES
108
+ Merges all tool outputs into structured JSON
109
+ Normalises formats · resolves cross-references (accession→UniProt→PDB)
110
+ Hands complete picture to AI + visualisation simultaneously
111
+ │
112
+ ├── Visualisation layer
113
+ │ MSA viewer · 3D structure · Phylogenetic tree
114
+ │ All in-browser · nothing downloads
115
+ │
116
+ └── AI interpretation (LiteLLM · streaming)
117
+ Plain language report · citation links · confidence indicators
118
+ │
119
+ ▼
120
+ Unified result page
121
+ One URL · shareable · exportable (PDF) · explainable to a first-year M.Sc. student
122
+ ```
123
+
124
+ ---
125
+
126
+ ## 5. Phase Breakdown
127
+
128
+ ### Phase 1 — Prove the pipeline engine works (Months 1–4)
129
+
130
+ One workflow. Done better than anyone else has done it. No feature creep.
131
+
132
+ **Workflow**: Protein sequence → BLAST (EBI) → UniProt annotation → AlphaFold structure → AI report
133
+
134
+ **Output**: A single result page:
135
+ - Top BLAST hits with E-value and identity clearly explained
136
+ - UniProt entry distilled to what matters (function, disease associations, active sites, organism)
137
+ - AlphaFold structure rendered in-browser with 3Dmol.js — nothing downloads
138
+ - AI section that ties it all together in plain language a first-year M.Sc. student can read and act on
139
+
140
+ No one offers this. Galaxy makes you build a workflow manually. EMBL-EBI runs each tool separately. Nobody gives a unified interpreted result.
141
+
142
+ ### Phase 2 — Expand the pipeline library (Months 5–10)
143
+
144
+ New pipelines:
145
+ - **MSA + phylogenetic tree** — paste sequences, get a tree with evolutionary distances explained
146
+ - **Domain & motif analysis** — Pfam, PROSITE — what are the functional regions
147
+ - **Gene ontology + KEGG enrichment** — given a gene list, what processes are enriched
148
+ - **Primer design** — Primer3 integration, conditions included
149
+ - **Molecular docking** — DiffDock via Replicate (GPU inference API, not local)
150
+
151
+ Pipeline selector UI: "What do you have?" → "What do you want to know?" → pipeline recommended automatically
152
+
153
+ ### Phase 3 — Handle raw sequencing data (Months 11–18)
154
+
155
+ - FASTQ → QC → trimming → alignment → variant calling → annotation → interpreted report
156
+ - RNA-seq differential expression
157
+ - Larger infrastructure: file storage, longer jobs, more compute
158
+ - Significantly expands user base from coursework students to researchers doing published work
159
+
160
+ ### Phase 4 — Platform + collaboration (Months 19–30)
161
+
162
+ - Lab workspaces (PI + students share a project)
163
+ - Custom pipeline builder for advanced users
164
+ - Institution licensing
165
+ - API access for programmatic use
166
+
167
+ ---
168
+
169
+ ## 6. What Changes Right Now
170
+
171
+ | Principle | Action |
172
+ |-----------|--------|
173
+ | Pipeline engine | Build results aggregator that merges BLAST + UniProt + AlphaFold + AI into one output |
174
+ | Unified result page | Replace separate BLAST/UniProt tabs with single pipeline results view |
175
+ | Student-first | `.edu.in` free tier, ₹299 individual, ₹999 lab |
176
+ | Honest AI | Working Groq key installed. Fallback shows visible banner, not fake analysis |
177
+ | Outreach | Target M.Sc./PhD/MBBS students. One question: "Walk me through the last sequence you tried to analyze" |
178
+ | Architecture | Aggregator layer normalises cross-tool output. AI sees the complete picture, not just one tool's output |
179
+
180
+ ---
181
+
182
+ ## 7. Weekly Rhythm
183
+
184
+ ```
185
+ Mon–Thu Build (pipeline engine, aggregator, one module at a time)
186
+ Fri 2–3 user calls (students, not faculty)
187
+ Sat Feedback → incorporate → build-in-public post
188
+ Sun Plan next week — no coding
189
+ ```
190
+
191
+ **Hard rule**: Two weeks without a user call → stop building, make calls.
192
+
193
+ ---
194
+
195
+ ## 8. The Honest Challenges
196
+
197
+ 1. **Working AI key** — Groq is installed and tested. If API calls fail, the user sees a visible yellow banner, not fake analysis.
198
+ 2. **Real user conversations** — Before Phase 2, need 10 conversations with students who tried to analyze a sequence. Ask: "What did you open, what went wrong, how long did it take?"
199
+ 3. **GPU budgeting** — DiffDock requires a paid inference API (Replicate). Student tier won't include it. Lab tier ($999/mo) covers the cost.
200
+ 4. **Galaxy comparison** — Deep answer: Galaxy gives power users flexibility. Bio Nexus gives non-experts answers. The markets barely overlap.
201
+
202
+ ---
203
+
204
+ *Bio Nexus Platform — Master Plan v3.0*
205
+ *Pipeline engine · Student-first · India-built*
bio-nexus/phase0/OUTREACH.md ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 0 — Researcher Validation (30+ Calls)
2
+
3
+ ## Target List
4
+
5
+ ### Priority 1 — M.Sc. Bioinformatics Programs at Central Universities (15 contacts)
6
+ | # | Name | Institution | Program | Contact | Status |
7
+ |---|------|-------------|---------|---------|--------|
8
+ | 1 | | Jamia Millia Islamia | M.Sc. Bioinformatics | Dept office → student group | |
9
+ | 2 | | AMU Aligarh | M.Sc. Bioinformatics | Dept office → student group | |
10
+ | 3 | | JNU New Delhi | M.Sc. Computational Biology | Dept office → student group | |
11
+ | 4 | | University of Hyderabad | M.Sc. Bioinformatics | Dept office → student group | |
12
+ | 5 | | Pondicherry University | M.Sc. Bioinformatics | Dept office → student group | |
13
+ | 6 | | University of Pune | M.Sc. Bioinformatics | Dept office → student group | |
14
+ | 7 | | University of Calcutta | M.Sc. Bioinformatics | Dept office → student group | |
15
+ | 8 | | University of Madras | M.Sc. Bioinformatics | Dept office → student group | |
16
+ | 9 | | Bharathiar University | M.Sc. Bioinformatics | Dept office → student group | |
17
+ | 10 | | Banaras Hindu University | M.Sc. Bioinformatics | Dept office → student group | |
18
+ | 11 | | University of Kerala | M.Sc. Bioinformatics | Dept office → student group | |
19
+ | 12 | | Goa University | M.Sc. Bioinformatics | Dept office → student group | |
20
+ | 13 | | North-Eastern Hill University | M.Sc. Bioinformatics | Dept office → student group | |
21
+ | 14 | | Tezpur University | M.Sc. Bioinformatics | Dept office → student group | |
22
+ | 15 | | University of Jammu | M.Sc. Bioinformatics | Dept office → student group | |
23
+
24
+ ### Priority 2 — PhD Students (Wet-lab, Need Bioinformatics) (10 contacts)
25
+ | # | Name | Institution | Lab/Dept | Contact | Status |
26
+ |---|------|-------------|----------|---------|--------|
27
+ | 16 | | AIIMS Delhi | Biochemistry PhD | Reach through PubMed → contact | |
28
+ | 17 | | AIIMS Delhi | Pharmacology PhD | Reach through PubMed → contact | |
29
+ | 18 | | NCBS Bangalore | PhD student | Reach through student groups | |
30
+ | 19 | | CCMB Hyderabad | PhD student | Reach through student groups | |
31
+ | 20 | | IISc Bangalore | PhD (Biochemistry) | Reach through department | |
32
+ | 21 | | JNU | PhD (Life Sciences) | Reach through department | |
33
+ | 22 | | University of Delhi | PhD (Zoology) | Reach through department | |
34
+ | 23 | | CSIR-IGIB | PhD student | Reach through student groups | |
35
+ | 24 | | IIT Bombay | PhD (Biosciences) | Reach through department | |
36
+ | 25 | | IISER Pune | PhD (Biology) | Reach through department | |
37
+
38
+ ### Priority 3 — MBBS / MD Researchers (5 contacts)
39
+ | # | Name | Institution | Dept | Contact | Status |
40
+ |---|------|-------------|------|---------|--------|
41
+ | 26 | | AIIMS Delhi | Junior Resident | Reach through student groups | |
42
+ | 27 | | AIIMS Delhi | MD Biochemistry | Reach through PubMed → contact | |
43
+ | 28 | | Maulana Azad Medical College | MBBS + research | Reach through college groups | |
44
+ | 29 | | JIPMER Pondicherry | MD / MS student | Reach through college groups | |
45
+ | 30 | | CMC Vellore | MBBS + research elective | Reach through college groups | |
46
+
47
+ ---
48
+
49
+ ## Cold Outreach Template — For Students
50
+
51
+ **Subject:** 15 min — struggling with BLAST / protein analysis?
52
+
53
+ Hey [Name],
54
+
55
+ I'm a fellow M.Sc. bioinformatics student building a tool that makes protein sequence analysis instant — paste a sequence, get a complete interpreted result without touching NCBI, UniProt, or any command line.
56
+
57
+ I'd love 15 minutes to understand:
58
+ 1. What do you do when you need to analyze a protein sequence?
59
+ 2. What's the most frustrating step?
60
+ 3. Would instant AI-powered results help?
61
+
62
+ This is purely a research conversation — no sales pitch, no demo. Just trying to build something useful for people like us.
63
+
64
+ Want to hop on a quick call this week?
65
+
66
+ Best,
67
+ Samad (you are yourself)
68
+ Bio Nexus
69
+
70
+ ---
71
+
72
+ ## Call Script (15 minutes)
73
+
74
+ ### Opening (1 min)
75
+ - "I'm building a bioinformatics tool and want to make sure I'm solving real problems real students face. Not a sales call — I want your honest frustration."
76
+
77
+ ### The One Question (10 min)
78
+ - **"Walk me through the last time you tried to analyze a biological sequence."**
79
+
80
+ Follow-ups (only if needed):
81
+ - What tools did you open first?
82
+ - What went wrong or was confusing?
83
+ - How long did it take from start to having an answer?
84
+ - Did you ask someone for help? Who?
85
+
86
+ ### Closing (4 min)
87
+ - "If you could wave a magic wand and fix one thing about that process, what would it be?"
88
+ - "Is there anyone else in your class/lab who has the same problem?"
89
+ - "Want early access to try the prototype?"
90
+
91
+ ### Post-Call
92
+ - Log feedback in TRACKER.md immediately
93
+ - Add to Beta Users list if interested
94
+ - Send a thank-you text within an hour
95
+
96
+ ---
97
+
98
+ ## Beta Signup Incentive
99
+ - Free lifetime Student tier for first 50 beta testers
100
+ - Your name in the "Thanks to" list on launch
101
+ - Direct line to the founder (me) for feature requests
102
+
103
+ ---
104
+
105
+ ## Success Metrics
106
+ - [ ] 30+ calls completed
107
+ - [ ] 10+ beta signups
108
+ - [ ] 5+ concrete stories of "Here's what I did last week and it was painful"
109
+ - [ ] 3+ student groups / WhatsApp groups joined for outreach
110
+ - [ ] 1+ referral to another student
bio-nexus/phase0/TRACKER.md ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Phase 0 — Student Validation Tracker
2
+
3
+ ## Status Legend
4
+ - [ ] Not contacted
5
+ - [~] Message sent, awaiting response
6
+ - [x] Call completed
7
+ - [!] Interested / Beta signup
8
+ - [-] Not interested / No response
9
+
10
+ ---
11
+
12
+ ## Priority 1: M.Sc. Bioinformatics Programs (Central Universities)
13
+
14
+ | # | Institution | Program | Contact | Status | Notes |
15
+ |---|-------------|---------|---------|--------|-------|
16
+ | 1 | Jamia Millia Islamia | M.Sc. Bioinformatics | Find student group WhatsApp/Telegram | [ ] | Your own program — easiest entry |
17
+ | 2 | AMU Aligarh | M.Sc. Bioinformatics | Reach via departmental mail | [ ] | |
18
+ | 3 | JNU New Delhi | M.Sc. Computational Biology | Find student group | [ ] | |
19
+ | 4 | University of Hyderabad | M.Sc. Bioinformatics | Find student group | [ ] | |
20
+ | 5 | Pondicherry University | M.Sc. Bioinformatics | Find student group | [ ] | |
21
+ | 6 | University of Pune | M.Sc. Bioinformatics | Find student group | [ ] | |
22
+ | 7 | University of Calcutta | M.Sc. Bioinformatics | Find student group | [ ] | |
23
+ | 8 | University of Madras | M.Sc. Bioinformatics | Find student group | [ ] | |
24
+ | 9 | Bharathiar University | M.Sc. Bioinformatics | Find student group | [ ] | |
25
+ | 10 | Banaras Hindu University | M.Sc. Bioinformatics | Find student group | [ ] | |
26
+ | 11 | University of Kerala | M.Sc. Bioinformatics | Find student group | [ ] | |
27
+ | 12 | Goa University | M.Sc. Bioinformatics | Find student group | [ ] | |
28
+ | 13 | NEHU Shillong | M.Sc. Bioinformatics | Find student group | [ ] | |
29
+ | 14 | Tezpur University | M.Sc. Bioinformatics | Find student group | [ ] | |
30
+ | 15 | University of Jammu | M.Sc. Bioinformatics | Find student group | [ ] | |
31
+
32
+ ## Priority 2: PhD Students (Wet-lab needing bioinformatics)
33
+
34
+ | # | Name | Institution | Lab/Dept | Status | Notes |
35
+ |---|------|-------------|----------|--------|-------|
36
+ | 16 | | AIIMS Delhi | Biochemistry PhD | [ ] | Reach via PubMed → email |
37
+ | 17 | | AIIMS Delhi | Pharmacology PhD | [ ] | Reach via PubMed → email |
38
+ | 18 | | NCBS Bangalore | PhD student | [ ] | Reach via student groups |
39
+ | 19 | | CCMB Hyderabad | PhD student | [ ] | Reach via student groups |
40
+ | 20 | | IISc Bangalore | PhD (Biochemistry) | [ ] | Reach via department |
41
+ | 21 | | JNU | PhD (Life Sciences) | [ ] | Reach via department |
42
+ | 22 | | University of Delhi | PhD (Zoology) | [ ] | Reach via department |
43
+ | 23 | | CSIR-IGIB | PhD student | [ ] | Reach via student groups |
44
+ | 24 | | IIT Bombay | PhD (Biosciences) | [ ] | Reach via department |
45
+ | 25 | | IISER Pune | PhD (Biology) | [ ] | Reach via department |
46
+
47
+ ## Priority 3: MBBS / MD Researchers
48
+
49
+ | # | Name | Institution | Status | Notes |
50
+ |---|------|-------------|--------|-------|
51
+ | 26 | | AIIMS Delhi — Junior Resident | [ ] | Reach via student groups |
52
+ | 27 | | AIIMS Delhi — MD Biochemistry | [ ] | Reach via PubMed → email |
53
+ | 28 | | Maulana Azad Medical College | [ ] | Reach via college groups |
54
+ | 29 | | JIPMER Pondicherry | [ ] | Reach via college groups |
55
+ | 30 | | CMC Vellore | [ ] | Reach via college groups |
56
+
57
+ ---
58
+
59
+ ## Weekly Targets
60
+
61
+ ### Week 1: 10 calls
62
+ - [ ] 5 from JMI M.Sc. Bioinformatics (your own classmates/batchmates)
63
+ - [ ] 3 from other central university M.Sc. programs
64
+ - [ ] 2 PhD students via cold email
65
+
66
+ ### Week 2: 10 calls
67
+ - [ ] 5 from central universities (new batch)
68
+ - [ ] 3 PhD students
69
+ - [ ] 2 MBBS/MD students
70
+
71
+ ### Week 3: 10 calls
72
+ - [ ] Fill remaining slots from any category
73
+
74
+ ---
75
+
76
+ ## Outreach Log
77
+
78
+ ### Week 1
79
+ | Date | Contact | Institution | Method | Response | Follow-up |
80
+ |------|---------|-------------|--------|----------|-----------|
81
+ | | | | | | |
82
+
83
+ ---
84
+
85
+ ## Feedback Summary
86
+
87
+ | Theme | Count | Quotes / Notes |
88
+ |-------|-------|----------------|
89
+ | | | |
90
+
91
+ ---
92
+
93
+ ## Beta Signups
94
+
95
+ | Name | Institution | Program | Email | Date |
96
+ |------|-------------|---------|-------|------|
97
+ | | | | | |
bioai-platform/backend/.env.example ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ GROQ_API_KEY=
2
+ REDIS_URL=redis://localhost:6379
3
+ SUPABASE_URL=
4
+ SUPABASE_SERVICE_ROLE_KEY=
bioai-platform/backend/__init__.py ADDED
File without changes
bioai-platform/backend/app/__init__.py ADDED
File without changes
bioai-platform/backend/app/ai/__init__.py ADDED
File without changes
bioai-platform/backend/app/ai/interpreter.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from typing import AsyncGenerator
4
+ from litellm import acompletion
5
+ from app.config import settings
6
+ from app.ai.llm_client import llm_client
7
+ from app.ai.prompts import get_prompt
8
+
9
+
10
+ async def interpret_stream(pipeline_type: str, context: dict) -> AsyncGenerator[str, None]:
11
+ if not llm_client.has_api_key():
12
+ yield _error_event("GROQ_API_KEY is not configured. AI interpretation unavailable.")
13
+ return
14
+
15
+ prompt = llm_client.build_prompt(pipeline_type, context)
16
+ model = llm_client.model
17
+
18
+ try:
19
+ response = await acompletion(
20
+ model=model,
21
+ messages=[{"role": "user", "content": prompt}],
22
+ temperature=0.3,
23
+ max_tokens=2000,
24
+ stream=True,
25
+ api_key=llm_client.api_key,
26
+ )
27
+ async for chunk in response:
28
+ if chunk.choices and chunk.choices[0].delta.content:
29
+ yield _chunk_event(chunk.choices[0].delta.content)
30
+
31
+ yield _done_event({"model": model, "pipeline_type": pipeline_type})
32
+ except Exception as e:
33
+ yield _error_event(str(e))
34
+
35
+
36
+ def _chunk_event(text: str) -> str:
37
+ return f"data: {json.dumps({'chunk': text})}\n\n"
38
+
39
+
40
+ def _done_event(meta: dict) -> str:
41
+ return f"data: {json.dumps({'done': True, 'meta': meta})}\n\n"
42
+
43
+
44
+ def _error_event(msg: str) -> str:
45
+ return f"data: {json.dumps({'error': msg})}\n\n"
bioai-platform/backend/app/ai/llm_client.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from app.config import settings
3
+ from app.ai.prompts import get_prompt
4
+
5
+
6
+ class LLMClient:
7
+ def __init__(self):
8
+ self.api_key = settings.GROQ_API_KEY
9
+ self.model = settings.DEFAULT_MODEL
10
+ self.pro_model = settings.PRO_MODEL
11
+
12
+ def has_api_key(self) -> bool:
13
+ return bool(self.api_key)
14
+
15
+ def build_prompt(self, pipeline_type: str, context: dict) -> str:
16
+ template = get_prompt(pipeline_type)
17
+ blast = context.get("blast", {})
18
+ top = blast.get("top_hit", {})
19
+ uniprot = context.get("uniprot", {}) or {}
20
+ af = context.get("alphafold", {}) or {}
21
+
22
+ return template.format(
23
+ blast_count=blast.get("count", 0),
24
+ top_hit_accession=top.get("accession", "N/A"),
25
+ top_hit_description=top.get("description", "N/A"),
26
+ top_hit_evalue=top.get("evalue", "N/A"),
27
+ top_hit_identity_pct=top.get("identity_pct", "N/A"),
28
+ top_hit_bit_score=top.get("bit_score", "N/A"),
29
+ uniprot_name=uniprot.get("full_name", "N/A"),
30
+ uniprot_organism=uniprot.get("organism", "N/A"),
31
+ uniprot_genes=", ".join(uniprot.get("gene_names", []) or []) or "N/A",
32
+ uniprot_functions="; ".join(uniprot.get("functions", []) or []) or "N/A",
33
+ uniprot_locations="; ".join(uniprot.get("subcellular_locations", []) or []) or "N/A",
34
+ uniprot_keywords=", ".join(uniprot.get("keywords", []) or []) or "N/A",
35
+ uniprot_go_terms=", ".join(uniprot.get("go_terms", []) or []) or "N/A",
36
+ uniprot_features="; ".join(
37
+ f"{f.get('type', '')}: {f.get('description', '')}" for f in (uniprot.get("features", []) or [])
38
+ ) or "N/A",
39
+ alphafold_available="Yes" if af.get("structure_available") else "No",
40
+ alphafold_confidence=af.get("confidence", "N/A"),
41
+ )
42
+
43
+
44
+ llm_client = LLMClient()
bioai-platform/backend/app/ai/prompts.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PROTEIN_ANALYSIS_PROMPT = """You are a computational biology assistant at Bio Nexus. A researcher submitted a protein sequence, and the system ran BLAST, UniProt lookup, and AlphaFold structure retrieval. Here is the complete assembled context.
2
+
3
+ ## BLAST Results
4
+ - Total hits found: {blast_count}
5
+ - Top hit: {top_hit_description} (accession {top_hit_accession})
6
+ - E-value: {top_hit_evalue}
7
+ - Sequence identity: {top_hit_identity_pct}%
8
+ - Bit score: {top_hit_bit_score}
9
+
10
+ ## UniProt Annotations (Top Hit)
11
+ - Protein name: {uniprot_name}
12
+ - Organism: {uniprot_organism}
13
+ - Gene: {uniprot_genes}
14
+ - Function: {uniprot_functions}
15
+ - Subcellular location: {uniprot_locations}
16
+ - Keywords: {uniprot_keywords}
17
+ - GO terms: {uniprot_go_terms}
18
+ - Active sites / binding regions: {uniprot_features}
19
+
20
+ ## AlphaFold Structure
21
+ - Structure available: {alphafold_available}
22
+ - Confidence score (pLDDT): {alphafold_confidence}
23
+
24
+ ## Instructions for your response
25
+ 1. Explain what the query protein likely is based on the BLAST hits and UniProt annotations.
26
+ 2. Interpret the E-value and identity percentage — what they mean for confidence in the match.
27
+ 3. Summarize the protein's function, cellular location, and any known domains or active sites.
28
+ 4. If an AlphaFold structure is available, note its confidence and what that means.
29
+ 5. Give a concise bottom-line assessment: what the researcher should conclude from this analysis.
30
+ 6. Use plain language. Avoid unnecessary jargon. When you use technical terms, explain them briefly.
31
+
32
+ Write in a helpful, instructive tone. If any data is missing, state that honestly."""
33
+
34
+
35
+ FALLBACK_PROMPT = """You are a computational biology assistant. The following BLAST search results were returned, but detailed annotations are not available. Summarize the search results and help the user understand what the top hits mean.
36
+
37
+ BLAST search found {blast_count} hits.
38
+ Top hit: {top_hit_description} (E-value: {top_hit_evalue}, Identity: {top_hit_identity_pct}%)
39
+ """
40
+
41
+
42
+ def get_prompt(pipeline_type: str) -> str:
43
+ prompts = {
44
+ "protein_analysis": PROTEIN_ANALYSIS_PROMPT,
45
+ }
46
+ return prompts.get(pipeline_type, FALLBACK_PROMPT)
bioai-platform/backend/app/config.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+
7
+ class Settings:
8
+ GROQ_API_KEY: str = os.getenv("GROQ_API_KEY", "")
9
+ REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379")
10
+ SUPABASE_URL: str = os.getenv("SUPABASE_URL", "")
11
+ SUPABASE_SERVICE_ROLE_KEY: str = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
12
+ CELERY_BROKER_URL: str = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/1")
13
+ CELERY_RESULT_BACKEND: str = os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/2")
14
+ EBI_BASE_URL: str = "https://www.ebi.ac.uk/Tools/services/rest/ncbiblast"
15
+ UNIPROT_BASE_URL: str = "https://rest.uniprot.org/uniprotkb"
16
+ ALPHAFOLD_DB_URL: str = "https://alphafold.ebi.ac.uk/api/prediction"
17
+ DAILY_LIMIT: int = 10
18
+ DEFAULT_MODEL: str = os.getenv("DEFAULT_MODEL", "groq/llama-3.3-70b-versatile")
19
+ PRO_MODEL: str = os.getenv("PRO_MODEL", "claude-sonnet-4-20250514")
20
+
21
+
22
+ settings = Settings()
bioai-platform/backend/app/main.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ load_dotenv()
3
+
4
+ from fastapi import FastAPI, Request
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.responses import JSONResponse
7
+ from slowapi import Limiter, _rate_limit_exceeded_handler
8
+ from slowapi.util import get_remote_address
9
+ from slowapi.errors import RateLimitExceeded
10
+ from app.routers import pipelines, results, ai, jobs, export, share, waitlist, profile
11
+ from app.services.cache import init_redis
12
+ from app.tools.registration import register_all_tools
13
+
14
+ limiter = Limiter(key_func=get_remote_address, default_limits=["30/minute"])
15
+
16
+ app = FastAPI(title="Bio Nexus API", version="0.1.0")
17
+ app.state.limiter = limiter
18
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
19
+
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["http://localhost:3000", "http://localhost:3001"],
23
+ allow_credentials=True,
24
+ allow_methods=["*"],
25
+ allow_headers=["*"],
26
+ )
27
+
28
+ app.include_router(pipelines.router, prefix="/api/pipelines", tags=["pipelines"])
29
+ app.include_router(results.router, prefix="/api/results", tags=["results"])
30
+ app.include_router(ai.router, prefix="/api/ai", tags=["ai"])
31
+ app.include_router(jobs.router, prefix="/api/jobs", tags=["jobs"])
32
+ app.include_router(export.router, prefix="/api/export", tags=["export"])
33
+ app.include_router(share.router, prefix="/api/share", tags=["share"])
34
+ app.include_router(waitlist.router, prefix="/api/waitlist", tags=["waitlist"])
35
+ app.include_router(profile.router, prefix="/api/profile", tags=["profile"])
36
+
37
+
38
+ @app.on_event("startup")
39
+ async def startup():
40
+ init_redis()
41
+ register_all_tools()
42
+
43
+
44
+ @app.get("/health")
45
+ async def health():
46
+ return {"status": "ok"}
47
+
48
+
49
+ @app.exception_handler(Exception)
50
+ async def global_exception_handler(request: Request, exc: Exception):
51
+ return JSONResponse(
52
+ status_code=500,
53
+ content={"detail": "Internal server error"},
54
+ )
bioai-platform/backend/app/pipeline/__init__.py ADDED
File without changes
bioai-platform/backend/app/pipeline/assembler.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+
4
+ class ContextAssembler:
5
+ def assemble(
6
+ self,
7
+ sequence: str,
8
+ blast_result: dict,
9
+ uniprot_result: dict | None,
10
+ alphafold_result: dict | None,
11
+ ) -> dict:
12
+ context = {
13
+ "query": {
14
+ "sequence": sequence,
15
+ "length": len([c for c in sequence if c.isalpha()]),
16
+ },
17
+ "blast": self._summarize_blast(blast_result),
18
+ "uniprot": self._summarize_uniprot(uniprot_result) if uniprot_result else None,
19
+ "alphafold": alphafold_result,
20
+ }
21
+ return context
22
+
23
+ def _summarize_blast(self, blast_result: dict) -> dict:
24
+ hits = blast_result.get("hits", [])
25
+ summary = {
26
+ "count": len(hits),
27
+ "source": blast_result.get("source", "EBI BLAST"),
28
+ "database": blast_result.get("database", "swissprot"),
29
+ }
30
+ if hits:
31
+ best = hits[0]
32
+ summary["top_hit"] = {
33
+ "accession": best.get("accession", ""),
34
+ "description": best.get("description", ""),
35
+ "evalue": best.get("evalue", 0),
36
+ "identity_pct": best.get("identity_pct", 0),
37
+ "bit_score": best.get("bit_score", 0),
38
+ "alignment_length": best.get("alignment_length", 0),
39
+ }
40
+ summary["hits"] = [
41
+ {
42
+ "accession": h.get("accession", ""),
43
+ "description": h.get("description", ""),
44
+ "evalue": h.get("evalue", 0),
45
+ "identity_pct": h.get("identity_pct", 0),
46
+ "bit_score": h.get("bit_score", 0),
47
+ }
48
+ for h in hits[:10]
49
+ ]
50
+ return summary
51
+
52
+ def _summarize_uniprot(self, uniprot_result: dict) -> dict:
53
+ return {
54
+ "accession": uniprot_result.get("accession", ""),
55
+ "full_name": uniprot_result.get("full_name", ""),
56
+ "organism": uniprot_result.get("organism", ""),
57
+ "gene_names": uniprot_result.get("gene_names", []),
58
+ "functions": uniprot_result.get("functions", []),
59
+ "keywords": uniprot_result.get("keywords", []),
60
+ "subcellular_locations": uniprot_result.get("subcellular_locations", []),
61
+ "pdb_ids": uniprot_result.get("pdb_ids", []),
62
+ "features": [
63
+ f for f in (uniprot_result.get("features", []) or [])
64
+ if f.get("type") in ("ACTIVE_SITE", "BINDING", "MUTAGENESIS")
65
+ ],
66
+ "go_terms": uniprot_result.get("go_terms", []),
67
+ "sequence_length": uniprot_result.get("sequence_length", 0),
68
+ }
bioai-platform/backend/app/pipeline/definitions/__init__.py ADDED
File without changes
bioai-platform/backend/app/pipeline/definitions/protein_analysis.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+
3
+ PIPELINE_DEFINITION = {
4
+ "id": "protein_analysis",
5
+ "name": "Protein Sequence Analysis",
6
+ "description": "Analyze a protein sequence: BLAST against Swiss-Prot, fetch UniProt annotations, retrieve AlphaFold structure",
7
+ "input_type": "sequence",
8
+ "input_label": "Protein sequence (FASTA or plain)",
9
+ "steps": ["blast", "uniprot", "alphafold"],
10
+ "default_database": "uniprotkb_swissprot",
11
+ "default_max_hits": 10,
12
+ }
13
+
14
+
15
+ def get_pipeline_definition() -> dict:
16
+ return PIPELINE_DEFINITION
bioai-platform/backend/app/pipeline/engine.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+ from app.pipeline.registry import registry
3
+ from app.pipeline.assembler import ContextAssembler
4
+ from app.tools.blast import BlastTool
5
+ from app.tools.uniprot import UniprotTool
6
+ from app.tools.alphafold import AlphaFoldTool
7
+
8
+
9
+ class PipelineEngine:
10
+ def __init__(self):
11
+ self.assembler = ContextAssembler()
12
+ self.blast_tool = BlastTool()
13
+ self.uniprot_tool = UniprotTool()
14
+ self.alphafold_tool = AlphaFoldTool()
15
+
16
+ async def execute(self, sequence: str, database: str = "uniprotkb_swissprot", max_hits: int = 10) -> dict:
17
+ blast_result = await self.blast_tool.run({
18
+ "sequence": sequence,
19
+ "database": database,
20
+ "max_hits": max_hits,
21
+ })
22
+
23
+ if "error" in blast_result:
24
+ return {"error": blast_result["error"], "steps_completed": []}
25
+
26
+ top_accession = ""
27
+ if blast_result.get("hits"):
28
+ top_accession = blast_result["hits"][0].get("accession", "")
29
+
30
+ uniprot_result = None
31
+ if top_accession:
32
+ uniprot_result = await self.uniprot_tool.run({"accession": top_accession})
33
+
34
+ alphafold_result = None
35
+ if top_accession and uniprot_result and "error" not in uniprot_result:
36
+ alphafold_result = await self.alphafold_tool.run({"uniprot_accession": top_accession})
37
+
38
+ context = self.assembler.assemble(sequence, blast_result, uniprot_result, alphafold_result)
39
+
40
+ return {
41
+ "context": context,
42
+ "steps_completed": ["blast", "uniprot", "alphafold"],
43
+ "blast_raw": blast_result,
44
+ "uniprot_raw": uniprot_result,
45
+ "alphafold_raw": alphafold_result,
46
+ }
bioai-platform/backend/app/pipeline/registry.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+ from app.tools.base import BaseTool
3
+
4
+
5
+ class ToolRegistry:
6
+ def __init__(self):
7
+ self._tools: dict[str, BaseTool] = {}
8
+
9
+ def register(self, tool: BaseTool):
10
+ self._tools[tool.name] = tool
11
+
12
+ def get(self, name: str) -> BaseTool | None:
13
+ return self._tools.get(name)
14
+
15
+ def list(self) -> list[str]:
16
+ return list(self._tools.keys())
17
+
18
+
19
+ registry = ToolRegistry()
bioai-platform/backend/app/routers/__init__.py ADDED
File without changes
bioai-platform/backend/app/routers/ai.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from fastapi import APIRouter, HTTPException, Depends
4
+ from fastapi.responses import StreamingResponse
5
+ from pydantic import BaseModel
6
+ from app.ai.interpreter import interpret_stream
7
+ from app.ai.llm_client import llm_client
8
+ from app.services.rate_limit import check_daily_limit
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ class InterpretRequest(BaseModel):
14
+ pipeline_type: str = "protein_analysis"
15
+ context: dict = {}
16
+
17
+
18
+ @router.post("/interpret", dependencies=[Depends(check_daily_limit)])
19
+ async def interpret_full_context(req: InterpretRequest):
20
+ if not llm_client.has_api_key():
21
+ raise HTTPException(status_code=502, detail="GROQ_API_KEY is not configured")
22
+
23
+ prompt = llm_client.build_prompt(req.pipeline_type, req.context)
24
+ return {"prompt": prompt, "context_size": len(json.dumps(req.context))}
25
+
26
+
27
+ @router.post("/interpret/stream", dependencies=[Depends(check_daily_limit)])
28
+ async def interpret_stream_endpoint(req: InterpretRequest):
29
+ return StreamingResponse(
30
+ interpret_stream(req.pipeline_type, req.context),
31
+ media_type="text/event-stream",
32
+ )
bioai-platform/backend/app/routers/export.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from fastapi.responses import Response
3
+ from pydantic import BaseModel
4
+ from app.services.export import export_blast_pdf, export_uniprot_pdf
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ class BlastExportRequest(BaseModel):
10
+ result: dict
11
+ sequence: str = ""
12
+
13
+
14
+ class UniprotExportRequest(BaseModel):
15
+ data: dict
16
+
17
+
18
+ @router.post("/blast")
19
+ async def export_blast(req: BlastExportRequest):
20
+ try:
21
+ pdf = export_blast_pdf(req.result, req.sequence)
22
+ return Response(content=pdf, media_type="application/pdf", headers={
23
+ "Content-Disposition": "attachment; filename=blast_results.pdf"
24
+ })
25
+ except Exception as e:
26
+ raise HTTPException(status_code=500, detail=str(e))
27
+
28
+
29
+ @router.post("/uniprot")
30
+ async def export_uniprot(req: UniprotExportRequest):
31
+ try:
32
+ pdf = export_uniprot_pdf(req.data)
33
+ return Response(content=pdf, media_type="application/pdf", headers={
34
+ "Content-Disposition": f"attachment; filename=uniprot_{req.data.get('accession', 'protein')}.pdf"
35
+ })
36
+ except Exception as e:
37
+ raise HTTPException(status_code=500, detail=str(e))
38
+
39
+
40
+ @router.post("/pipeline/{job_id}")
41
+ async def export_pipeline(job_id: str):
42
+ raise HTTPException(status_code=501, detail="Pipeline PDF export coming in Phase 1.5")
bioai-platform/backend/app/routers/jobs.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends
2
+ from pydantic import BaseModel
3
+ from datetime import datetime, timezone
4
+ from app.services.supabase import get_supabase
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.get("/count")
10
+ async def job_count(user_id: str = ""):
11
+ supabase = get_supabase()
12
+ today = datetime.now(timezone.utc).date().isoformat()
13
+ result = supabase.table("jobs").select("id", count="exact").gte("created_at", today).execute()
14
+ count = result.count or 0
15
+ return {"count": count, "limit": 10, "remaining": max(0, 10 - count)}
16
+
17
+
18
+ @router.get("")
19
+ async def list_jobs(user_id: str = ""):
20
+ supabase = get_supabase()
21
+ result = supabase.table("jobs").select("*").order("created_at", desc=True).limit(50).execute()
22
+ return {"jobs": result.data or []}
23
+
24
+
25
+ @router.get("/{job_id}")
26
+ async def get_job(job_id: str):
27
+ supabase = get_supabase()
28
+ result = supabase.table("jobs").select("*").eq("id", job_id).execute()
29
+ if not result.data:
30
+ raise HTTPException(status_code=404, detail="Job not found")
31
+ return result.data[0]
32
+
33
+
34
+ @router.delete("/{job_id}")
35
+ async def delete_job(job_id: str):
36
+ supabase = get_supabase()
37
+ supabase.table("jobs").delete().eq("id", job_id).execute()
38
+ return {"status": "deleted"}
bioai-platform/backend/app/routers/pipelines.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends
2
+ from pydantic import BaseModel
3
+ from app.services.validators import validate_fasta, ValidationResult
4
+ from app.pipeline.engine import PipelineEngine
5
+ from app.pipeline.definitions.protein_analysis import get_pipeline_definition
6
+ from app.services.supabase import get_supabase
7
+ from app.services.rate_limit import check_daily_limit
8
+ from app.workers.celery_app import celery_app
9
+ from app.workers.pipeline_worker import run_pipeline_sync
10
+ from datetime import datetime, timezone
11
+ import uuid
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ class PipelineRunRequest(BaseModel):
17
+ sequence: str
18
+ pipeline_type: str = "protein_analysis"
19
+ database: str = "uniprotkb_swissprot"
20
+ max_hits: int = 10
21
+
22
+
23
+ @router.post("/run", dependencies=[Depends(check_daily_limit)])
24
+ async def run_pipeline(req: PipelineRunRequest):
25
+ validation = validate_fasta(req.sequence, "blast")
26
+ if not validation.valid:
27
+ raise HTTPException(status_code=400, detail=validation.error)
28
+
29
+ seq = str(validation.sequences[0].seq).upper().replace(" ", "")
30
+
31
+ job_id = str(uuid.uuid4())
32
+ supabase = get_supabase()
33
+
34
+ supabase.table("jobs").insert({
35
+ "id": job_id,
36
+ "tool": "pipeline",
37
+ "query_preview": seq[:100],
38
+ "status": "pending",
39
+ "pipeline_type": req.pipeline_type,
40
+ "steps_completed": [],
41
+ "context_json": None,
42
+ "created_at": datetime.now(timezone.utc).isoformat(),
43
+ }).execute()
44
+
45
+ if celery_app is not None:
46
+ from app.workers.pipeline_worker import run_pipeline_job
47
+ run_pipeline_job.delay(job_id, seq, req.database, req.max_hits)
48
+ else:
49
+ import threading
50
+ t = threading.Thread(target=run_pipeline_sync, args=(job_id, seq, req.database, req.max_hits), daemon=True)
51
+ t.start()
52
+
53
+ return {"job_id": job_id, "status": "pending"}
54
+
55
+
56
+ @router.get("/definitions")
57
+ async def list_pipeline_definitions():
58
+ return {"pipelines": [get_pipeline_definition()]}
59
+
60
+
61
+ @router.get("/{pipeline_type}/definition")
62
+ async def get_pipeline_definition_endpoint(pipeline_type: str):
63
+ if pipeline_type == "protein_analysis":
64
+ return get_pipeline_definition()
65
+ raise HTTPException(status_code=404, detail=f"Unknown pipeline: {pipeline_type}")
bioai-platform/backend/app/routers/profile.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends
2
+ from pydantic import BaseModel
3
+ from app.services.supabase import get_supabase
4
+
5
+ router = APIRouter()
6
+
7
+ class ProfileUpdate(BaseModel):
8
+ full_name: str = ""
9
+ institution: str = ""
10
+
11
+ @router.get("")
12
+ async def get_profile(user_id: str = ""):
13
+ supabase = get_supabase()
14
+ result = supabase.table("profiles").select("*").limit(1).execute()
15
+ if result.data:
16
+ return result.data[0]
17
+ return {"error": "Profile not found"}
18
+
19
+ @router.put("")
20
+ async def update_profile(profile: ProfileUpdate, user_id: str = ""):
21
+ supabase = get_supabase()
22
+ data = {}
23
+ if profile.full_name: data["full_name"] = profile.full_name
24
+ if profile.institution: data["institution"] = profile.institution
25
+ if data:
26
+ result = supabase.table("profiles").update(data).eq("id", user_id).execute()
27
+ return {"status": "updated", "data": result.data[0] if result.data else data}
28
+ return {"status": "no changes"}
bioai-platform/backend/app/routers/results.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends
2
+ from app.services.supabase import get_supabase
3
+ from app.ai.interpreter import interpret_stream
4
+ from fastapi.responses import StreamingResponse
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.get("/{job_id}")
10
+ async def get_result(job_id: str):
11
+ supabase = get_supabase()
12
+ result = supabase.table("jobs").select("*").eq("id", job_id).execute()
13
+ if not result.data:
14
+ raise HTTPException(status_code=404, detail="Job not found")
15
+ return result.data[0]
16
+
17
+
18
+ @router.get("/{job_id}/stream")
19
+ async def stream_result(job_id: str):
20
+ supabase = get_supabase()
21
+ job = supabase.table("jobs").select("*").eq("id", job_id).execute()
22
+ if not job.data:
23
+ raise HTTPException(status_code=404, detail="Job not found")
24
+
25
+ job_data = job.data[0]
26
+ context = job_data.get("context_json")
27
+ pipeline_type = job_data.get("pipeline_type", "protein_analysis")
28
+
29
+ if not context:
30
+ raise HTTPException(status_code=400, detail="Job has not completed yet")
31
+
32
+ return StreamingResponse(
33
+ interpret_stream(pipeline_type, context),
34
+ media_type="text/event-stream",
35
+ )
bioai-platform/backend/app/routers/share.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from app.services.supabase import get_supabase
3
+
4
+ router = APIRouter()
5
+
6
+
7
+ @router.get("/{token}")
8
+ async def get_shared_result(token: str):
9
+ supabase = get_supabase()
10
+ result = supabase.table("jobs").select("*").eq("share_token", token).execute()
11
+ if not result.data:
12
+ raise HTTPException(status_code=404, detail="Shared result not found")
13
+ return result.data[0]
bioai-platform/backend/app/routers/waitlist.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ import httpx
4
+ from app.config import settings
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ class WaitlistEntry(BaseModel):
10
+ email: str
11
+
12
+
13
+ @router.post("")
14
+ async def join_waitlist(entry: WaitlistEntry):
15
+ url = f"{settings.SUPABASE_URL}/rest/v1/waitlist"
16
+ headers = {
17
+ "apikey": settings.SUPABASE_SERVICE_ROLE_KEY,
18
+ "Authorization": f"Bearer {settings.SUPABASE_SERVICE_ROLE_KEY}",
19
+ "Content-Type": "application/json",
20
+ "Prefer": "return=minimal",
21
+ }
22
+ async with httpx.AsyncClient() as client:
23
+ resp = await client.post(url, headers=headers, json={"email": entry.email})
24
+ if resp.status_code == 409:
25
+ raise HTTPException(status_code=409, detail="Already on waitlist")
26
+ if resp.status_code >= 400:
27
+ raise HTTPException(status_code=500, detail=resp.text)
28
+ return {"status": "added", "email": entry.email}
bioai-platform/backend/app/services/__init__.py ADDED
File without changes
bioai-platform/backend/app/services/cache.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import redis
2
+ import hashlib
3
+ import json
4
+ import functools
5
+ from typing import Callable, Any
6
+ from app.config import settings
7
+
8
+ _redis = None
9
+
10
+
11
+ def init_redis():
12
+ global _redis
13
+ try:
14
+ _redis = redis.from_url(settings.REDIS_URL, decode_responses=True)
15
+ _redis.ping()
16
+ except Exception:
17
+ _redis = None
18
+
19
+
20
+ def get_redis():
21
+ return _redis
22
+
23
+
24
+ def cache_get(key: str) -> str | None:
25
+ r = get_redis()
26
+ if r:
27
+ return r.get(key)
28
+ return None
29
+
30
+
31
+ def cache_set(key: str, value: str, ttl: int = 86400):
32
+ r = get_redis()
33
+ if r:
34
+ r.setex(key, ttl, value)
35
+
36
+
37
+ def ttl_cache(ttl: int = 86400, prefix: str = "cache"):
38
+ def decorator(func: Callable) -> Callable:
39
+ @functools.wraps(func)
40
+ async def wrapper(self, input: dict, *args, **kwargs):
41
+ raw = json.dumps(input, sort_keys=True)
42
+ key_hash = hashlib.sha256(raw.encode()).hexdigest()[:16]
43
+ cache_key = f"{prefix}:{key_hash}"
44
+
45
+ cached = cache_get(cache_key)
46
+ if cached is not None:
47
+ try:
48
+ return json.loads(cached)
49
+ except (json.JSONDecodeError, TypeError):
50
+ pass
51
+
52
+ result = await func(self, input, *args, **kwargs)
53
+ try:
54
+ cache_set(cache_key, json.dumps(result), ttl=ttl)
55
+ except (TypeError, ValueError):
56
+ pass
57
+ return result
58
+
59
+ return wrapper
60
+
61
+ return decorator
bioai-platform/backend/app/services/export.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from io import BytesIO
2
+ from reportlab.lib.pagesizes import A4
3
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
4
+ from reportlab.lib.units import inch
5
+ from reportlab.lib import colors
6
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
7
+
8
+ def export_blast_pdf(result: dict, sequence: str = "") -> bytes:
9
+ buffer = BytesIO()
10
+ doc = SimpleDocTemplate(buffer, pagesize=A4, topMargin=0.75 * inch, bottomMargin=0.75 * inch)
11
+ styles = getSampleStyleSheet()
12
+ elements = []
13
+
14
+ title_style = ParagraphStyle("Title2", parent=styles["Title"], fontSize=16, spaceAfter=12)
15
+ heading_style = ParagraphStyle("Heading", parent=styles["Heading2"], fontSize=12, spaceAfter=6, textColor=colors.HexColor("#16a34a"))
16
+ body_style = ParagraphStyle("Body", parent=styles["BodyText"], fontSize=10, spaceAfter=6)
17
+
18
+ elements.append(Paragraph("Bio Nexus \u2014 BLAST Results", title_style))
19
+ elements.append(Spacer(1, 12))
20
+
21
+ if sequence:
22
+ preview = sequence[:100] + ("..." if len(sequence) > 100 else "")
23
+ elements.append(Paragraph(f"<b>Query:</b> {preview}", body_style))
24
+
25
+ elements.append(Paragraph(f"<b>Hits found:</b> {result.get('count', 0)}", body_style))
26
+ elements.append(Spacer(1, 12))
27
+
28
+ hits = result.get("hits", [])
29
+ if not hits:
30
+ elements.append(Paragraph("No hits found.", body_style))
31
+ doc.build(elements)
32
+ return buffer.getvalue()
33
+
34
+ table_data = [["#", "Accession", "Description", "E-value", "Identity", "Coverage"]]
35
+ for i, hit in enumerate(hits[:20], 1):
36
+ desc = (hit.get("description", "")[:60] + "...") if len(hit.get("description", "")) > 60 else hit.get("description", "")
37
+ table_data.append([
38
+ str(i),
39
+ hit.get("accession", ""),
40
+ desc,
41
+ f"{hit.get('evalue', 0):.1e}",
42
+ f"{hit.get('identity_pct', 0)}%",
43
+ f"{hit.get('coverage_pct', 0)}%",
44
+ ])
45
+
46
+ col_widths = [0.4 * inch, 1.0 * inch, 2.5 * inch, 1.0 * inch, 0.8 * inch, 0.8 * inch]
47
+ t = Table(table_data, colWidths=col_widths, repeatRows=1)
48
+ t.setStyle(TableStyle([
49
+ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#16a34a")),
50
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
51
+ ("FONTSIZE", (0, 0), (-1, 0), 9),
52
+ ("FONTSIZE", (0, 1), (-1, -1), 8),
53
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
54
+ ("ALIGN", (0, 0), (-1, -1), "LEFT"),
55
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
56
+ ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#e5e7eb")),
57
+ ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f9fafb")]),
58
+ ("TOPPADDING", (0, 0), (-1, -1), 4),
59
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
60
+ ("LEFTPADDING", (0, 0), (-1, -1), 4),
61
+ ("RIGHTPADDING", (0, 0), (-1, -1), 4),
62
+ ]))
63
+ elements.append(t)
64
+ elements.append(Spacer(1, 16))
65
+
66
+ elements.append(Paragraph("Significance Guide", heading_style))
67
+ elements.append(Paragraph("\u2022 <b>E-value &lt; 1e-50</b>: Highly significant match", body_style))
68
+ elements.append(Paragraph("\u2022 <b>E-value 1e-5 to 1e-50</b>: Significant match", body_style))
69
+ elements.append(Paragraph("\u2022 <b>E-value &gt; 1e-5</b>: Weak or non-significant", body_style))
70
+ elements.append(Spacer(1, 8))
71
+ elements.append(Paragraph(f"<i>Generated by Bio Nexus Platform \u2014 {result.get('count', 0)} hits</i>", body_style))
72
+
73
+ doc.build(elements)
74
+ return buffer.getvalue()
75
+
76
+
77
+ def export_uniprot_pdf(data: dict) -> bytes:
78
+ buffer = BytesIO()
79
+ doc = SimpleDocTemplate(buffer, pagesize=A4, topMargin=0.75 * inch, bottomMargin=0.75 * inch)
80
+ styles = getSampleStyleSheet()
81
+ elements = []
82
+
83
+ title_style = ParagraphStyle("Title2", parent=styles["Title"], fontSize=16, spaceAfter=12)
84
+ heading_style = ParagraphStyle("Heading", parent=styles["Heading2"], fontSize=12, spaceAfter=6, textColor=colors.HexColor("#16a34a"))
85
+ body_style = ParagraphStyle("Body", parent=styles["BodyText"], fontSize=10, spaceAfter=6)
86
+
87
+ elements.append(Paragraph("Bio Nexus \u2014 UniProt Annotation", title_style))
88
+ elements.append(Spacer(1, 12))
89
+
90
+ elements.append(Paragraph(f"<b>Protein:</b> {data.get('full_name', 'Unknown')}", body_style))
91
+ elements.append(Paragraph(f"<b>Accession:</b> {data.get('accession', '')}", body_style))
92
+ elements.append(Paragraph(f"<b>Organism:</b> {data.get('organism', 'Unknown')}", body_style))
93
+
94
+ genes = data.get("gene_names", [])
95
+ if genes:
96
+ elements.append(Paragraph(f"<b>Gene:</b> {', '.join(genes)}", body_style))
97
+
98
+ ec = data.get("ec_number", "")
99
+ if ec:
100
+ elements.append(Paragraph(f"<b>EC Number:</b> {ec}", body_style))
101
+
102
+ elements.append(Paragraph(f"<b>Sequence Length:</b> {data.get('sequence_length', 0)} amino acids", body_style))
103
+ elements.append(Spacer(1, 12))
104
+
105
+ functions = data.get("functions", [])
106
+ if functions:
107
+ elements.append(Paragraph("Function", heading_style))
108
+ for f in functions:
109
+ elements.append(Paragraph(f, body_style))
110
+
111
+ locations = data.get("subcellular_locations", [])
112
+ if locations:
113
+ elements.append(Paragraph("Subcellular Location", heading_style))
114
+ elements.append(Paragraph(", ".join(locations), body_style))
115
+
116
+ pdb_ids = data.get("pdb_ids", [])
117
+ if pdb_ids:
118
+ elements.append(Paragraph("PDB Structures", heading_style))
119
+ elements.append(Paragraph(", ".join(pdb_ids), body_style))
120
+
121
+ keywords = data.get("keywords", [])
122
+ if keywords:
123
+ elements.append(Paragraph("Keywords", heading_style))
124
+ elements.append(Paragraph(", ".join(keywords[:10]), body_style))
125
+
126
+ elements.append(Spacer(1, 12))
127
+ elements.append(Paragraph("<i>Generated by Bio Nexus Platform</i>", body_style))
128
+
129
+ doc.build(elements)
130
+ return buffer.getvalue()
bioai-platform/backend/app/services/rate_limit.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import date
2
+ from app.services.redis import get_redis
3
+ from app.config import settings
4
+
5
+
6
+ async def check_daily_limit(user_id: str = "anonymous") -> bool:
7
+ r = get_redis()
8
+ if not r:
9
+ return True
10
+
11
+ today = date.today().isoformat()
12
+ key = f"rate_limit:daily:{user_id}:{today}"
13
+ count = r.get(key)
14
+
15
+ if count is None:
16
+ r.setex(key, 86400, 1)
17
+ return True
18
+
19
+ if int(count) >= settings.DAILY_LIMIT:
20
+ return False
21
+
22
+ r.incr(key)
23
+ return True
24
+
25
+
26
+ class RateLimitExceededError(Exception):
27
+ pass
bioai-platform/backend/app/services/redis.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import redis
2
+ from app.config import settings
3
+
4
+ _r = None
5
+
6
+
7
+ def get_redis():
8
+ global _r
9
+ if _r is None:
10
+ try:
11
+ _r = redis.from_url(settings.REDIS_URL, decode_responses=True)
12
+ _r.ping()
13
+ except Exception:
14
+ _r = None
15
+ return _r
bioai-platform/backend/app/services/supabase.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from supabase import create_client, Client
2
+ from app.config import settings
3
+
4
+ _supabase: Client | None = None
5
+
6
+
7
+ def get_supabase() -> Client:
8
+ global _supabase
9
+ if _supabase is None:
10
+ _supabase = create_client(settings.SUPABASE_URL, settings.SUPABASE_SERVICE_ROLE_KEY)
11
+ return _supabase
bioai-platform/backend/app/services/validators.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from Bio import SeqIO
2
+ from io import StringIO
3
+ from dataclasses import dataclass, field
4
+ from typing import List
5
+
6
+
7
+ @dataclass
8
+ class ValidationResult:
9
+ valid: bool = True
10
+ error: str = ""
11
+ sequences: List = field(default_factory=list)
12
+
13
+
14
+ def validate_fasta(text: str, tool: str = "blast") -> ValidationResult:
15
+ if not text or not text.strip():
16
+ return ValidationResult(valid=False, error="Empty sequence")
17
+
18
+ # Try parsing as FASTA
19
+ try:
20
+ records = list(SeqIO.parse(StringIO(text), "fasta"))
21
+ except Exception:
22
+ records = []
23
+
24
+ if records:
25
+ for rec in records:
26
+ seq_str = str(rec.seq)
27
+ if len(seq_str) < 6:
28
+ return ValidationResult(valid=False, error=f"Sequence too short: {len(seq_str)} residues")
29
+ if not set(seq_str.upper()).issubset(set("ACDEFGHIKLMNPQRSTVWY")):
30
+ return ValidationResult(valid=False, error="Invalid amino acid characters found")
31
+ return ValidationResult(sequences=records)
32
+
33
+ # Plain sequence (no FASTA header)
34
+ clean = "".join(c for c in text if c.isalpha()).upper()
35
+ if len(clean) < 6:
36
+ return ValidationResult(valid=False, error=f"Sequence too short: {len(clean)} residues")
37
+ if not set(clean).issubset(set("ACDEFGHIKLMNPQRSTVWY")):
38
+ return ValidationResult(valid=False, error="Invalid amino acid characters found")
39
+
40
+ from Bio.Seq import Seq
41
+ from Bio.SeqRecord import SeqRecord
42
+ record = SeqRecord(Seq(clean), id="query", description="")
43
+ return ValidationResult(sequences=[record])
bioai-platform/backend/app/tools/__init__.py ADDED
File without changes
bioai-platform/backend/app/tools/alphafold.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ from typing import Any
3
+ from app.tools.base import BaseTool
4
+ from app.config import settings
5
+ from app.services.cache import ttl_cache
6
+
7
+
8
+ class AlphaFoldTool(BaseTool):
9
+ name = "alphafold"
10
+
11
+ @ttl_cache(ttl=86400, prefix="alphafold")
12
+ async def run(self, input: dict) -> dict:
13
+ uniprot_accession = input.get("uniprot_accession", "").strip()
14
+ if not uniprot_accession:
15
+ return {"error": "No UniProt accession provided", "structure_available": False}
16
+
17
+ result = await self._lookup(uniprot_accession)
18
+ return result
19
+
20
+ async def _lookup(self, accession: str) -> dict:
21
+ url = f"{settings.ALPHAFOLD_DB_URL}/{accession}"
22
+ async with httpx.AsyncClient(timeout=15) as client:
23
+ resp = await client.get(url)
24
+ if resp.status_code == 404:
25
+ return {
26
+ "uniprot_accession": accession,
27
+ "structure_available": False,
28
+ "message": "No AlphaFold prediction available for this protein",
29
+ "pdb_url": None,
30
+ "cif_url": None,
31
+ "confidence": None,
32
+ }
33
+ resp.raise_for_status()
34
+ data = resp.json()
35
+ entry = data[0] if isinstance(data, list) else data
36
+ return {
37
+ "uniprot_accession": accession,
38
+ "structure_available": True,
39
+ "pdb_url": entry.get("pdbUrl", ""),
40
+ "cif_url": entry.get("cifUrl", ""),
41
+ "confidence": entry.get("confidenceScore", None),
42
+ "model_created_date": entry.get("modelCreatedDate", ""),
43
+ "latest_version": entry.get("latestVersion", 0),
44
+ }
bioai-platform/backend/app/tools/base.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+
5
+ class BaseTool(ABC):
6
+ name: str = ""
7
+
8
+ @abstractmethod
9
+ async def run(self, input: dict) -> dict:
10
+ ...
11
+
12
+ def requires_input_from(self) -> list[str]:
13
+ return []
14
+
15
+ def provides_output_to(self) -> list[str]:
16
+ return []
bioai-platform/backend/app/tools/blast.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import asyncio
3
+ import hashlib
4
+ import json
5
+ from typing import Any
6
+ from app.tools.base import BaseTool
7
+ from app.config import settings
8
+ from app.services.cache import ttl_cache
9
+
10
+
11
+ class BlastTool(BaseTool):
12
+ name = "blast"
13
+
14
+ POLL_INTERVAL = 2.0
15
+ MAX_POLL_TIME = 120
16
+
17
+ @ttl_cache(ttl=86400, prefix="blast")
18
+ async def run(self, input: dict) -> dict:
19
+ sequence = input.get("sequence", "").strip()
20
+ database = input.get("database", "uniprotkb_swissprot")
21
+ program = input.get("program", "blastp")
22
+ max_hits = input.get("max_hits", 10)
23
+
24
+ job_id = await self._submit(sequence, program, database)
25
+ status = await self._poll(job_id)
26
+ if status != "FINISHED":
27
+ return {"error": f"BLAST job {job_id} ended with status {status}", "hits": []}
28
+
29
+ hits = await self._fetch_results(job_id)
30
+ parsed = self._parse_hits(hits, max_hits)
31
+ return {"hits": parsed, "count": len(parsed), "source": "EBI BLAST", "database": database}
32
+
33
+ async def _submit(self, sequence: str, program: str, database: str) -> str:
34
+ async with httpx.AsyncClient(timeout=30) as client:
35
+ resp = await client.post(
36
+ f"{settings.EBI_BASE_URL}/run",
37
+ data={"email": "user@bio-nexus.app", "sequence": sequence, "program": program, "database": database},
38
+ )
39
+ resp.raise_for_status()
40
+ return resp.text.strip()
41
+
42
+ async def _poll(self, job_id: str) -> str:
43
+ start = asyncio.get_event_loop().time()
44
+ async with httpx.AsyncClient(timeout=10) as client:
45
+ while True:
46
+ elapsed = asyncio.get_event_loop().time() - start
47
+ if elapsed > self.MAX_POLL_TIME:
48
+ return "TIMEOUT"
49
+ resp = await client.get(f"{settings.EBI_BASE_URL}/status/{job_id}")
50
+ resp.raise_for_status()
51
+ status = resp.text.strip()
52
+ if status in ("FINISHED", "ERROR", "FAILED"):
53
+ return status
54
+ await asyncio.sleep(self.POLL_INTERVAL)
55
+
56
+ async def _fetch_results(self, job_id: str) -> list[dict]:
57
+ async with httpx.AsyncClient(timeout=30) as client:
58
+ resp = await client.get(f"{settings.EBI_BASE_URL}/result/{job_id}/json")
59
+ resp.raise_for_status()
60
+ data = resp.json()
61
+ return data.get("hits", [])
62
+
63
+ def _parse_hits(self, raw_hits: list[dict], max_hits: int) -> list[dict]:
64
+ parsed = []
65
+ for hit in raw_hits[:max_hits]:
66
+ hsps = hit.get("hsps", [{}])[0] if hit.get("hsps") else {}
67
+ parsed.append({
68
+ "accession": hit.get("hit_acc", ""),
69
+ "id": hit.get("hit_id", ""),
70
+ "description": hit.get("hit_desc", ""),
71
+ "evalue": hsps.get("hsp_expect", 0),
72
+ "bit_score": hsps.get("hsp_bit_score", 0),
73
+ "identity_pct": hsps.get("hsp_identity", 0),
74
+ "alignment_length": hsps.get("hsp_align_len", 0),
75
+ "query_from": hsps.get("hsp_query_from", 0),
76
+ "query_to": hsps.get("hsp_query_to", 0),
77
+ "hit_from": hsps.get("hsp_hit_from", 0),
78
+ "hit_to": hsps.get("hsp_hit_to", 0),
79
+ })
80
+ return parsed
bioai-platform/backend/app/tools/registration.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from app.tools.blast import BlastTool
2
+ from app.tools.uniprot import UniprotTool
3
+ from app.tools.alphafold import AlphaFoldTool
4
+ from app.pipeline.registry import registry
5
+
6
+
7
+ def register_all_tools():
8
+ registry.register(BlastTool())
9
+ registry.register(UniprotTool())
10
+ registry.register(AlphaFoldTool())
bioai-platform/backend/app/tools/uniprot.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ from typing import Any
3
+ from app.tools.base import BaseTool
4
+ from app.config import settings
5
+ from app.services.cache import ttl_cache
6
+
7
+
8
+ class UniprotTool(BaseTool):
9
+ name = "uniprot"
10
+
11
+ @ttl_cache(ttl=86400, prefix="uniprot")
12
+ async def run(self, input: dict) -> dict:
13
+ accession = input.get("accession", "").strip()
14
+ if not accession:
15
+ return {"error": "No accession provided"}
16
+
17
+ data = await self._fetch(accession)
18
+ if "error" in data:
19
+ return data
20
+
21
+ return {
22
+ "accession": data.get("primaryAccession", ""),
23
+ "full_name": self._extract_name(data),
24
+ "ec_number": (data.get("proteinDescription", {}) or {}).get("ecNumbers", [{}])[0].get("ecNumber", "") if data.get("proteinDescription") else "",
25
+ "gene_names": [g.get("geneName", {}).get("value", "") for g in (data.get("genes") or []) if g.get("geneName")],
26
+ "organism": ((data.get("organism", {}) or {}).get("scientificName", "")),
27
+ "functions": self._extract_functions(data),
28
+ "keywords": [kw.get("name", "") for kw in (data.get("keywords") or [])],
29
+ "sequence_length": ((data.get("sequence", {}) or {}).get("length", 0)),
30
+ "subcellular_locations": self._extract_locations(data),
31
+ "pdb_ids": self._extract_pdb(data),
32
+ "features": self._extract_features(data),
33
+ "go_terms": self._extract_go_terms(data),
34
+ }
35
+
36
+ async def _fetch(self, accession: str) -> dict:
37
+ url = f"{settings.UNIPROT_BASE_URL}/{accession}"
38
+ async with httpx.AsyncClient(timeout=15) as client:
39
+ resp = await client.get(url, params={"format": "json"})
40
+ if resp.status_code == 404:
41
+ return {"error": f"Accession {accession} not found"}
42
+ resp.raise_for_status()
43
+ return resp.json()
44
+
45
+ def _extract_name(self, data: dict) -> str:
46
+ desc = data.get("proteinDescription", {}) or {}
47
+ rec_name = desc.get("recommendedName", {}) or {}
48
+ return (rec_name.get("fullName", {}) or {}).get("value", "")
49
+
50
+ def _extract_functions(self, data: dict) -> list[str]:
51
+ comments = data.get("comments") or []
52
+ funcs = []
53
+ for c in comments:
54
+ if c.get("commentType") == "FUNCTION":
55
+ texts = c.get("texts") or []
56
+ for t in texts:
57
+ val = (t.get("value") or "").strip()
58
+ if val:
59
+ funcs.append(val)
60
+ return funcs
61
+
62
+ def _extract_locations(self, data: dict) -> list[str]:
63
+ comments = data.get("comments") or []
64
+ locs = []
65
+ for c in comments:
66
+ if c.get("commentType") == "SUBCELLULAR_LOCATION":
67
+ subcels = c.get("subcellularLocations") or []
68
+ for s in subcels:
69
+ loc = (s.get("location", {}) or {}).get("value", "")
70
+ if loc:
71
+ locs.append(loc)
72
+ return locs
73
+
74
+ def _extract_pdb(self, data: dict) -> list[str]:
75
+ refs = data.get("uniProtKBCrossReferences") or []
76
+ pdbs = []
77
+ for r in refs:
78
+ if r.get("database") == "PDB":
79
+ pdbs.append(r.get("id", ""))
80
+ return pdbs
81
+
82
+ def _extract_features(self, data: dict) -> list[dict]:
83
+ features = data.get("features") or []
84
+ result = []
85
+ for f in features:
86
+ result.append({
87
+ "type": f.get("type", ""),
88
+ "description": f.get("description", ""),
89
+ "begin": (f.get("location", {}) or {}).get("start", {}).get("value"),
90
+ "end": (f.get("location", {}) or {}).get("end", {}).get("value"),
91
+ })
92
+ return result
93
+
94
+ def _extract_go_terms(self, data: dict) -> list[str]:
95
+ refs = data.get("uniProtKBCrossReferences") or []
96
+ go = []
97
+ for r in refs:
98
+ if r.get("database") == "GO":
99
+ term = r.get("properties", [{}])[0].get("value", "") if r.get("properties") else ""
100
+ if term:
101
+ go.append(term)
102
+ return go
bioai-platform/backend/app/workers/__init__.py ADDED
File without changes
bioai-platform/backend/app/workers/celery_app.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ try:
2
+ from celery import Celery
3
+ from app.config import settings
4
+
5
+ celery_app = Celery(
6
+ "bio_nexus",
7
+ broker=settings.CELERY_BROKER_URL,
8
+ backend=settings.CELERY_RESULT_BACKEND,
9
+ )
10
+
11
+ celery_app.conf.update(
12
+ task_serializer="json",
13
+ accept_content=["json"],
14
+ result_serializer="json",
15
+ timezone="UTC",
16
+ enable_utc=True,
17
+ task_track_started=True,
18
+ task_acks_late=True,
19
+ worker_prefetch_multiplier=1,
20
+ )
21
+ except ImportError:
22
+ celery_app = None
bioai-platform/backend/app/workers/pipeline_worker.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from app.workers.celery_app import celery_app
3
+ from app.pipeline.engine import PipelineEngine
4
+ from app.services.supabase import get_supabase
5
+ from datetime import datetime, timezone
6
+
7
+
8
+ def run_pipeline_sync(job_id: str, sequence: str, database: str, max_hits: int) -> dict:
9
+ """Run pipeline synchronously (used when Celery is unavailable)."""
10
+ supabase = get_supabase()
11
+ try:
12
+ supabase.table("jobs").update({
13
+ "status": "running",
14
+ "progress_pct": 10,
15
+ }).eq("id", job_id).execute()
16
+
17
+ async def execute():
18
+ engine = PipelineEngine()
19
+ return await engine.execute(sequence, database, max_hits)
20
+
21
+ loop = asyncio.new_event_loop()
22
+ asyncio.set_event_loop(loop)
23
+ result = loop.run_until_complete(execute())
24
+ loop.close()
25
+
26
+ if "error" in result:
27
+ supabase.table("jobs").update({
28
+ "status": "failed",
29
+ "error": result["error"],
30
+ "completed_at": datetime.now(timezone.utc).isoformat(),
31
+ }).eq("id", job_id).execute()
32
+ return {"status": "failed", "error": result["error"]}
33
+
34
+ context = result.get("context", {})
35
+ supabase.table("jobs").update({
36
+ "status": "complete",
37
+ "progress_pct": 100,
38
+ "context_json": context,
39
+ "steps_completed": result.get("steps_completed", []),
40
+ "completed_at": datetime.now(timezone.utc).isoformat(),
41
+ }).eq("id", job_id).execute()
42
+
43
+ return {"status": "complete", "job_id": job_id}
44
+
45
+ except Exception as exc:
46
+ supabase.table("jobs").update({
47
+ "status": "failed",
48
+ "error": str(exc)[:500],
49
+ "completed_at": datetime.now(timezone.utc).isoformat(),
50
+ }).eq("id", job_id).execute()
51
+ return {"status": "failed", "error": str(exc)}
52
+
53
+
54
+ if celery_app is not None:
55
+ @celery_app.task(bind=True, max_retries=2, default_retry_delay=30)
56
+ def run_pipeline_job(self, job_id: str, sequence: str, database: str, max_hits: int):
57
+ return run_pipeline_sync(job_id, sequence, database, max_hits)
bioai-platform/backend/requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ slowapi
4
+ redis
5
+ httpx
6
+ biopython
7
+ litellm
8
+ python-dotenv
9
+ supabase
10
+ reportlab
11
+ celery
12
+ pydantic[email]
bioai-platform/frontend/.env.local.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ NEXT_PUBLIC_SUPABASE_URL=
2
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=
3
+ SUPABASE_SERVICE_ROLE_KEY=
bioai-platform/frontend/next-env.d.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
bioai-platform/frontend/next.config.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ async rewrites() {
4
+ return [
5
+ {
6
+ source: '/api/backend/:path*',
7
+ destination: 'http://localhost:8000/:path*',
8
+ },
9
+ ];
10
+ },
11
+ };
12
+
13
+ module.exports = nextConfig;
bioai-platform/frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
bioai-platform/frontend/package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "bionexus-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@supabase/ssr": "^0.12.0",
13
+ "@supabase/supabase-js": "^2.49.4",
14
+ "axios": "^1.7.9",
15
+ "lucide-react": "^0.474.0",
16
+ "next": "^14.2.23",
17
+ "react": "^18.3.1",
18
+ "react-dom": "^18.3.1",
19
+ "react-hot-toast": "^2.5.2"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.13.4",
23
+ "@types/react": "^18.3.18",
24
+ "@types/react-dom": "^18.3.5",
25
+ "autoprefixer": "^10.4.20",
26
+ "eslint": "^8.56.0",
27
+ "eslint-config-next": "^14.2.23",
28
+ "postcss": "^8.5.2",
29
+ "tailwindcss": "^3.4.17",
30
+ "typescript": "^5.7.3"
31
+ }
32
+ }