Skip to content

Commit ecbf9e4

Browse files
committed
fix(pts): preserve valid extractions on dataframe errors
1 parent 735d735 commit ecbf9e4

3 files changed

Lines changed: 54 additions & 5 deletions

File tree

orchestration/src/orchestration/dags/config/aact_trial_extraction.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ bucket: 'gs://aact_data'
77

88
# Pipeline package versions
99
pis_version: '26.9.0.dev0.aacttest.1'
10-
pts_version: '26.9.0.dev0.aacttest.4'
10+
pts_version: '26.9.0.dev0.aacttest.5'
1111

1212
# Each step runs on its own machine, sized for the AACT archive plus its restore
1313
machine_type: 'n1-standard-8'

pts/config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,8 +791,8 @@ steps:
791791
snapshot: '{{snapshot}}'
792792
model: gpt-5-nano-2025-08-07
793793
service_tier: auto
794-
concurrency: 50
795-
shard_size: 500
794+
concurrency: 75
795+
shard_size: 200
796796
publications:
797797
# see PublicationsSpec — this fetches abstracts for every trial, not
798798
# only cache misses, so it needs its own cache before a full run

pts/src/pts/tasks/llm_extract.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414

1515
import hashlib
1616
import json
17+
from collections.abc import Sequence
1718
from concurrent.futures import ThreadPoolExecutor
1819
from datetime import UTC, datetime
1920
from importlib import import_module, resources
2021
from pathlib import Path
21-
from typing import Any, Self
22+
from typing import Any, Self, cast
2223

2324
import polars as pl
2425
from clinical_mining.provider.aact import extract_clinical_report
@@ -28,6 +29,7 @@
2829
parse_batch_results,
2930
sample_report,
3031
)
32+
from clinical_mining.workflows import llm as llm_workflow
3133
from clinical_mining.workflows.llm import run_extraction
3234
from loguru import logger
3335
from otter.manifest.model import Artifact
@@ -381,5 +383,52 @@ def _run_extraction_in_thread(**kwargs: Any) -> pl.DataFrame | None:
381383
a short-lived worker thread gives it a thread-local loop without changing
382384
either library's public API.
383385
"""
386+
def run_with_full_schema_inference() -> pl.DataFrame | None:
387+
# clinical-mining's default inference samples only the first 100 model
388+
# responses. Later validated nested values can then disagree with that
389+
# inferred schema, losing the whole shard before cached_map can stage it.
390+
original = llm_workflow._extractions_to_df
391+
392+
def to_df(extractions: Sequence[BaseModel]) -> pl.DataFrame:
393+
return _models_to_dataframe(extractions)
394+
395+
workflow = cast(Any, llm_workflow)
396+
workflow._extractions_to_df = to_df
397+
try:
398+
return run_extraction(**kwargs)
399+
finally:
400+
workflow._extractions_to_df = original
401+
384402
with ThreadPoolExecutor(max_workers=1, thread_name_prefix='llm-extraction') as executor:
385-
return executor.submit(run_extraction, **kwargs).result()
403+
return executor.submit(run_with_full_schema_inference).result()
404+
405+
406+
def _models_to_dataframe(extractions: Sequence[BaseModel]) -> pl.DataFrame:
407+
"""Convert validated model results without losing a whole shard on one row.
408+
409+
Full inference handles nullable fields that only become populated late in a
410+
shard. The row-wise fallback protects the checkpoint boundary if a model
411+
response still exposes a value Polars cannot combine with its neighbours.
412+
"""
413+
if not extractions:
414+
return pl.DataFrame()
415+
416+
rows = [extraction.model_dump() for extraction in extractions]
417+
try:
418+
return pl.from_dicts(rows, infer_schema_length=None)
419+
except pl.exceptions.PolarsError as error:
420+
logger.warning(
421+
f'could not combine {len(rows)} extractions at once: {error}; '
422+
'falling back to row-wise conversion'
423+
)
424+
425+
converted: list[pl.DataFrame] = []
426+
for row in rows:
427+
try:
428+
converted.append(pl.from_dicts([row], infer_schema_length=None))
429+
except pl.exceptions.PolarsError as error:
430+
logger.error(f'dropping one validated extraction during dataframe conversion: {error}')
431+
432+
if not converted:
433+
return pl.DataFrame()
434+
return pl.concat(converted, how='diagonal_relaxed')

0 commit comments

Comments
 (0)