faisalAI27 commited on
Commit
8fbda8a
·
1 Parent(s): d7b0e52

polish and verification

Browse files
README.md CHANGED
@@ -1,51 +1,72 @@
1
  # Variant Risk Explainer
2
 
3
- Variant Risk Explainer is a full-stack AI genomics research demo for exploring how a sequence model might explain variant risk signals from ClinVar-style data.
4
 
5
- This repository is for research and education only. It is not a medical device, not a diagnostic tool, and must not be used to make clinical decisions.
6
 
7
- ## Repository Structure
 
 
 
 
 
 
 
8
 
9
  ```text
10
- training/
11
- backend/
12
- frontend/
13
- docs/
 
 
 
 
 
 
 
14
  ```
15
 
16
- - `training/`: Google Colab-oriented ClinVar + DNABERT-2 fine-tuning pipeline.
17
- - `backend/`: FastAPI service exposing `POST /analyze`.
18
- - `frontend/`: Next.js interface with variant input, loading state, result card, and history.
19
- - `docs/`: Architecture, API contract, and model card.
 
 
 
 
 
 
 
 
 
20
 
21
- ## Prerequisites
 
 
 
 
 
 
 
22
 
23
- - Python 3.11+
24
- - Node.js 20+
25
- - Google Colab for model training
26
- - GRCh38 reference assets for training data preparation
27
 
28
- ## Backend Quick Start
29
 
30
  ```bash
31
  cd backend
32
  python -m venv .venv
33
  source .venv/bin/activate
 
34
  pip install -r requirements.txt
35
  cp .env.example .env
36
- uvicorn app.main:app --reload --port 8000
37
  ```
38
 
39
- The backend defaults to mock model mode when no trained model directory is configured.
40
 
41
- Run tests:
42
-
43
- ```bash
44
- cd backend
45
- pytest
46
- ```
47
-
48
- ## Frontend Quick Start
49
 
50
  ```bash
51
  cd frontend
@@ -56,29 +77,39 @@ npm run dev
56
 
57
  Open `http://localhost:3000`.
58
 
59
- ## Training Quick Start
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- Training is intended for Google Colab only. Do not train the model locally.
 
 
62
 
63
- 1. Open `training/colab_dnabert2_heavy_training.ipynb` in Google Colab for the current heavy-training workflow.
64
- 2. Follow the notebook cells to install dependencies, download ClinVar GRCh38 data, prepare examples, and fine-tune DNABERT-2.
65
- 3. Export the trained Hugging Face model directory.
66
- 4. Point the backend `MODEL_DIR` environment variable to that exported model directory.
67
 
68
  ## Data and Model Artifact Policy
69
 
70
- - Full datasets are not committed to GitHub.
71
- - Trained model weights are not committed to GitHub.
72
- - Use the training scripts or Colab notebook to regenerate datasets.
73
- - Store trained models in Google Drive or local storage.
74
- - For local evaluation, place the 20k alternate-sequence dataset in `training/csv_files_20k_alt/`.
75
- - For backend use, place the model folder in `backend/models/final_model/` or set the backend model path environment variable.
76
- - The local final model folder `training/training_model_files/` and the 20k CSV folder `training/csv_files_20k_alt/` are intentionally ignored by Git.
77
 
78
- ## Environment Files
 
 
 
79
 
80
- Each app folder includes a `.env.example`. Do not commit `.env`, `.env.local`, API keys, model checkpoints, or private data.
81
 
82
- ## Safety Notice
83
 
84
- Predictions and explanations are experimental model outputs. They can be wrong, incomplete, biased by the training data, or invalid for variants outside the training distribution. Use this project only for software, ML, and genomics workflow demonstrations.
 
1
  # Variant Risk Explainer
2
 
3
+ Variant Risk Explainer is a full-stack AI genomics research demo. It uses a fine-tuned DNABERT-2 model to estimate whether a submitted DNA sequence looks more similar to benign/likely benign or pathogenic/likely pathogenic ClinVar examples.
4
 
5
+ This project is for AI/ML research and education only. It is not a medical device, not a diagnostic system, and must not be used for clinical decisions.
6
 
7
+ ## Project Overview
8
+
9
+ - `training/`: ClinVar GRCh38 data preparation, DNABERT-2 training notebooks, local evaluation scripts.
10
+ - `backend/`: FastAPI inference API with DNABERT-2 prediction and explanation services.
11
+ - `frontend/`: Next.js demo UI with input form, backend status, result card, explanation, and history.
12
+ - `docs/`: Architecture notes, API contract, model card, demo examples, limitations, and testing checklist.
13
+
14
+ ## Architecture
15
 
16
  ```text
17
+ User
18
+
19
+ Next.js Frontend
20
+
21
+ FastAPI Backend
22
+
23
+ DNABERT-2 Prediction Service
24
+
25
+ Explanation Layer
26
+
27
+ Research/Demo Result
28
  ```
29
 
30
+ The frontend sends a DNA sequence to `POST /analyze`. The backend cleans and crops the sequence, runs the DNABERT-2 classifier, applies the tuned threshold, then returns probabilities, a research-only label, and a cautious explanation.
31
+
32
+ ## Model Training Summary
33
+
34
+ - Base model: DNABERT-2
35
+ - Dataset: 20k ClinVar alternate-sequence dataset
36
+ - Genome build: GRCh38
37
+ - Task: binary research classification
38
+ - Label `0`: Benign / Likely benign
39
+ - Label `1`: Pathogenic / Likely pathogenic
40
+ - Decision threshold: `0.16`
41
+
42
+ ## Final Metrics
43
 
