diff --git a/app/api/routes/extraction.py b/app/api/routes/extraction.py index bf6dc29..28f26ea 100644 --- a/app/api/routes/extraction.py +++ b/app/api/routes/extraction.py @@ -17,6 +17,7 @@ from app.api.schemas.incident_contract import IncidentContract from app.core.config import ( ESTIMATED_EXTRACTION_SECONDS, + EXTRACTION_ALLOW_RERUN, EXTRACTION_POLL_INTERVAL_SECONDS, ) from app.core.errors.base import AppError @@ -91,7 +92,7 @@ def create_extraction( ) existing = get_extraction_by_input(db, input_id) - if existing is not None: + if existing is not None and not EXTRACTION_ALLOW_RERUN: raise AppError( "An extraction already exists for this input", status_code=409, diff --git a/app/core/config.py b/app/core/config.py index e956cc6..40ba28b 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -123,6 +123,12 @@ # manual entry rather than guessed at. EXTRACTION_CHUNK_RETRIES = int(os.getenv("EXTRACTION_CHUNK_RETRIES", "1")) +# One input gets one extraction, which makes re-testing the same narrative mean +# re-uploading it. Switching this on lets a repeat run through; every run still +# gets its own extraction and its own draft incident, so the old ones stay put. +# Development only, leave it off anywhere real. +EXTRACTION_ALLOW_RERUN = os.getenv("EXTRACTION_ALLOW_RERUN", "false").strip().lower() == "true" + # The contract file the chunk registry reads its tiers and triggers from. INCIDENT_CONTRACT_PATH = Path( os.getenv("INCIDENT_CONTRACT_PATH", BASE_DIR / "contracts" / "schemas" / "incident-contract.yaml") diff --git a/docker/.env.example b/docker/.env.example index 446336b..3bb20f6 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -83,6 +83,10 @@ LLM_EXTRA_HEADERS= # --- Extraction ----------------------------------------------------------- # Extra tries after a chunk's first answer fails validation. EXTRACTION_CHUNK_RETRIES=1 +# Let the same input be extracted more than once. Development only: it makes +# testing a narrative repeatedly possible without re-uploading it. Each run +# still gets its own extraction and its own draft incident. +EXTRACTION_ALLOW_RERUN=false # Deployment context the extractor falls back on when the narrative is silent. # A request can override any of these through the extraction body's "defaults". FIREFORM_DEFAULT_COUNTRY=US diff --git a/tests/test_v1_extraction.py b/tests/test_v1_extraction.py index ec2f226..94d54fe 100644 --- a/tests/test_v1_extraction.py +++ b/tests/test_v1_extraction.py @@ -163,12 +163,26 @@ def test_409_input_not_ready(self, client, db): def test_409_extraction_already_exists(self, client, db): inp = _ready_input(db) existing = create_extraction(db, Extraction(input_id=inp.input_id)) - resp, _ = self._post(client, inp.input_id) + # Pinned rather than left to the environment, so a developer running + # with the rerun flag on still tests the shipped behaviour. + with patch("app.api.routes.extraction.EXTRACTION_ALLOW_RERUN", False): + resp, _ = self._post(client, inp.input_id) assert resp.status_code == 409 body = resp.json() assert body["error_code"] == "EXTRACTION_EXISTS" assert body["detail"]["existing_extract_id"] == str(existing.extract_id) + def test_202_rerun_allowed_when_flag_is_on(self, client, db): + # Development escape hatch: the same narrative can be extracted again + # instead of having to be re-uploaded. The earlier extraction stays. + inp = _ready_input(db) + existing = create_extraction(db, Extraction(input_id=inp.input_id)) + with patch("app.api.routes.extraction.EXTRACTION_ALLOW_RERUN", True): + resp, _ = self._post(client, inp.input_id) + assert resp.status_code == 202 + assert resp.json()["extract_id"] != str(existing.extract_id) + assert get_extraction(db, existing.extract_id) is not None + def test_503_ollama_unavailable(self, client, db): inp = _ready_input(db) resp, _ = self._post(client, inp.input_id, ollama_up=False)