Appearance
locus-workers
Python 3.12 · FastAPI (server.py) · port 8000 (PORT, HOST env) · version string 2.5.0 on /health.
Responsibilities
Deterministic analytical pipelines called by the C# AnalysisOrchestrator and by the BFF OCR escalation path:
- Arbitrary-precision SYSCOHADA calculations (
Decimal,ROUND_HALF_UP). - Nelson–Siegel sovereign curve fitting and fixed-income pricing helpers.
- Spatial OCR bounding-box extraction (Tesseract).
- Pairwise contradiction evaluation.
- Emits typed, structured
AnalysisResultJSON.
Principle: reproducible numeric outputs — no LLM for figures. Workers hold no tenant state; the SoR persists everything.
text
locus-backend AnalysisOrchestrator ──POST /api/v1/analyze──►┐
locus-web BFF extractPagesWithEngine ──POST /api/v1/ocr────►│ locus-workers :8000
locus-backend DossierAnalyticsService ─POST /api/v1/extract-syscohada─►┘Module table
| Module | Path | Function |
|---|---|---|
table_extractor | table_extractor/parser.py (SyscohadaTableParser, SYSCOHADA_TAXONOMY) | Regex taxonomy → canonical keys; Actif≡Passif and VA − Personnel ≡ EBE invariants; bbox binding |
ratio_engine | ratio_engine/calculator.py (DeterministicRatioEngine) | Debt, leverage, ICR, DSCR, FRNG/BFR, covenant evaluation; round_ratio = ROUND_HALF_UP |
contradiction_rag | contradiction_rag/detector.py (CrossDocumentContradictionDetector) | Pairwise cross-document variances on ExtractedClauseFact |
memo_generator | memo_generator/drafter.py | Compiles the memo with [FAIT] / [DÉDUCTION] / [QUESTION] tags |
ocr_engine | ocr_engine/tesseract_engine.py | Tesseract fra+eng, pypdfium2 rasterization |
fixed_income | fixed_income/{curve_fitting,pricing_engine,parser} | Nelson–Siegel, pricing, relative value |
run_pipeline.py | locus-workers/run_pipeline.py | Deterministic end-to-end CLI pipeline |
scripts/market_ingest_producer.py | locus-workers/scripts/ | Market ingest CLI — see Market ingest |
| tests | locus-workers/tests/ | test_table_extractor, test_ratio_engine, test_contradiction_rag, test_ocr_engine, test_regulatory_engine, fixed_income/, regulatory/ |
HTTP endpoints (server.py)
| Method + path | Purpose | Notes |
|---|---|---|
GET /health | Service + OCR capability probe | status: HEALTHY, service: locus-analytical-worker-python, standards: [SYSCOHADA Revised, COSUMAF-SDB-01/2021, ISO 9001:2015], ocr: { engine: tesseract, available, lang } |
POST /api/v1/ocr | High-fidelity Tesseract OCR | Body { filename, contentBase64, lang?, maxPages (1–100, default 40) }; 40 MB decoded guardrail (413 OCR_TOO_LARGE); 400 OCR_BAD_PAYLOAD; 503 OCR_ENGINE_UNAVAILABLE; 422 engine errors |
POST /api/v1/extract-syscohada | Parse OCR text into canonical SYSCOHADA line values | Body { text, periodLabel } → { periodLabel, values, engine: "SyscohadaTableParser" }; called by DossierAnalyticsService after PersistOcr |
POST /api/v1/analyze | Deterministic analysis for a deal | Body AnalyzeDealRequest; returns AnalysisResultResponse; 422 when fail-closed (no fixtures), 500 otherwise |
GET /api/v1/deals/{dealId}/results | Latest computed AnalysisResult | Reads data/extracted_tables/analysis_result.json; 404 if absent |
POST /api/v1/ocr response
json
{
"engine": "tesseract",
"path": "tesseract",
"ocrConfidence": 0.91,
"pagesExtracted": 12,
"layoutBoxesCount": 4310,
"boundingBoxes": [{ "page": 1, "box": [x0, y0, x1, y1], "text": "…", "confidence": 0.93 }],
"pages": [ /* per-page text + words */ ],
"diagnostic": "Tesseract OCR (fra+eng)"
}boundingBoxes is capped at 200 entries (first 40 words per page); each pages[] entry carries the full word list that the SoR stores in ExtractedPage.WordsJson.
AnalyzeDealRequest
| Field | Type | Meaning |
|---|---|---|
dealId | string | Target deal / mandate |
tenantId | string? | Tenant organization |
scenario | string? | default BASE_CASE |
jobId | string? | Job id from the C# orchestrator |
requestedBy | string? | Requesting user |
useDiskFixtures | bool | Legacy only — SEEG disk fixtures; production callers must leave false |
documentTexts | [{documentId, documentName, page, text}]? | Optional OCR page payloads |
The C# orchestrator sends { dealId, tenantId, jobId, scenario: "BASE_CASE", requestedBy, useDiskFixtures: false } via the named HttpClient PythonAnalyticalWorker.
AnalysisResult JSON contract
The canonical schema consumed by dashboards, export engines and the Copilot (technical documentation §2.2):
json
{
"analysisId": "analysis_seeg_2026",
"dealId": "deal_seeg_refinancing_2026",
"engine": { "name": "cemara-analytical-worker-python", "version": "2.4.0" },
"status": "completed",
"metrics": {
"revenue": 198450000000.0,
"ebitda": 34850000000.0,
"netDebt": 87900000000.0,
"netDebtEbitda": 2.52,
"interestCoverage": 3.20,
"dscr": 1.08,
"bfrDays": 32.2
},
"covenants": [
{ "name": "Net Debt / EBITDA", "actual": 2.52, "limit": 3.50, "status": "PASS" },
{ "name": "Interest Coverage Ratio (ICR)", "actual": 3.20, "limit": 2.50, "status": "PASS" },
{ "name": "Debt Service Coverage Ratio (DSCR)", "actual": 1.08, "limit": 1.20, "status": "WARNING" }
],
"findings": [
{
"id": "FINDING-001",
"type": "CONTRADICTION",
"severity": "CRITICAL",
"description": "Conflicting Interest Rate / Margin (0.45% Variance)",
"varianceMetric": "45 bps discrepancy",
"documentA": "03_term_sheet_syndication.pdf",
"documentB": "02_convention_pret_relais_53m_eur.pdf"
}
],
"sources": [
{
"documentId": "doc_01_financial_statements",
"documentName": "01_seeg_etats_financiers_syscohada_2023.pdf",
"page": 14,
"boundingBox": [75, 150, 525, 420]
}
],
"artifacts": {
"markdownMemo": "preliminary_investment_memo.md",
"jsonResult": "analysis_result.json"
}
}| Block | Pydantic model (server.py) | Fields |
|---|---|---|
engine | EngineMetadata | name, version |
metrics | FinancialMetricsModel | revenue, ebitda, netDebt, netDebtEbitda, interestCoverage, dscr, bfrDays |
covenants[] | CovenantCheckModel | name, actual, limit, status |
findings[] | FindingModel | id, type, severity, description, varianceMetric, documentA, documentB |
sources[] | SourceModel | documentId, documentName, page, boundingBox: [x0, top, x1, bottom] |
artifacts | Dict[str, str] | file refs; response also carries completedAt |
SoR variant
When the SoR itself builds the result (DossierAnalyticsService, engine locus-dossier-analytics 3.0.0), covenants use Net Debt / EBITDA limit 3.50 (BREACH), ICR limit 2.50 (BREACH), DSCR limit 1.20 (WARNING), and artifacts = { source: "postgres_sor", engine: "dossier-analytics-v3" }. Contract tests: analytical_jobs_and_contracts.test.ts. The worker docs quote DSCR target ≥ 1.30× (WARNING below); see SYSCOHADA & ratios.
run_pipeline.py
CLI runner kept for deterministic regression / local demo (locus-workers/run_pipeline.py). It loads data/deal_room/deal_room_manifest.json and data/extracted_tables/seeg_syscohada_normalized_2021_2023.json, then chains the modules:
mermaid
sequenceDiagram
autonumber
participant CLI as run_pipeline.py
participant P as SyscohadaTableParser
participant R as DeterministicRatioEngine
participant C as CrossDocumentContradictionDetector
participant M as InvestmentMemoGenerator
CLI->>P: parse_dict(period) for 2021-2023
P-->>CLI: normalized periods + delta checks
CLI->>R: compute_period_ratios(period)
R-->>CLI: ratios history
CLI->>C: find_contradictions(facts)
C-->>CLI: alerts (rate, maturity, EBITDA, pledge)
CLI->>M: ratios + citations + alerts
M-->>CLI: preliminary_investment_memo.mdThe historical POST /api/pipeline route in locus-web (exec(python3 run_pipeline.py) in the original prototype) is the "Lancer l'analyse" bridge; the production path is the asynchronous SoR job (see Dossier → OCR → analysis).
Why async jobs replaced exec()
| # | Reason |
|---|---|
| 1 | Long-running workloads (OCR + contradiction: 30 s – 4 min) |
| 2 | Concurrency & resource isolation (Redis job store caps load) |
| 3 | Container / cloud portability (no shared filesystem assumption) |
| 4 | Typed AnalysisResult JSON instead of unformatted Markdown on disk |
| 5 | Security (no user parameters reaching OS shell commands) |
| 6 | Observability (phases, duration, structured failure) |
Ops notes
- Dockerfile installs
tesseract-ocr+tesseract-ocr-fra+tesseract-ocr-eng; envOCR_TESSERACT_LANG(defaultfra+eng). - Coolify: point
WorkerSettings__PythonWorkerUrl(SoR) andLOCUS_WORKER_URL(web) at the internal service hostname. - Run locally:
cd locus-workers && python server.py; tests:python3 -m unittest discover -s tests -p 'test_*.py'. - CORS is permissive (
*) — the service must stay on an internal network; never expose it publicly. - SLA references: Nelson–Siegel calibration < 500 ms; document upload + SHA-256 < 2000 ms.