44
+ | Metric | Value |
45
+ | --- | ---: |
46
+ | Accuracy | 0.5537 |
47
+ | Precision | 0.5384 |
48
+ | Recall | 0.7533 |
49
+ | F1 | 0.6280 |
50
+ | MCC | 0.1171 |
51
+ | AUC ROC | 0.5928 |
52
 
53
+ These metrics are limited and support demo use only, not clinical interpretation.
 
 
 
54
 
55
+ ## Run Backend
56
 
57
  ```bash
58
  cd backend
59
  python -m venv .venv
60
  source .venv/bin/activate
61
+ python -m pip install --upgrade pip
62
  pip install -r requirements.txt
63
  cp .env.example .env
64
+ uvicorn app.main:app --reload
65
  ```
66
 
67
+ Open `http://localhost:8000/docs`.
68
 
69
+ ## Run Frontend
 
 
 
 
 
 
 
70
 
71
  ```bash
72
  cd frontend
 
77
 
78
  Open `http://localhost:3000`.
79
 
80
+ ## Environment Variables
81
+
82
+ Backend values live in `backend/.env`:
83
+
84
+ ```bash
85
+ MODEL_DIR=../training/training_model_files
86
+ MODEL_THRESHOLD=0.16
87
+ MODEL_MAX_LENGTH=512
88
+ MODEL_NAME=DNABERT-2 ClinVar 20k
89
+ DEVICE=auto
90
+ OPENAI_API_KEY=your_openai_api_key_here
91
+ USE_AI_EXPLANATION=true
92
+ ```
93
+
94
+ Frontend values live in `frontend/.env.local`:
95
 
96
+ ```bash
97
+ NEXT_PUBLIC_API_URL=http://127.0.0.1:8000
98
+ ```
99
 
100
+ Never commit `.env`, `.env.local`, API keys, datasets, or model weights.
 
 
 
101
 
102
  ## Data and Model Artifact Policy
103
 
104
+ Large files are intentionally ignored by Git:
 
 
 
 
 
 
105
 
106
+ - trained model folders such as `training/training_model_files/`
107
+ - generated datasets such as `training/csv_files_20k_alt/`
108
+ - model weight files such as `.safetensors`, `.bin`, `.pt`, and `.ckpt`
109
+ - local environment files such as `.env` and `.env.local`
110
 
111
+ Use local storage, Google Drive, or another private artifact store for trained models and datasets.
112
 
113
+ ## Demo Disclaimer
114
 
115
+ Predictions and explanations are experimental model outputs. They can be wrong, incomplete, biased by ClinVar labels, or invalid outside the training distribution. This project is intended only for AI/ML research demonstrations and must not be used for diagnosis, treatment, or medical decision-making.
backend/.env.example CHANGED
@@ -3,14 +3,5 @@ MODEL_THRESHOLD=0.16
3
  MODEL_MAX_LENGTH=512
4
  MODEL_NAME=DNABERT-2 ClinVar 20k
5
  DEVICE=auto
6
-
7
- # Optional OpenAI explanation layer.
8
- # Keep USE_OPENAI_EXPLANATION=false for the local rule-based explanation.
9
- # To enable it, copy this file to backend/.env and paste your real key there.
10
- USE_OPENAI_EXPLANATION=false
11
- OPENAI_API_KEY=
12
- OPENAI_EXPLANATION_MODEL=gpt-4.1-mini
13
- OPENAI_EXPLANATION_TIMEOUT=12
14
-
15
- # Comma-separated origins for browser clients
16
- ALLOWED_ORIGINS=http://localhost:3000
 
3
  MODEL_MAX_LENGTH=512
4
  MODEL_NAME=DNABERT-2 ClinVar 20k
5
  DEVICE=auto
6
+ OPENAI_API_KEY=your_openai_api_key_here
7
+ USE_AI_EXPLANATION=true
 
 
 
 
 
 
 
 
 
backend/README.md CHANGED
@@ -58,10 +58,8 @@ Do not paste your OpenAI API key into source code or `.env.example`.
58
  Paste it only into your local `backend/.env` file:
59
 
60
  ```bash
61
- USE_OPENAI_EXPLANATION=true
62
- OPENAI_API_KEY=sk-your-real-key-here
63
- OPENAI_EXPLANATION_MODEL=gpt-4.1-mini
64
- OPENAI_EXPLANATION_TIMEOUT=12
65
  ```
66
 
67
  Then restart the backend. If the OpenAI key is missing, the package is not
@@ -131,6 +129,7 @@ print(response.json())
131
  - `threshold`: decision threshold, currently `0.16`
132
  - `sequence_length_used`: sequence length after optional center crop
133
  - `explanation`: plain-language explanation of the model output
 
134
  - `confidence_level`: rough confidence category based on model probability
135
  - `recommendation`: safe research/demo recommendation
136
  - `limitations`: important limitations to show users
@@ -138,7 +137,7 @@ print(response.json())
138
  ## Explanation Layer
139
 
140
  The default explanation is generated by local backend rules. When
141
- `USE_OPENAI_EXPLANATION=true`, the backend asks OpenAI to rewrite only the
142
  explanation paragraph in beginner-friendly language. The prediction,
143
  probabilities, threshold, confidence level, recommendation, and limitations stay
144
  controlled by backend logic.
 
58
  Paste it only into your local `backend/.env` file:
59
 
60
  ```bash
61
+ USE_AI_EXPLANATION=true
62
+ OPENAI_API_KEY=your_openai_api_key_here
 
 
63
  ```
64
 
65
  Then restart the backend. If the OpenAI key is missing, the package is not
 
