Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion apps/api/src/genomeai_api/services/gene_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,13 @@ def _parse_ai_response(

data: dict[str, Any] = {}
try:
parsed = json.loads(response_text)
cleaned = response_text.strip()
if cleaned.startswith("```"):
lines = cleaned.split("\n")
cleaned = "\n".join(
ln for ln in lines if not ln.strip().startswith("```")
)
parsed = json.loads(cleaned)
if isinstance(parsed, dict):
data = parsed
except json.JSONDecodeError:
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/genomeai_api/services/variant_interpretation.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,13 @@ def _build_prompt(

def _parse_ai_response(self, response_text: str) -> dict[str, str | list[str]]:
try:
data = json.loads(response_text)
cleaned = response_text.strip()
if cleaned.startswith("```"):
lines = cleaned.split("\n")
cleaned = "\n".join(
ln for ln in lines if not ln.strip().startswith("```")
)
data = json.loads(cleaned)
if isinstance(data, dict):
raw_criteria = data.get("acmg_criteria", [])
acmg: list[str] = (
Expand Down
53 changes: 53 additions & 0 deletions apps/api/tests/test_gene_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,32 @@ async def list_models(self) -> list[str]:
return []


class FencedJsonAI(AIProvider):
"""AI provider returning markdown-fenced JSON, as gemini-3.x models do."""

name = "fenced"

async def generate(self, request: AIRequest) -> AIResponse:
return AIResponse(
text='```json\n{\n "function": "Tumor suppressor",\n '
' "key_variants": ["BRCA1 185delAG"],\n '
' "associated_diseases": ["Breast cancer"],\n '
' "drug_targets": ["Olaparib"],\n '
' "clinical_significance": "High",\n '
' "summary": "BRCA1 repairs DNA."\n}\n```',
model="gemini-fenced",
provider="gemini",
tokens_used=0,
finish_reason="stop",
)

async def health_check(self) -> bool:
return False

async def list_models(self) -> list[str]:
return []


@pytest.mark.asyncio
async def test_basic_analysis_without_ai() -> None:
"""Test basic analysis without AI (fallback mode), no Ollama required."""
Expand All @@ -163,3 +189,30 @@ async def test_basic_analysis_without_ai() -> None:
assert analysis.gene_symbol == "BRCA1"
assert analysis.gene_id == "672"
assert analysis.source == "ncbi"


@pytest.mark.asyncio
async def test_ai_parses_markdown_fenced_json() -> None:
"""AI output wrapped in ```json fences must still parse into fields."""
from genomeai_api.integration.connectors.ncbi.models import NCBIGeneRecord

record = NCBIGeneRecord(
gene_id="672",
symbol="BRCA1",
name="BRCA1 DNA repair associated",
organism="Homo sapiens",
chromosome="17",
map_location="17q21.31",
)
engine = GeneAnalysisEngine(
ai_provider=FencedJsonAI(),
ncbi_client=NCBIClient(),
)
analysis = await engine.analyze_from_record(record)
assert analysis.source == "ncbi+ollama"
assert analysis.function == "Tumor suppressor"
assert analysis.key_variants == ["BRCA1 185delAG"]
assert analysis.associated_diseases == ["Breast cancer"]
assert analysis.drug_targets == ["Olaparib"]
assert analysis.clinical_significance == "High"
assert analysis.summary == "BRCA1 repairs DNA."
Loading