5.8 KiB
Phase 2 Handoff — Prescreen Stage
Scope: insert a deterministic prescreen stage between extraction and LLM analysis for Russian/Belarusian contracts. Completed: 2026-08-16.
Goal
Short-circuit obvious low-risk contracts and protect the expensive LLM worker
from trivial documents. Use a deterministic regex+pydantic extractor because the
Needle 2 (cactus-needle) model is English-only and unusable for RU/BY contracts
(see docs/SPIKE_PHASE0.md).
What changed
DB schema (migrations/versions/0006_prescreen.py)
- New
prescreen_resultstable with contract metadata, field-coverage score, routing decision, and lightweight auto-approve output. documents.statusadds'prescreening'.jobs.queueadds'prescreen'.reportsgainsprescreen_result_idFK andprescreen_metaJSONB.
Models (src/contract_check/core/db/)
models.py: addedPrescreenResultmodel and relationships.enums.py:DocStatusandQueueNameliterals updated.
RabbitMQ topology (core/mq/topology.py)
- Added
prescreen.q,prescreen.retry.q,prescreen.dlq. - Added routing keys
prescreen/retry.prescreen.
Messages (core/mq/messages.py)
PrescreenRequested— worker-extract → prescreen.q.PrescreenCompleted— prescreen result, persisted and forwarded.AnalyzeRequested— worker-prescreen → analyze.q, carriesprescreen_meta.DocumentExtractedkept for backward compatibility but no longer published.
New service: worker_prescreen/
| File | Purpose |
|---|---|
extractor.py |
Regex extractor for RU/BY contracts; returns PrescreenContractMeta + confidence score. |
router.py |
Routing decision: auto_approve / manual_review / deep_analysis. auto_approve is disabled by default. |
handler.py |
Download Markdown, run extractor, persist prescreen_results, publish next message. |
consumer.py |
RabbitMQ consumer for prescreen.q. |
config.py |
PrescreenSettings with prefetch tunable. |
__main__.py |
Entrypoint, metrics server on :9104. |
Wired existing services
worker_extract/handler.py: now publishesPrescreenRequestedtoprescreen.qand setsdocuments.status = 'prescreening'.worker_analyze/consumer.py+handler.py: now consumesAnalyzeRequested, receivesprescreen_meta, appends it to the LLM prompt as known contract fields.core/llm/port.py+ollama_cloud.py:analyze()acceptsextra_context: strand injects it into each chunk's user prompt.
Configuration (core/config.py)
New envs:
PRESCREEN_ENABLED(defaulttrue)PRESCREEN_AUTO_APPROVE(defaultfalse)PRESCREEN_CONFIDENCE_THRESHOLD(default0.75)PRESCREEN_HIGH_VALUE_THRESHOLD(default100000)
Metrics (core/metrics.py)
prescreen_duration_seconds{decision}prescreen_runs_total{decision,contract_type}prescreen_confidencehistogram
Docker / compose
- New
srv/worker-prescreen/Dockerfile(lean, no tesseract). - Added
worker-prescreenservice todocker-compose.ymlon port9104.
Dependencies (pyproject.toml)
- Added
prescreendependency group (core + db/mq/s3/obs). - Added to
devgroup.
Tests
- Unit:
tests/unit/test_prescreen_extractor.py,tests/unit/test_prescreen_router.py. - Integration: updated
test_extract_worker.py,test_analyze_worker.py,test_upload_pipeline.py,test_b2b_api.pyfor the new pipeline.
Docs
- Updated
docs/ARCHITECTURE.mdwith prescreen topology, messages, schema, metrics, and Docker/compose rows.
Behavior
- Upload →
extract.q(unchanged). - worker-extract uploads Markdown to MinIO, publishes
PrescreenRequested. - worker-prescreen:
- extracts contract type, parties, amount, currency, dates, penalty/ termination/arbitration clauses via regex
confidence_score = matched_fields / total_fields- routes:
deep_analysisif high value, penalty clause, or arbitrationmanual_reviewif low confidence, missing parties, or auto_approve disabledauto_approveonly whenPRESCREEN_AUTO_APPROVE=trueand low risk
deep_analysis→AnalyzeRequested→ worker-analyze with prescreen context.manual_review→ terminal DB state, no LLM call, credit not refunded (this is an intentional routing outcome, not a failure).auto_approve→ lightweight report row,status=done.
Verification
make lint # passed
make typecheck # passed (99 files)
make test-unit # 147 passed, 32 deselected
make test-integration # 32 passed, 147 deselected (with live workers stopped to avoid races)
Deployment notes
- Run migration:
make migrate(ordocker compose --profile services run --rm api alembic upgrade head). - Rebuild images:
docker compose --profile services up -d --build. - New worker-prescreen must be started; worker-extract and worker-analyze images also changed.
- No application code outside core/workers was changed; the API just returns
the new
prescreeningstatus string.
Deviations from the original prescreen spec
- Extractor: regex instead of Needle 2. Rationale in
docs/SPIKE_PHASE0.md. - No separate credits split: the credit is still reserved once on upload; prescreen is treated as part of the same paid job.
- No admin/web SPA yet:
manual_reviewdocuments are terminal in the DB until the admin panel lands.
Next step
Phase 3 is storage modernization evaluation (RustFS watch). The current stack keeps MinIO as the production default; RustFS is tracked as a future option. Alternatively, continue with the backlog: admin/web SPA, ЮKassa payments, heavy OCR adapters.
Recommended immediate follow-up: end-to-end smoke test in Docker with all
services running (make services-up) and a real PDF/DOCX upload through the
API or bot, verifying that worker-prescreen routes to analyze.q or
manual_review correctly.