129
  - `threshold`: decision threshold, currently `0.16`
130
  - `sequence_length_used`: sequence length after optional center crop
131
  - `explanation`: plain-language explanation of the model output
132
+ - `explanation_source`: `openai`, `rule-based`, or `rule-based-fallback`
133
  - `confidence_level`: rough confidence category based on model probability
134
  - `recommendation`: safe research/demo recommendation
135
  - `limitations`: important limitations to show users
 
137
  ## Explanation Layer
138
 
139
  The default explanation is generated by local backend rules. When
140
+ `USE_AI_EXPLANATION=true`, the backend asks OpenAI to rewrite only the
141
  explanation paragraph in beginner-friendly language. The prediction,
142
  probabilities, threshold, confidence level, recommendation, and limitations stay
143
  controlled by backend logic.
backend/app/core/config.py CHANGED
@@ -25,6 +25,12 @@ def _env_bool(name: str, default: bool = False) -> bool:
25
  return value.strip().lower() in {"1", "true", "yes", "on"}
26
 
27
 
 
 
 
 
 
 
28
  @dataclass(frozen=True)
29
  class Settings:
30
  app_name: str = "variant-risk-explainer"
@@ -35,7 +41,7 @@ class Settings:
35
  model_name: str = os.getenv("MODEL_NAME", "DNABERT-2 ClinVar 20k").strip() or "DNABERT-2 ClinVar 20k"
36
  device: str = os.getenv("DEVICE", "auto").strip().lower() or "auto"
37
  max_sequence_context_length: int = int(os.getenv("MAX_SEQUENCE_CONTEXT_LENGTH", "2000"))
38
- use_openai_explanation: bool = _env_bool("USE_OPENAI_EXPLANATION", False)
39
  openai_api_key: str = os.getenv("OPENAI_API_KEY", "").strip()
