Spaces:
Running
Running
Commit ·
5a0e691
0
Parent(s):
Initial commit: Bio Nexus pipeline engine — BLAST + UniProt + AlphaFold + AI interpretation (Phase 1)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +32 -0
- MASTER_PLAN.md +205 -0
- bio-nexus/phase0/OUTREACH.md +110 -0
- bio-nexus/phase0/TRACKER.md +97 -0
- bioai-platform/backend/.env.example +4 -0
- bioai-platform/backend/__init__.py +0 -0
- bioai-platform/backend/app/__init__.py +0 -0
- bioai-platform/backend/app/ai/__init__.py +0 -0
- bioai-platform/backend/app/ai/interpreter.py +45 -0
- bioai-platform/backend/app/ai/llm_client.py +44 -0
- bioai-platform/backend/app/ai/prompts.py +46 -0
- bioai-platform/backend/app/config.py +22 -0
- bioai-platform/backend/app/main.py +54 -0
- bioai-platform/backend/app/pipeline/__init__.py +0 -0
- bioai-platform/backend/app/pipeline/assembler.py +68 -0
- bioai-platform/backend/app/pipeline/definitions/__init__.py +0 -0
- bioai-platform/backend/app/pipeline/definitions/protein_analysis.py +16 -0
- bioai-platform/backend/app/pipeline/engine.py +46 -0
- bioai-platform/backend/app/pipeline/registry.py +19 -0
- bioai-platform/backend/app/routers/__init__.py +0 -0
- bioai-platform/backend/app/routers/ai.py +32 -0
- bioai-platform/backend/app/routers/export.py +42 -0
- bioai-platform/backend/app/routers/jobs.py +38 -0
- bioai-platform/backend/app/routers/pipelines.py +65 -0
- bioai-platform/backend/app/routers/profile.py +28 -0
- bioai-platform/backend/app/routers/results.py +35 -0
- bioai-platform/backend/app/routers/share.py +13 -0
- bioai-platform/backend/app/routers/waitlist.py +28 -0
- bioai-platform/backend/app/services/__init__.py +0 -0
- bioai-platform/backend/app/services/cache.py +61 -0
- bioai-platform/backend/app/services/export.py +130 -0
- bioai-platform/backend/app/services/rate_limit.py +27 -0
- bioai-platform/backend/app/services/redis.py +15 -0
- bioai-platform/backend/app/services/supabase.py +11 -0
- bioai-platform/backend/app/services/validators.py +43 -0
- bioai-platform/backend/app/tools/__init__.py +0 -0
- bioai-platform/backend/app/tools/alphafold.py +44 -0
- bioai-platform/backend/app/tools/base.py +16 -0
- bioai-platform/backend/app/tools/blast.py +80 -0
- bioai-platform/backend/app/tools/registration.py +10 -0
- bioai-platform/backend/app/tools/uniprot.py +102 -0
- bioai-platform/backend/app/workers/__init__.py +0 -0
- bioai-platform/backend/app/workers/celery_app.py +22 -0
- bioai-platform/backend/app/workers/pipeline_worker.py +57 -0
- bioai-platform/backend/requirements.txt +12 -0
- bioai-platform/frontend/.env.local.example +3 -0
- bioai-platform/frontend/next-env.d.ts +5 -0
- bioai-platform/frontend/next.config.js +13 -0
- bioai-platform/frontend/package-lock.json +0 -0
- 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 < 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 > 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 |
+
}
|