Skip to content

PR 5: GEO extraction agent (vertical slice) - #9

Merged
mengerj merged 1 commit into
mainfrom
pr5-geo-extraction-agent
Jun 29, 2026
Merged

PR 5: GEO extraction agent (vertical slice)#9
mengerj merged 1 commit into
mainfrom
pr5-geo-extraction-agent

Conversation

@mengerj

@mengerj mengerj commented Jun 28, 2026

Copy link
Copy Markdown
Owner

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.

Branched off the latest origin/main (4dcd5b7, post PR 4 + PR 4b). The routine worktree's local main was 2 merges stale and its roadmap wrongly showed PR 4 as "Next up"; caught via git fetch + compare, so PR 4 was not rebuilt.

What's here

  • sources/geo.pyGeoAdapter (deterministic). Fetches GEO Series+Sample SOFT text from the GEO accession endpoint and parses it with a small focused parser (no GEOparse dependency). Carries characteristics_ch1 verbatim in the RawRecord — no interpretation. discover() is the identity on a GSEnnnnn (mirrors CELLxGENE's DOI identity; Entrez keyword search → backlog).
  • normalize/geo.pyGeoNormalizer (agent-backed) + GeoExtraction/SampleExtraction schemas. The LLM fills the schema via response_formatdesign covariates only; the schema has no field for any data-inferred annotation, so leakage is structurally impossible. SampleNodes are now populated (one per real GSM): organism + data_uri are read deterministically from structured SOFT fields, condition/perturbation/timepoint/subject come from the LLM. Facets grounded through the existing OntologyResolver.
  • agent/base.pyStructuredExtractor: a narrow synchronous Protocol the normalizers depend on (inject a fake → offline tests).
  • agent/extraction.pyAzureExtractionAgent (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 (accepts a result only if the CURIE prefix matches the facet's ontology).
  • Removed the parce.agent.* mypy exemption — all of src/parce is now type-checked (28 files).
  • main.py: parce geo GSEnnnnn path (Azure import deferred so the default CELLxGENE path needs no creds). Settings: optional ncbi_email/ncbi_api_key (+ .env.example).

Key design decisions (full rationale in ROADMAP/ARCHITECTURE)

  • Deterministic where GEO is structured, LLM only for the free text. Organism + suppl URI are read straight from SOFT; the LLM only parses characteristics_ch1 and the series prose.
  • No DatasetNode for GEO — a series is the study, so design-context + HAS_SAMPLE edges originate at the StudyNode. Cross-source merge keys on entity ontology_id targets, so the differing origin vs CELLxGENE doesn't matter (ARCHITECTURE §4).
  • Sample set is the record's, not the LLM's — extraction matched in by 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 .env in the worktree)

  • ruff check . ✅ · ruff format --check . ✅ (47 files)
  • mypy src/parce ✅ (28 files)
  • pytest -m "not integration"168 passed, 13 deselected
  • Live TestLiveGeoFetch run against the real GEO endpoint — passes (SOFT parser validated on GSE10072).

⚠️ Blocker (integration boundary, per session protocol)

The live Azure extraction round-trip is unverified: this headless env has az login but no AZURE_AI_PROJECT_ENDPOINT configured (no worktree .env), so TestLiveGeoExtraction skips. The Azure call shape mirrors the previously-working agent/curator.py (agent.run(prompt, response_format=Model)result.value). Before relying on the GEO path: with Azure creds, run uv run pytest -m integration tests/test_geo_integration.py.

🤖 Generated with Claude Code

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>
@mengerj
mengerj marked this pull request as ready for review June 29, 2026 06:06
@mengerj
mengerj merged commit b1103d0 into main Jun 29, 2026
2 checks passed
mengerj added a commit that referenced this pull request Jun 29, 2026
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>
@mengerj

mengerj commented Jun 29, 2026

Copy link
Copy Markdown
Owner Author

✅ Integration boundary verified — blocker cleared

Follow-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 az login + AZURE_AI_* sourced from the repo-root .env; Settings reads the worktree root, which has no .env, which is why the prior session saw it as unconfigured):

uv run pytest -m integration tests/test_geo_integration.py
# 2 passed in 48.34s
#   TestLiveGeoFetch::test_fetch_real_series          PASSED
#   TestLiveGeoExtraction::test_fetch_extract_normalize PASSED  ← live gpt-4o extraction

All four CI gates re-confirmed green on the branch tip, hermetically (no .env):

gate result
ruff check .
ruff format --check . ✅ 47 files
mypy src/parce ✅ 28 files
pytest -m "not integration" ✅ 168 passed, 13 deselected

Non-blocking follow-up: agent_framework emits a DeprecationWarningagent/extraction.py passes temperature via run(options=...) runtime kwargs; the new API wants client_kwargs / function_invocation_kwargs. Works today; worth tidying when PR 7 (PRIDE) next touches that module.

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 ea72f76.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant