diff --git a/src/casecrawler/api/routes/datasets.py b/src/casecrawler/api/routes/datasets.py index 9057be3..e125679 100644 --- a/src/casecrawler/api/routes/datasets.py +++ b/src/casecrawler/api/routes/datasets.py @@ -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 @@ -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, @@ -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, + "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() diff --git a/tests/test_api_datasets.py b/tests/test_api_datasets.py index 0318fd3..53fe836 100644 --- a/tests/test_api_datasets.py +++ b/tests/test_api_datasets.py @@ -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, @@ -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) @@ -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)