40
  openai_explanation_model: str = (
41
  os.getenv("OPENAI_EXPLANATION_MODEL", "gpt-4.1-mini").strip() or "gpt-4.1-mini"
 
25
  return value.strip().lower() in {"1", "true", "yes", "on"}
26
 
27
 
28
+ def _ai_explanation_enabled() -> bool:
29
+ if os.getenv("USE_AI_EXPLANATION") is not None:
30
+ return _env_bool("USE_AI_EXPLANATION", False)
31
+ return _env_bool("USE_OPENAI_EXPLANATION", False)
32
+
33
+
34
  @dataclass(frozen=True)
35
  class Settings:
36
  app_name: str = "variant-risk-explainer"
 
41
  model_name: str = os.getenv("MODEL_NAME", "DNABERT-2 ClinVar 20k").strip() or "DNABERT-2 ClinVar 20k"
42
  device: str = os.getenv("DEVICE", "auto").strip().lower() or "auto"
43
  max_sequence_context_length: int = int(os.getenv("MAX_SEQUENCE_CONTEXT_LENGTH", "2000"))
44
+ use_openai_explanation: bool = _ai_explanation_enabled()
45
  openai_api_key: str = os.getenv("OPENAI_API_KEY", "").strip()
46
  openai_explanation_model: str = (
47
  os.getenv("OPENAI_EXPLANATION_MODEL", "gpt-4.1-mini").strip() or "gpt-4.1-mini"
backend/app/main.py CHANGED
@@ -88,6 +88,7 @@ def analyze(request: AnalyzeRequest) -> AnalyzeResponse:
88
  model_name=prediction.model_name,
89
  sequence_length_used=prediction.sequence_length_used,
90
  explanation=explanation["explanation"],
 
91
  confidence_level=explanation["confidence_level"],
92
  recommendation=explanation["recommendation"],
93
  limitations=explanation["limitations"],
 
88
  model_name=prediction.model_name,
89
  sequence_length_used=prediction.sequence_length_used,
90
  explanation=explanation["explanation"],
91
+ explanation_source=explanation["explanation_source"],
92
  confidence_level=explanation["confidence_level"],
93
  recommendation=explanation["recommendation"],
94
  limitations=explanation["limitations"],
backend/app/schemas.py CHANGED
@@ -43,6 +43,7 @@ class AnalyzeResponse(BaseModel):
43
  model_name: str
44
  sequence_length_used: int
45
  explanation: str
 
46
  confidence_level: str
47
  recommendation: str
48
  limitations: list[str]
 
43
  model_name: str
44
  sequence_length_used: int
45
  explanation: str
46
+ explanation_source: str
47
  confidence_level: str
48
  recommendation: str
49
  limitations: list[str]
backend/app/services/explanation_service.py CHANGED
@@ -101,12 +101,19 @@ def _generate_rule_based_explanation(
101
 
102
  return {
103
  "explanation": explanation,
 
104
  "confidence_level": confidence_level,
105
  "recommendation": RECOMMENDATION,
106
  "limitations": LIMITATIONS,
107
  }
108
 
109
 
 
 
 
 
 
 
110
  def _extract_json_object(text: str) -> dict:
111
  try:
112
  return json.loads(text)
@@ -136,7 +143,7 @@ def _generate_openai_explanation(
136
  from openai import OpenAI
137
  except ImportError:
138
  print("OpenAI package is not installed. Using rule-based explanation.")
139
- return fallback
140
 
141
  context = {
142
  "variant_name": variant_name,
@@ -169,20 +176,21 @@ def _generate_openai_explanation(
169
  output_text = str(getattr(response, "output_text", "")).strip()
170
  if not output_text:
171
  print("OpenAI explanation response was empty. Using rule-based explanation.")
172
- return fallback
173
 
174
  parsed = _extract_json_object(output_text)
175
  explanation = str(parsed.get("explanation", "")).strip()
176
  if not explanation:
177
  print("OpenAI explanation JSON did not include explanation. Using rule-based explanation.")
178
- return fallback
179
 
180
  enhanced = dict(fallback)
181
  enhanced["explanation"] = explanation
 
182
  return enhanced
183
  except Exception as exc: # pragma: no cover - network/API failures vary.
184
  print(f"OpenAI explanation failed: {type(exc).__name__}: {exc}. Using rule-based explanation.")
185
- return fallback
186
 
187
 
188
  def generate_explanation(
@@ -215,8 +223,8 @@ def generate_explanation(
215
  if not use_openai:
216
  return fallback
217
  if not openai_api_key:
218
- print("USE_OPENAI_EXPLANATION is true, but OPENAI_API_KEY is missing. Using rule-based explanation.")
219
- return fallback
220
 
221
  return _generate_openai_explanation(
222
  fallback=fallback,
 
101
 
102
  return {
103
  "explanation": explanation,
104
+ "explanation_source": "rule-based",
105
  "confidence_level": confidence_level,
106
  "recommendation": RECOMMENDATION,
107
  "limitations": LIMITATIONS,
108
  }
109
 
110
 
111
+ def _fallback_with_source(fallback: dict) -> dict:
112
+ result = dict(fallback)
113
+ result["explanation_source"] = "rule-based-fallback"
114
+ return result
115
+
116
+
117
  def _extract_json_object(text: str) -> dict:
118
  try:
119
  return json.loads(text)
 
143
  from openai import OpenAI
144
  except ImportError:
145
  print("OpenAI package is not installed. Using rule-based explanation.")
146
+ return _fallback_with_source(fallback)
147
 
148
  context = {
149
  "variant_name": variant_name,
 
176
  output_text = str(getattr(response, "output_text", "")).strip()
177
  if not output_text:
178
  print("OpenAI explanation response was empty. Using rule-based explanation.")
179
+ return _fallback_with_source(fallback)
180
 
181
  parsed = _extract_json_object(output_text)
182
  explanation = str(parsed.get("explanation", "")).strip()
183
  if not explanation:
184
  print("OpenAI explanation JSON did not include explanation. Using rule-based explanation.")
185
+ return _fallback_with_source(fallback)
186
 
187
  enhanced = dict(fallback)
188
  enhanced["explanation"] = explanation
189
+ enhanced["explanation_source"] = "openai"
190
  return enhanced
191
  except Exception as exc: # pragma: no cover - network/API failures vary.
192
  print(f"OpenAI explanation failed: {type(exc).__name__}: {exc}. Using rule-based explanation.")
193
+ return _fallback_with_source(fallback)
194
 
195
 
196
  def generate_explanation(
 
223
  if not use_openai:
224
  return fallback
225
  if not openai_api_key:
226
+ print("AI explanation is enabled, but OPENAI_API_KEY is missing. Using rule-based explanation.")
227
+ return _fallback_with_source(fallback)
228
 
229
  return _generate_openai_explanation(
230
  fallback=fallback,
docs/DEMO_EXAMPLES.md ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Demo Examples
2
+
3
+ These examples use synthetic/demo DNA sequences only. They are not real clinical examples and should not be interpreted as medical evidence.
4
+
5
+ ## Example 1: Demo Variant A
6
+
7
+ Request:
8
+
9
+ ```json
10
+ {
11
+ "variant_name": "Demo Variant A",
12
+ "gene": "BRCA1",
13
+ "sequence": "ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGT",
14
+ "notes": "Synthetic short sequence for endpoint testing."
15
+ }
16
+ ```
17
+
18
+ Example curl:
19
+
20
+ ```bash
21
+ curl -X POST http://127.0.0.1:8000/analyze \
22
+ -H "Content-Type: application/json" \
23
+ -d '{
24
+ "variant_name": "Demo Variant A",
25
+ "gene": "BRCA1",
26
+ "sequence": "ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGT",
27
+ "notes": "Synthetic short sequence for endpoint testing."
28
+ }'
29
+ ```
30
+
31
+ ## Example 2: Demo Variant B
32
+
33
+ Request:
34
+
35
+ ```json
36
+ {
37
+ "variant_name": "Demo Variant B",
38
+ "gene": "TP53",
39
+ "sequence": "TTGCAAGCTTAGGCTAACCGTTGCAAGCTTAGGCTAACCGTTGCAAGCTTAGGCTAACCGTTGCAAGC",
40
+ "notes": "Second synthetic sequence for frontend demo testing."
41
+ }
42
+ ```
43
+
44
+ Example curl:
45
+
46
+ ```bash
47
+ curl -X POST http://127.0.0.1:8000/analyze \
48
+ -H "Content-Type: application/json" \
49
+ -d '{
50
+ "variant_name": "Demo Variant B",
51
+ "gene": "TP53",
52
+ "sequence": "TTGCAAGCTTAGGCTAACCGTTGCAAGCTTAGGCTAACCGTTGCAAGCTTAGGCTAACCGTTGCAAGC",
53
+ "notes": "Second synthetic sequence for frontend demo testing."
54
+ }'
55
+ ```
56
+
57
+ ## Example 3: Invalid Sequence
58
+
59
+ Request:
60
+
61
+ ```json
62
+ {
63
+ "variant_name": "Invalid Demo Variant",
64
+ "gene": "TP53",
65
+ "sequence": "ACGTXYZACGT",
66
+ "notes": "This should fail validation because X, Y, and Z are not valid DNA bases."
67
+ }
68
+ ```
69
+
70
+ Expected behavior:
71
+
72
+ - HTTP status: `400`
73
+ - Error message includes invalid DNA characters.
74
+
75
+ Example response:
76
+
77
+ ```json
78
+ {
79
+ "detail": "sequence contains invalid DNA characters: XYZ"
80
+ }
81
+ ```
docs/LIMITATIONS.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Project Limitations
2
+
3
+ Variant Risk Explainer is an AI/ML research demonstration only.
4
+
5
+ Key limitations:
6
+
7
+ - The model is trained on DNA sequence patterns only.
8
+ - The prediction does not include full clinical evidence.
9
+ - The prediction does not include family history.
10
+ - The prediction does not include population frequency.
11
+ - The prediction does not include functional studies.
12
+ - ClinVar labels can be incomplete, conflicting, biased, or updated over time.
13
+ - Model performance is limited, with AUC ROC around `0.5928`.
14
+ - The model may not generalize to variants outside the training distribution.
15
+ - The system is not for diagnosis or treatment decisions.
16
+ - The project is intended only for AI/ML research demonstration and education.
17
+
18
+ Any real genetic or medical decision requires validated clinical testing, clinical databases, and review by qualified clinical genetics professionals.
docs/README.md CHANGED
@@ -5,3 +5,7 @@ This folder contains project documentation for the Variant Risk Explainer demo.
5
  - `architecture.md`: high-level system design and data flow.
6
  - `api_contract.md`: backend request and response schema.
7
  - `model_card.md`: research model card and safety limitations.
 
 
 
 
 
5
  - `architecture.md`: high-level system design and data flow.
6
  - `api_contract.md`: backend request and response schema.
7
  - `model_card.md`: research model card and safety limitations.
8
+ - `DEMO_EXAMPLES.md`: synthetic demo requests for frontend/backend testing.
9
+ - `LIMITATIONS.md`: project limitations and safety boundaries.
10
+ - `TESTING_CHECKLIST.md`: final pre-push and demo testing checklist.
11
+ - `MODEL_ARTIFACTS.md`: local model artifact handling notes.
docs/TESTING_CHECKLIST.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Testing Checklist
2
+
3
+ Use this checklist before pushing or presenting the demo.
4
+
5
+ ## Backend
6
+
7
+ - [ ] `GET /health` returns `status: ok`.
8
+ - [ ] `GET /health` shows the expected model directory and threshold `0.16`.
9
+ - [ ] `POST /analyze` works for a valid synthetic DNA sequence.
10
+ - [ ] `POST /analyze` returns prediction probabilities and `prediction_class`.
11
+ - [ ] Invalid DNA sequence returns a clear HTTP `400` error.
12
+ - [ ] Response includes `explanation`.
13
+ - [ ] Response includes `explanation_source`.
14
+ - [ ] Response includes `confidence_level`, `recommendation`, `limitations`, and `disclaimer`.
15
+ - [ ] OpenAI explanation works when `USE_AI_EXPLANATION=true` and `OPENAI_API_KEY` is set.
16
+ - [ ] OpenAI fallback works if the key is missing or the OpenAI request fails.
17
+
18
+ ## Frontend
19
+
20
+ - [ ] Frontend loads at `http://localhost:3000`.
21
+ - [ ] Backend status indicator shows connected/model loaded.
22
+ - [ ] Form accepts variant name, gene, sequence, and notes.
23
+ - [ ] Result card displays prediction label and risk level.
24
+ - [ ] Result card displays pathogenic and benign probabilities.
25
+ - [ ] Result card displays explanation and explanation source.
26
+ - [ ] Result card displays limitations and research/demo disclaimer.
27
+ - [ ] History panel records recent demo analyses.
28
+
29
+ ## Security And Artifacts
30
+
31
+ - [ ] No `.env` files are tracked by Git.
32
+ - [ ] No `.env.local` files are tracked by Git.
33
+ - [ ] No OpenAI API key appears in tracked source, README, or docs files.
34
+ - [ ] Trained model folders are ignored by Git.
35
+ - [ ] Dataset folders are ignored by Git.
36
+ - [ ] Model weight files such as `.safetensors`, `.bin`, `.pt`, and `.ckpt` are ignored by Git.
docs/api_contract.md CHANGED
@@ -12,17 +12,21 @@ http://localhost:8000
12
  GET /health
13
  ```
14
 
15
- ### Response
16
 
17
  ```json
18
  {
19
  "status": "ok",
20
- "service": "variant-risk-explainer",
21
- "model_mode": "mock"
 
 
 
 
22
  }
23
  ```
24
 
25
- ## Analyze Variant
26
 
27
  ```http
28
  POST /analyze
@@ -33,70 +37,64 @@ Content-Type: application/json
33
 
34
  ```json
35
  {
36
- "chromosome": "7",
37
- "position": 140753336,
38
- "reference": "A",
39
- "alternate": "T",
40
- "gene": "BRAF",
41
- "sequence_context": "ACGTACGTACGT"
42
  }
43
  ```
44
 
45
- ### Fields
46
 
47
- - `chromosome`: GRCh38 chromosome name. Accepts `1` through `22`, `X`, `Y`, `MT`, and optional `chr` prefix.
48
- - `position`: 1-based GRCh38 genomic coordinate.
49
- - `reference`: reference allele using `A`, `C`, `G`, `T`, or `N`.
50
- - `alternate`: alternate allele using `A`, `C`, `G`, `T`, or `N`.
51
- - `gene`: optional gene symbol for display and explanation context.
52
- - `sequence_context`: optional GRCh38 sequence context around the variant.
53
 
54
  ### Response Body
55
 
56
  ```json
57
  {
58
- "request_id": "0e7c3d55-6f46-4c19-a1fd-860bc4f8a88d",
59
- "submitted_at": "2026-06-04T10:00:00Z",
60
- "input": {
61
- "chromosome": "7",
62
- "position": 140753336,
63
- "reference": "A",
64
- "alternate": "T",
65
- "gene": "BRAF",
66
- "sequence_context": "ACGTACGTACGT"
67
- },
68
- "grch_build": "GRCh38",
69
- "risk_label": "uncertain",
70
- "confidence": 0.54,
71
- "model_mode": "mock",
72
- "explanation": "Mock mode produced a deterministic research-only score from variant features. No clinical meaning should be inferred.",
73
  "limitations": [
74
- "Research demo only.",
75
- "Not validated for diagnosis or treatment decisions.",
76
- "ClinVar labels may be incomplete, conflicting, or biased."
 
77
  ],
78
- "disclaimer": "For research and education only. Not for medical diagnosis."
79
  }
80
  ```
81
 
82
- ### Risk Labels
83
 
84
- - `likely_benign`
85
- - `uncertain`
86
- - `likely_pathogenic`
87
-
88
- These labels are demo categories only and are not clinical classifications.
89
 
90
  ### Error Example
91
 
 
 
92
  ```json
93
  {
94
- "detail": [
95
- {
96
- "type": "value_error",
97
- "loc": ["body", "position"],
98
- "msg": "Value error, position must be a positive GRCh38 coordinate"
99
- }
100
- ]
101
  }
102
  ```
 
 
 
 
 
12
  GET /health
13
  ```
14
 
15
+ Example response:
16
 
17
  ```json
18
  {
19
  "status": "ok",
20
+ "model_loaded": true,
21
+ "device": "mps",
22
+ "model_dir": "../training/training_model_files",
23
+ "threshold": 0.16,
24
+ "model_name": "DNABERT-2 ClinVar 20k",
25
+ "load_error": null
26
  }
27
  ```
28
 
29
+ ## Analyze Variant Sequence
30
 
31
  ```http
32
  POST /analyze
 
37
 
38
  ```json
39
  {
40
+ "sequence": "ACGTACGTACGTACGT",
41
+ "variant_name": "Demo Variant A",
42
+ "gene": "BRCA1",
43
+ "notes": "Synthetic demo request"
 
 
44
  }
45
  ```
46
 
47
+ ### Request Fields
48
 
49
+ - `sequence`: required DNA sequence using only `A`, `C`, `G`, `T`, or `N`.
50
+ - `variant_name`: optional display name.
51
+ - `gene`: optional gene symbol for display/explanation context.
52
+ - `notes`: optional demo notes.
 
 
53
 
54
  ### Response Body
55
 
56
  ```json
57
  {
58
+ "variant_name": "Demo Variant A",
59
+ "gene": "BRCA1",
60
+ "prediction_class": 1,
61
+ "prediction_label": "Pathogenic / Likely pathogenic",
62
+ "risk_level": "Elevated",
63
+ "benign_probability": 0.671463,
64
+ "pathogenic_probability": 0.328537,
65
+ "threshold": 0.16,
66
+ "model_name": "DNABERT-2 ClinVar 20k",
67
+ "sequence_length_used": 64,
68
+ "explanation": "The model estimated...",
69
+ "explanation_source": "openai",
70
+ "confidence_level": "Low model confidence",
71
+ "recommendation": "This result is for research/demo use only...",
 
72
  "limitations": [
73
+ "The model uses DNA sequence patterns and does not replace clinical interpretation.",
74
+ "The model performance is limited, with test AUC around 0.5928.",
75
+ "The prediction does not include full clinical evidence, family history, population frequency, or functional studies.",
76
+ "The result should not be used for diagnosis or treatment decisions."
77
  ],
78
+ "disclaimer": "Research/demo use only. This model is not a clinical diagnostic system and must not be used for medical decisions."
79
  }
80
  ```
81
 
82
+ ### Explanation Sources
83
 
84
+ - `openai`: OpenAI successfully rewrote the explanation paragraph.
85
+ - `rule-based`: local rule-based explanation was used because AI explanation is disabled.
86
+ - `rule-based-fallback`: AI explanation was enabled, but the key was missing or the OpenAI request failed.
 
 
87
 
88
  ### Error Example
89
 
90
+ Invalid DNA characters return a clear error:
91
+
92
  ```json
93
  {
94
+ "detail": "sequence contains invalid DNA characters: XZ"
 
 
 
 
 
 
95
  }
96
  ```
97
+
98
+ ## Safety Boundary
99
+
100
+ All outputs are research/demo outputs only. They are not clinical classifications and must not be used for diagnosis, treatment, or medical decision-making.
docs/architecture.md CHANGED
@@ -3,61 +3,78 @@
3
  Variant Risk Explainer is split into four top-level folders:
4
 
5
  ```text
6
- training/ -> Colab training pipeline
7
  backend/ -> FastAPI inference API
8
  frontend/ -> Next.js research demo UI
9
  docs/ -> project documentation
10
  ```
11
 
12
- ## Component Flow
13
 
14
  ```text
15
  User
16
  |
17
  v
18
- Next.js frontend
19
  |
20
  | POST /analyze
21
  v
22
- FastAPI backend
23
  |
24
- | mock mode or trained DNABERT-2 model directory
25
  v
26
- Variant risk response
 
 
 
 
 
 
27
  ```
28
 
29
- ## Training Flow
30
-
31
- 1. Download ClinVar GRCh38 VCF data in Google Colab.
32
- 2. Load a GRCh38 reference FASTA.
33
- 3. Extract sequence windows around single nucleotide variants.
34
- 4. Map ClinVar clinical significance labels into research classes.
35
- 5. Fine-tune DNABERT-2 with Hugging Face Transformers.
36
- 6. Save an exported model directory for backend inference.
37
 
38
- Training is intentionally not wired into the local runtime. The backend can run in mock mode until a trained model is available.
 
 
 
 
 
 
 
 
39
 
40
- ## Backend Flow
41
 
42
- The backend exposes `POST /analyze`. It validates the submitted GRCh38 variant, chooses a model implementation, returns a risk label, confidence, explanation, and research-only disclaimers.
43
 
44
- Model mode is controlled by environment variables:
 
 
45
 
46
- - `MODEL_MODE=mock`: always use deterministic mock inference.
47
- - `MODEL_MODE=trained`: require a trained model at `MODEL_DIR`.
48
- - `MODEL_MODE=auto`: use the trained model when available, otherwise fall back to mock mode.
49
 
50
  ## Frontend Flow
51
 
52
  The frontend provides:
53
 
54
- - Variant input form.
55
- - Loading and error states.
56
- - Result card.
57
- - Local in-browser history panel.
 
 
 
 
 
58
 
59
- The frontend uses `NEXT_PUBLIC_API_BASE_URL` to find the backend.
 
 
 
 
 
 
60
 
61
  ## Safety Boundary
62
 
63
- This is a research and educational demo. It does not diagnose disease, recommend care, or replace genetic counseling or clinical interpretation. All user-facing layers should preserve that boundary.
 
3
  Variant Risk Explainer is split into four top-level folders:
4
 
5
  ```text
6
+ training/ -> ClinVar GRCh38 preparation, DNABERT-2 training, evaluation scripts
7
  backend/ -> FastAPI inference API
8
  frontend/ -> Next.js research demo UI
9
  docs/ -> project documentation
10
  ```
11
 
12
+ ## Runtime Flow
13
 
14
  ```text
15
  User
16
  |
17
  v
18
+ Next.js Frontend
19
  |
20
  | POST /analyze
21
  v
22
+ FastAPI Backend
23
  |
 
24
  v
25
+ DNABERT-2 Prediction Service
26
+ |
27
+ v
28
+ Explanation Layer
29
+ |
30
+ v
31
+ Research/Demo Result
32
  ```
33
 
34
+ ## Backend Flow
 
 
 
 
 
 
 
35
 
36
+ 1. Load settings from `backend/.env`.
37
+ 2. Load the DNABERT-2 tokenizer and sequence-classification model from `MODEL_DIR`.
38
+ 3. Select device automatically: CUDA, then MPS, then CPU.
39
+ 4. Clean the submitted DNA sequence.
40
+ 5. Center crop sequences longer than `MODEL_MAX_LENGTH`.
41
+ 6. Run DNABERT-2 in inference mode.
42
+ 7. Apply the tuned pathogenic threshold, currently `0.16`.
43
+ 8. Generate a cautious explanation.
44
+ 9. Return prediction probabilities, label, explanation, limitations, and disclaimer.
45
 
46
+ ## Explanation Layer
47
 
48
+ The explanation layer is designed to be safe for a research demo:
49
 
50
+ - `rule-based`: local deterministic explanation.
51
+ - `openai`: optional OpenAI-generated explanation paragraph.
52
+ - `rule-based-fallback`: local explanation used because AI explanation failed or was missing configuration.
53
 
54
+ The LLM, when enabled, rewrites only the explanation paragraph. It does not control the prediction, probabilities, threshold, confidence level, recommendation, limitations, or disclaimer.
 
 
55
 
56
  ## Frontend Flow
57
 
58
  The frontend provides:
59
 
60
+ - backend health indicator
61
+ - DNA sequence input form
62
+ - loading and error states
63
+ - result card
64
+ - explanation source display
65
+ - local in-browser history panel
66
+ - research/demo disclaimer
67
+
68
+ ## Training Flow
69
 
70
+ 1. Prepare ClinVar GRCh38 records.
71
+ 2. Extract reference sequence windows.
72
+ 3. Build alternate-allele sequence windows.
73
+ 4. Filter uncertain/conflicting labels.
74
+ 5. Fine-tune DNABERT-2 on the alternate-sequence dataset.
75
+ 6. Evaluate on held-out validation and test splits.
76
+ 7. Export a self-contained Hugging Face model folder for backend inference.
77
 
78
  ## Safety Boundary
79
 
80
+ This is a research and educational demo. It does not diagnose disease, recommend care, or replace genetic counseling or clinical variant interpretation. All user-facing layers should preserve that boundary.
docs/model_card.md CHANGED
@@ -28,10 +28,10 @@ GRCh38 is used consistently for ClinVar coordinates and reference sequence extra
28
 
29
  The pipeline prepares examples from ClinVar GRCh38 VCF records. It focuses on single nucleotide variants with clinical significance labels that can be mapped into binary or compact research classes.
30
 
31
- Example label mapping:
32
 
33
- - `Pathogenic` and `Likely_pathogenic` -> `likely_pathogenic`
34
- - `Benign` and `Likely_benign` -> `likely_benign`
35
  - conflicting, uncertain, or unsupported labels -> skipped or held out depending on notebook settings
36
 
37
  ## Inputs
@@ -40,22 +40,31 @@ The model receives sequence windows centered on a submitted variant. The demo sc
40
 
41
  ## Outputs
42
 
43
- The backend maps model scores into demo labels:
44
 
45
- - `likely_benign`
46
- - `uncertain`
47
- - `likely_pathogenic`
 
48
 
49
  ## Evaluation
50
 
51
- Recommended evaluation:
 
 
 
 
 
 
 
 
 
52
 
53
- - Stratified train, validation, and test splits.
54
- - Accuracy, precision, recall, F1, ROC-AUC where appropriate.
55
- - Confusion matrix.
56
- - Per-class metrics.
57
- - Label distribution audit.
58
- - Gene and chromosome holdout experiments for stronger leakage checks.
59
 
60
  ## Limitations
61
 
 
28
 
29
  The pipeline prepares examples from ClinVar GRCh38 VCF records. It focuses on single nucleotide variants with clinical significance labels that can be mapped into binary or compact research classes.
30
 
31
+ Final label mapping:
32
 
33
+ - `Pathogenic` and `Likely_pathogenic` -> `1`
34
+ - `Benign` and `Likely_benign` -> `0`
35
  - conflicting, uncertain, or unsupported labels -> skipped or held out depending on notebook settings
36
 
37
  ## Inputs
 
40
 
41
  ## Outputs
42
 
43
+ The backend maps model probabilities into demo labels:
44
 
45
+ - `0`: Benign / Likely benign
46
+ - `1`: Pathogenic / Likely pathogenic
47
+
48
+ The current research threshold for class `1` is `0.16`.
49
 
50
  ## Evaluation
51
 
52
+ Final confirmed 20k alternate-sequence evaluation:
53
+
54
+ - Accuracy: `0.5537`
55
+ - Precision: `0.5384`
56
+ - Recall: `0.7533`
57
+ - F1: `0.6280`
58
+ - MCC: `0.1171`
59
+ - AUC ROC: `0.5928`
60
+
61
+ Recommended future evaluation:
62
 
63
+ - Larger independent test sets.
64
+ - Gene and chromosome holdout experiments.
65
+ - Leakage checks across related variants.
66
+ - Per-variant-type reporting for SNVs and indels.
67
+ - Calibration analysis across thresholds.
 
68
 
69
  ## Limitations
70
 
frontend/README.md CHANGED
@@ -68,11 +68,12 @@ NEXT_PUBLIC_API_URL=http://127.0.0.1:8001
68
  5. Click `Analyze Variant`.
69
 
70
  The result card shows benign/pathogenic probabilities, threshold, model name,
71
- sequence length used after center cropping, and a beginner-friendly explanation.
 
72
 
73
  The explanation is generated from the backend model output and threshold. It is
74
  not medical advice and is not a clinical interpretation. If the backend has
75
- `USE_OPENAI_EXPLANATION=true`, the backend may use OpenAI to improve the
76
  explanation paragraph. Do not put the OpenAI API key in the frontend; keep it in
77
  `backend/.env` only.
78
 
 
68
  5. Click `Analyze Variant`.
69
 
70
  The result card shows benign/pathogenic probabilities, threshold, model name,
71
+ sequence length used after center cropping, a beginner-friendly explanation, and
72
+ the explanation source.
73
 
74
  The explanation is generated from the backend model output and threshold. It is
75
  not medical advice and is not a clinical interpretation. If the backend has
76
+ `USE_AI_EXPLANATION=true`, the backend may use OpenAI to improve the
77
  explanation paragraph. Do not put the OpenAI API key in the frontend; keep it in
78
  `backend/.env` only.
79
 
frontend/components/ResultCard.tsx CHANGED
@@ -13,6 +13,16 @@ function asPercent(value: number): string {
13
  return `${(value * 100).toFixed(1)}%`;
14
  }
15
 
 
 
 
 
 
 
 
 
 
 
16
  export function ResultCard({ error, isLoading, result }: ResultCardProps) {
17
  if (isLoading) {
18
  return (
@@ -101,6 +111,9 @@ export function ResultCard({ error, isLoading, result }: ResultCardProps) {
101
  <p>
102
  <strong>Confidence level:</strong> {result.confidence_level}
103
  </p>
 
 
 
104
  </div>
105
 
106
  <div className="recommendationBlock">
 
13
  return `${(value * 100).toFixed(1)}%`;
14
  }
15
 
16
+ function explanationSourceLabel(source: string): string {
17
+ if (source === "openai") {
18
+ return "OpenAI";
19
+ }
20
+ if (source === "rule-based-fallback") {
21
+ return "Rule-based fallback";
22
+ }
23
+ return "Rule-based";
24
+ }
25
+
26
  export function ResultCard({ error, isLoading, result }: ResultCardProps) {
27
  if (isLoading) {
28
  return (
 
111
  <p>
112
  <strong>Confidence level:</strong> {result.confidence_level}
113
  </p>
114
+ <p>
115
+ <strong>Explanation source:</strong> {explanationSourceLabel(result.explanation_source)}
116
+ </p>
117
  </div>
118
 
119
  <div className="recommendationBlock">
frontend/types.ts CHANGED
@@ -17,6 +17,7 @@ export type AnalyzeResponse = {
17
  model_name: string;
18
  sequence_length_used: number;
19
  explanation: string;
 
20
  confidence_level: string;
21
  recommendation: string;
22
  limitations: string[];
 
17
  model_name: string;
18
  sequence_length_used: number;
19
  explanation: string;
20
+ explanation_source: "rule-based" | "openai" | "rule-based-fallback" | string;
21
  confidence_level: string;
22
  recommendation: string;
23
  limitations: string[];