PR 5: GEO extraction agent (vertical slice) - #9
Conversation
First source with unstructured metadata, so the first LLM-using path. - sources/geo.py: deterministic GeoAdapter. Fetches GEO Series+Sample SOFT text from the GEO accession endpoint and parses it (no GEOparse dep); carries characteristics_ch1 verbatim in the RawRecord. discover() is the identity on a GSEnnnnn (mirrors CELLxGENE's DOI identity). - normalize/geo.py: agent-backed GeoNormalizer + GeoExtraction schema. The LLM fills the schema via response_format — design covariates only; the schema has no field for any data-inferred annotation, so leakage is structurally impossible. SampleNodes now populated (organism + data_uri read deterministically from structured SOFT fields; condition/perturbation/ timepoint/subject from the LLM). One SampleNode per real GSM. No DatasetNode: a series is the study, so design-context + HAS_SAMPLE edges originate at the StudyNode (merge keys on entity ontology_id targets — ARCHITECTURE §4). - agent/base.py: narrow synchronous StructuredExtractor seam the normalizers depend on (inject a fake to test offline). - agent/extraction.py: AzureExtractionAgent (the only Azure-touching module) bridges the async agent-framework API to the sync seam; make_ontology_fallback wires the agent in as the resolver's opt-in LLM fallback. - Removed the parce.agent.* mypy exemption: all of src/parce is type-checked. - main.py: `parce geo GSEnnnnn` path (deferred Azure import); shared persist step. - Settings: optional ncbi_email/ncbi_api_key (+ .env.example). Gates (hermetic, no .env): ruff check, ruff format --check (47), mypy (28), 168 unit tests; 13 integration deselected. Live TestLiveGeoFetch passes against the real GEO endpoint. Live Azure extraction is the documented blocker — unverified here (no AZURE_AI_PROJECT_ENDPOINT in the headless env). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No code change. Verifies the one open blocker on PR 5 (#9): the live Azure extraction round-trip, previously unverified for lack of reachable creds. - Sourced AZURE_AI_* from the repo-root .env (Settings reads the worktree root, which has none) with the active az login; ran the marked integration suite: `pytest -m integration tests/test_geo_integration.py` -> 2 passed (live GEO fetch + live Azure extraction against the gpt-4o deployment, ~48s). - Re-confirmed all four gates green on this branch (hermetic, no .env): ruff check, ruff format --check (47), mypy (28 files), 168 unit tests. - Prepended a 2026-06-29 Session Log entry; annotated the prior blocker RESOLVED. - Noted a non-blocking agent_framework DeprecationWarning (temperature passed via run() runtime kwargs) for cleanup when PR 7 touches agent/extraction.py. PR 5 is now verified end-to-end; left as draft for human review/merge (not merged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ Integration boundary verified — blocker clearedFollow-up scheduled session (2026-06-29). The one documented blocker on this PR — the live Azure extraction round-trip was unverified — is now resolved. What was run (creds: active All four CI gates re-confirmed green on the branch tip, hermetically (no
Non-blocking follow-up: The GEO vertical slice is verified end-to-end and ready for review/merge. Left as draft, not merged, per the session protocol — a human should merge. Roadmap session log updated in 🤖 Generated with Claude Code |
Scope (roadmap PR 5)
GEO is the project's first unstructured source, so this is the first path with an LLM in it. One focused vertical slice: GEO adapter → agent-backed extraction normalizer → existing ontology grounding → canonical KG.
What's here
sources/geo.py—GeoAdapter(deterministic). Fetches GEO Series+Sample SOFT text from the GEO accession endpoint and parses it with a small focused parser (no GEOparse dependency). Carriescharacteristics_ch1verbatim in theRawRecord— no interpretation.discover()is the identity on aGSEnnnnn(mirrors CELLxGENE's DOI identity; Entrez keyword search → backlog).normalize/geo.py—GeoNormalizer(agent-backed) +GeoExtraction/SampleExtractionschemas. The LLM fills the schema viaresponse_format— design covariates only; the schema has no field for any data-inferred annotation, so leakage is structurally impossible.SampleNodes are now populated (one per realGSM):organism+data_uriare read deterministically from structured SOFT fields,condition/perturbation/timepoint/subjectcome from the LLM. Facets grounded through the existingOntologyResolver.agent/base.py—StructuredExtractor: a narrow synchronous Protocol the normalizers depend on (inject a fake → offline tests).agent/extraction.py—AzureExtractionAgent(the only Azure-touching module): bridges the asyncagent-frameworkAPI to the sync seam.make_ontology_fallbackwires the agent in as the resolver's opt-in LLM fallback (accepts a result only if the CURIE prefix matches the facet's ontology).parce.agent.*mypy exemption — all ofsrc/parceis now type-checked (28 files).main.py:parce geo GSEnnnnnpath (Azure import deferred so the default CELLxGENE path needs no creds). Settings: optionalncbi_email/ncbi_api_key(+.env.example).Key design decisions (full rationale in ROADMAP/ARCHITECTURE)
characteristics_ch1and the series prose.DatasetNodefor GEO — a series is the study, so design-context +HAS_SAMPLEedges originate at theStudyNode. Cross-source merge keys on entityontology_idtargets, so the differing origin vs CELLxGENE doesn't matter (ARCHITECTURE §4).sample_id; a dropped/hallucinated sample can't change graph shape. Extraction failure degrades to samples-without-covariates (logged), never a crash.Verification (all four CI gates, hermetic — no
.envin the worktree)ruff check .✅ ·ruff format --check .✅ (47 files)mypy src/parce✅ (28 files)pytest -m "not integration"✅ 168 passed, 13 deselectedTestLiveGeoFetchrun against the real GEO endpoint — passes (SOFT parser validated onGSE10072).The live Azure extraction round-trip is unverified: this headless env has
az loginbut noAZURE_AI_PROJECT_ENDPOINTconfigured (no worktree.env), soTestLiveGeoExtractionskips. The Azure call shape mirrors the previously-workingagent/curator.py(agent.run(prompt, response_format=Model)→result.value). Before relying on the GEO path: with Azure creds, runuv run pytest -m integration tests/test_geo_integration.py.🤖 Generated with Claude Code