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
34 changes: 34 additions & 0 deletions src/casecrawler/api/routes/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Literal
from uuid import uuid4

from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import FileResponse, PlainTextResponse, Response, StreamingResponse
Expand All @@ -30,7 +31,9 @@
build_export_transparency_summary,
infer_dataset_origin_flags,
)
from casecrawler.generation.blueprint_pipeline import BlueprintPipeline
from casecrawler.generation.synthetic_pipeline import SyntheticPipeline
from casecrawler.models.blueprint import BlueprintGenerationRequest
from casecrawler.models.dataset import (
ExportFormat,
GenerationRequest,
Expand Down Expand Up @@ -196,6 +199,37 @@ async def generate_dataset(req: GenerationRequest):
}


@router.post("/datasets/blueprints/generate")
async def generate_blueprint_dataset(req: BlueprintGenerationRequest):
config = get_config()
max_count = config.synthetic.max_api_generation_count
max_returned = config.synthetic.max_api_returned_records
if req.target_count > max_count:
raise HTTPException(
status_code=422,
detail=f"target_count must be less than or equal to {max_count}",
)

dataset_id = f"blueprint-ds-{uuid4()}"
store = DatasetStore.shared()
try:
result = await BlueprintPipeline().generate(
req,
dataset_id=dataset_id,
store=store,
)
except ValueError as err:
raise HTTPException(status_code=422, detail=str(err)) from err
returned_blueprints = result.blueprints[:max_returned]
return {
"dataset_id": result.dataset_id,
Comment on lines +213 to +225

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return the route-generated dataset ID, not the pipeline-reported one.

This handler creates dataset_id on Line 213 and uses that ID for pipeline/store side effects, but the response on Line 225 trusts result.dataset_id. If those ever diverge, clients get an ID that does not match the persisted dataset and follow-up reads will break.

Suggested fix
     dataset_id = f"blueprint-ds-{uuid4()}"
     store = DatasetStore.shared()
     try:
         result = await BlueprintPipeline().generate(
             req,
             dataset_id=dataset_id,
             store=store,
         )
     except ValueError as err:
         raise HTTPException(status_code=422, detail=str(err)) from err
+    if result.dataset_id != dataset_id:
+        raise HTTPException(
+            status_code=500,
+            detail="blueprint pipeline returned a mismatched dataset_id",
+        )
     returned_blueprints = result.blueprints[:max_returned]
     return {
-        "dataset_id": result.dataset_id,
+        "dataset_id": dataset_id,
         "generated": result.generated_count,
         "total_blueprints": len(result.blueprints),
         "plan": result.plan.model_dump(),
         "blueprints": [blueprint.model_dump() for blueprint in returned_blueprints],
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dataset_id = f"blueprint-ds-{uuid4()}"
store = DatasetStore.shared()
try:
result = await BlueprintPipeline().generate(
req,
dataset_id=dataset_id,
store=store,
)
except ValueError as err:
raise HTTPException(status_code=422, detail=str(err)) from err
returned_blueprints = result.blueprints[:max_returned]
return {
"dataset_id": result.dataset_id,
dataset_id = f"blueprint-ds-{uuid4()}"
store = DatasetStore.shared()
try:
result = await BlueprintPipeline().generate(
req,
dataset_id=dataset_id,
store=store,
)
except ValueError as err:
raise HTTPException(status_code=422, detail=str(err)) from err
if result.dataset_id != dataset_id:
raise HTTPException(
status_code=500,
detail="blueprint pipeline returned a mismatched dataset_id",
)
returned_blueprints = result.blueprints[:max_returned]
return {
"dataset_id": dataset_id,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/casecrawler/api/routes/datasets.py` around lines 213 - 225, The response
currently returns result.dataset_id which may diverge from the route-generated
dataset_id; update the handler so that after calling
BlueprintPipeline().generate(...) you always use the locally created dataset_id
variable in the response (e.g., replace uses of result.dataset_id with
dataset_id) while keeping the pipeline call and error handling intact—ensure
BlueprintPipeline.generate(...) and any references to returned_blueprints remain
unchanged except for switching the response's dataset_id to the route-generated
dataset_id.

"generated": result.generated_count,
"total_blueprints": len(result.blueprints),
"plan": result.plan.model_dump(),
"blueprints": [blueprint.model_dump() for blueprint in returned_blueprints],
}


@router.post("/datasets/release-package")
async def generate_release_package(req: ReleasePackageRequest):
config = get_config()
Expand Down
117 changes: 117 additions & 0 deletions tests/test_api_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

from casecrawler.api.routes import datasets as datasets_routes
from casecrawler.api.app import app
from casecrawler.generation.blueprint_pipeline import BlueprintPipelineResult
from casecrawler.models.blueprint import (
BlueprintEvidence,
ClinicalBlueprint,
CohortArchetype,
CohortPlan,
GenerationRole,
)
from casecrawler.models.config import AppConfig, SyntheticConfig
from casecrawler.models.synthetic import (
ComplexityProfile,
Expand All @@ -19,6 +27,49 @@
from casecrawler.storage.dataset_store import DatasetStore


def _blueprint_api_result() -> BlueprintPipelineResult:
archetype = CohortArchetype(
name="anticoagulation decision",
organ_system="cardiovascular",
setting="outpatient",
target_count=1,
acuity_mix={"routine": 1.0},
difficulty_mix={"moderate": 1.0},
required_modalities=[Modality.STRUCTURED_EHR, Modality.CLINICAL_TEXT],
)
plan = CohortPlan(
plan_id="plan-1",
request="Generate anticoagulation decision cases.",
target_count=1,
archetypes=[archetype],
created_by=GenerationRole.PLANNER,
)
blueprint = ClinicalBlueprint(
blueprint_id="bp-1",
dataset_id="ds-blueprint",
cohort_plan_id="plan-1",
archetype_name="anticoagulation decision",
organ_system="cardiovascular",
setting="outpatient",
patient={"age": 72, "sex": "female"},
chief_concern="Atrial fibrillation anticoagulation follow-up.",
diagnoses=[
{
"name": "atrial fibrillation",
"supporting_findings": ["ECG confirms AF"],
}
],
evidence=BlueprintEvidence(
supported_claims=["AF anticoagulation requires renal-dose review."],
),
)
return BlueprintPipelineResult(
dataset_id="ds-blueprint",
plan=plan,
blueprints=[blueprint],
)


def test_generate_dataset_api_smoke(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
client = TestClient(app)
Expand Down Expand Up @@ -152,6 +203,72 @@ def test_generate_dataset_api_rejects_unbounded_counts(tmp_path, monkeypatch):
assert "less than or equal to 1" in response.json()["detail"]


def test_generate_blueprint_dataset_api_uses_model_driven_request(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
captured = []

class FakeBlueprintPipeline:
async def generate(self, req, *, dataset_id, store):
captured.append((req, dataset_id, store))
return _blueprint_api_result()

monkeypatch.setattr(datasets_routes, "BlueprintPipeline", FakeBlueprintPipeline)
client = TestClient(app)

response = client.post(
"/api/datasets/blueprints/generate",
json={
"request": "Generate anticoagulation decision cases.",
"target_count": 1,
"role_policies": [
{
"role": "planner",
"provider": "openrouter",
"model": "anthropic/claude-sonnet-4-6",
},
{
"role": "blueprint_generator",
"provider": "openrouter",
"model": "anthropic/claude-sonnet-4-6",
},
],
},
)

body = response.json()
assert response.status_code == 200
assert body["dataset_id"] == "ds-blueprint"
assert body["generated"] == 1
assert body["plan"]["plan_id"] == "plan-1"
assert body["blueprints"][0]["blueprint_id"] == "bp-1"
assert captured[0][0].request == "Generate anticoagulation decision cases."
assert captured[0][0].target_count == 1
assert captured[0][1].startswith("blueprint-ds-")
assert isinstance(captured[0][2], DatasetStore)


def test_generate_blueprint_dataset_api_rejects_unbounded_counts(
tmp_path,
monkeypatch,
):
monkeypatch.chdir(tmp_path)
config = AppConfig(synthetic=SyntheticConfig(max_api_generation_count=1))
monkeypatch.setattr(datasets_routes, "get_config", lambda: config)
client = TestClient(app)

response = client.post(
"/api/datasets/blueprints/generate",
json={
"request": "Generate anticoagulation decision cases.",
"target_count": 2,
"role_policies": [],
},
)

assert response.status_code == 422
assert "target_count must be less than or equal to 1" in response.json()["detail"]


def test_generate_dataset_api_reports_unknown_recipe(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
client = TestClient(app)
Expand Down
Loading