Skip to content

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 AnalysisResult JSON.

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 ​

ModulePathFunction
table_extractortable_extractor/parser.py (SyscohadaTableParser, SYSCOHADA_TAXONOMY)Regex taxonomy → canonical keys; Actif≡Passif and VA − Personnel ≡ EBE invariants; bbox binding
ratio_engineratio_engine/calculator.py (DeterministicRatioEngine)Debt, leverage, ICR, DSCR, FRNG/BFR, covenant evaluation; round_ratio = ROUND_HALF_UP
contradiction_ragcontradiction_rag/detector.py (CrossDocumentContradictionDetector)Pairwise cross-document variances on ExtractedClauseFact
memo_generatormemo_generator/drafter.pyCompiles the memo with [FAIT] / [DÉDUCTION] / [QUESTION] tags
ocr_engineocr_engine/tesseract_engine.pyTesseract fra+eng, pypdfium2 rasterization
fixed_incomefixed_income/{curve_fitting,pricing_engine,parser}Nelson–Siegel, pricing, relative value
run_pipeline.pylocus-workers/run_pipeline.pyDeterministic end-to-end CLI pipeline
scripts/market_ingest_producer.pylocus-workers/scripts/Market ingest CLI — see Market ingest
testslocus-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 + pathPurposeNotes
GET /healthService + OCR capability probestatus: HEALTHY, service: locus-analytical-worker-python, standards: [SYSCOHADA Revised, COSUMAF-SDB-01/2021, ISO 9001:2015], ocr: { engine: tesseract, available, lang }
POST /api/v1/ocrHigh-fidelity Tesseract OCRBody { 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-syscohadaParse OCR text into canonical SYSCOHADA line valuesBody { text, periodLabel } → { periodLabel, values, engine: "SyscohadaTableParser" }; called by DossierAnalyticsService after PersistOcr
POST /api/v1/analyzeDeterministic analysis for a dealBody AnalyzeDealRequest; returns AnalysisResultResponse; 422 when fail-closed (no fixtures), 500 otherwise
GET /api/v1/deals/{dealId}/resultsLatest computed AnalysisResultReads 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 ​

FieldTypeMeaning
dealIdstringTarget deal / mandate
tenantIdstring?Tenant organization
scenariostring?default BASE_CASE
jobIdstring?Job id from the C# orchestrator
requestedBystring?Requesting user
useDiskFixturesboolLegacy 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"
  }
}
BlockPydantic model (server.py)Fields
engineEngineMetadataname, version
metricsFinancialMetricsModelrevenue, ebitda, netDebt, netDebtEbitda, interestCoverage, dscr, bfrDays
covenants[]CovenantCheckModelname, actual, limit, status
findings[]FindingModelid, type, severity, description, varianceMetric, documentA, documentB
sources[]SourceModeldocumentId, documentName, page, boundingBox: [x0, top, x1, bottom]
artifactsDict[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.md

The 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
1Long-running workloads (OCR + contradiction: 30 s – 4 min)
2Concurrency & resource isolation (Redis job store caps load)
3Container / cloud portability (no shared filesystem assumption)
4Typed AnalysisResult JSON instead of unformatted Markdown on disk
5Security (no user parameters reaching OS shell commands)
6Observability (phases, duration, structured failure)

Ops notes ​

  • Dockerfile installs tesseract-ocr + tesseract-ocr-fra + tesseract-ocr-eng; env OCR_TESSERACT_LANG (default fra+eng).
  • Coolify: point WorkerSettings__PythonWorkerUrl (SoR) and LOCUS_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.

Source of trust for Locus. If this conflicts with code, code + IMPLEMENTATION_TRACKER.md win.