Diagnosis Engine — Disease Cards, Curator, and Document Generation
Audiences: doctor, clinical-buyer, developer, investor
The Diagnosis Engine is three coordinated sub-systems: a curated DiseaseCard knowledge base (structured disease definitions with comorbidity graphs and RAG-indexed guidelines), an AI Curator agent (LLM-driven curation runs that mutate and version disease knowledge with rollback), and a Document Generation Engine (parallel AI section generation from versioned templates, exported as PDF via
ddx-pdf-engine).
Business Purpose
Clinical decision support requires two things: reliable, up-to-date clinical knowledge, and the ability to express that knowledge in patient-facing documents. Manually maintaining disease encyclopedias and generating per-patient documents from scratch would take hours per case.
Dudoxx HMS resolves this at three levels:
- DiseaseCards + Curator: Clinical administrators curate a structured disease knowledge base (ICD-coded cards with symptoms, treatments, comorbidities, diagnostic flows). The AI Curator agent automatically proposes knowledge mutations from external guidelines (WHO, AWMF, PubMed PDF uploads) and commits them as auditable
KnowledgeMutationrows withbeforeJson/afterJsondiffs — fully rollback-safe. - DiagnosticFlows: Each disease card can carry a JSON decision-tree graph, validated server-side, that guides clinicians through a structured diagnostic protocol step-by-step.
- Document Generation: TUCAN tools generate clinical documents (intake summaries, patient letters, diagnostic reports) by instantiating versioned templates, running sections in parallel through an AI sub-agent, and exporting the final HTML to PDF via the
ddx-pdf-engine(Playwright/WeasyPrint).
Audiences
- Investor: The Curator agent demonstrates autonomous clinical knowledge management — a clinic can upload a new AWMF guideline PDF and the AI proposes, diffs, and applies all changes to the disease catalog with one click. This is a strong SaaS moat: competitors sell static medical databases.
- Clinical buyer (doctor/nurse/receptionist): Doctors see diagnostic flow decision trees when opening a disease card. The TUCAN assistant can generate a patient intake summary document (PDF) in real-time while the doctor speaks. All documents are versioned and reviewable before publication.
- Developer/partner:
POST /api/v1/document-templatescreates a template.POST /api/v1/generated-documentsinstantiates it.GET /api/v1/generated-documents/:id/export?format=pdfexports the finished document. Curator runs:POST /api/v1/diagnosis-engine/curator/runs. - Internal (ops/support): DiseaseCards and DiagnosticFlows in Prisma
ddx_api_main. Document templates, generated instances, and sections in Prismaddx_api_ai. Curator SSE events:CURATOR_RUN_STARTED,CURATOR_RUN_COMPLETED,CURATOR_MUTATION_APPLIED. Document SSE:DOCUMENT_GENERATION_START,DOCUMENT_SECTION_COMPLETE,DOCUMENT_GENERATION_COMPLETE.
Architecture
Diagnosis Engine — 4 subsystems, all on ddx_api_main:
| Subsystem (store) | Component | Responsibility |
|---|---|---|
DiseaseCards (ddx_api_main) | DiseaseCardsController | — |
| ” | DiseaseCardsService | — |
| ” | DiseaseCardCrudService | — |
| ” | DiseaseCardSearchService | — |
| ” | DiseaseCardVersionService | — |
| ” | ComorbidityGraphService | — |
| ” | DiagnosisRagService | Qdrant semantic search over disease corpus |
DiagnosticFlows (ddx_api_main) | DiagnosticFlowsService | — |
| ” | FlowValidatorService | validates JSON flow graph before save |
PatientDiagnoses (ddx_api_main) | PatientDiagnosesService | links PatientDiagnosis to DiseaseCard + Practitioner |
Curator (ddx_api_main) | CuratorController | /diagnosis-engine/curator |
| ” | CurationRunService | start / execute / cancel runs |
| ” | ↳ knowledgeCuratorWorkflow | Mastra AI workflow (in-process) |
| ” | KnowledgeMutationService | per-mutation CRUD + rollback |
| ” | ExternalReferenceService | attach references to entities |
| ” | EntityAssetService | MinIO image upload for entities |
| ” | GuidelinesRagService | Qdrant wing ddx-guidelines PDF ingest |
Document Generation Engine (cross-cutting):
| Component | Responsibility |
|---|---|
DocumentTemplatesController | /api/v1/document-templates |
DocumentTemplatesService | PUBLISHED template lifecycle (ddx_api_ai) |
DocumentTemplateVersionsService | semantic versioning, immutability policy |
GeneratedDocumentsController | /api/v1/generated-documents |
DocumentGenerationOrchestratorService | create instance + input context |
ParallelReportGenerationService | parallel section AI generation (p-limit) |
DocumentExportService | PDF/HTML export (PDF Engine → PDFKit fallback) |
Curator fires-and-forgets: POST /diagnosis-engine/curator/runs returns the CurationRun immediately, then uses setTimeout(100ms) to let the HTTP response return and let the frontend mount its SSE subscription before CURATOR_STEP_PROGRESS events start firing (curator.controller.ts:116).
Document generation is parallel: ParallelReportGenerationService uses p-limit concurrency control to process template sections in dependency-respecting batches, each section emitting SSE events.
Tech Stack & Choices
| Layer | Technology | Notes |
|---|---|---|
| DiseaseCard storage | Prisma ddx_api_main | DiseaseCard, DiagnosticFlow, PatientDiagnosis, CurationRun, KnowledgeMutation |
| Document storage | Prisma ddx_api_ai | DocumentTemplate, DocumentTemplateVersion, GeneratedDocumentInstance, GeneratedDocumentSection |
| Curator AI | Mastra knowledgeCuratorWorkflow (in-process) | LLM-driven mutation proposal from text + PDF sources |
| Section AI | Parallel agent sub-calls via ParallelReportGenerationService | p-limit concurrency; retry logic (max 2 retries per section) |
| PDF export | DocumentExportService → PdfEngineService (Playwright, port 6450) | Falls back to PDFKit if PDF engine unavailable (document-export.service.ts:29) |
| Disease RAG | DiagnosisRagService → Qdrant | Disease corpus semantic search |
| Guidelines RAG | GuidelinesRagService → Qdrant ddx-guidelines | PDF upload → chunk → embed → Qdrant; POST /curator/rag/guidelines/ingest |
| SSE | EventBusService (Redis DB1) | CURATOR_* and DOCUMENT_* event families |
| RBAC | `@RequirePermission('diagnosis-engine-curator', 'curate | rollback')` |
| Document RBAC | DOCUMENT_MANAGE_ROLES, DOCUMENT_VIEW_ROLES, DOCUMENT_GENERATE_ROLES, DOCUMENT_REVIEW_ROLES | Defined in rbac.ts:3 |
Design choice: Curator mutations carry beforeJson snapshots. Rolling back a mutation replays the beforeJson to the entity — no migration needed, no data loss. This means the knowledge base is a fully auditable event log of AI-proposed changes.
Data Flow
Curator enriches a disease card from a guideline PDF
Business outcome: Clinical admin uploads an AWMF guideline PDF for "Hypertension". The AI Curator extracts updated treatment recommendations, proposes mutations to the Hypertension DiseaseCard, and shows a diff for review.
Technical mechanism:
POST /diagnosis-engine/curator/rag/guidelines/ingestwithfile=awmf-hypertension.pdf→GuidelinesRagService.ingestPdf()chunks + embeds the document into Qdrantddx-guidelines.POST /diagnosis-engine/curator/runswith{ scope: { kind: 'disease-card', entityId } }→CurationRunService.start()creates aCurationRunin statusPENDING.CuratorController.startRun()(curator.controller.ts:116) returns the run immediately, then firessetTimeout(100ms)→runs.executeRun(run.id).executeRun()invokesknowledgeCuratorWorkflow(Mastra in-process) which callsGuidelinesRagServicefor relevant chunks, proposes mutations, emitsCURATOR_STEP_PROGRESSSSE events.- On completion, each mutation is written as a
KnowledgeMutationrow withbeforeJson+afterJsonsnapshots and statusAPPLIED. GET /diagnosis-engine/curator/runs/:id/mutations→ clinician reviews the diff list.- If rejected:
POST /diagnosis-engine/curator/mutations/:id/rollback→KnowledgeMutationService.rollback()replaysbeforeJsonto the entity.
TUCAN generates a patient intake document
Business outcome: Doctor asks TUCAN: "Generate an intake summary for Maria Schmidt". A PDF appears in the patient's document list within 30 seconds.
Technical mechanism:
- TUCAN tool calls
DocumentGenerationOrchestratorService.createDocumentInstance()(document-generation-orchestrator.service.ts:167) withtemplateKey: 'patient-intake-summary'. - Orchestrator finds the
PUBLISHEDtemplate inddx_api_ai.documentTemplate, creates anDocumentInputContextrow with patient data payload, creates aGeneratedDocumentInstancewith NGeneratedDocumentSectionrows inPENDINGstatus. ParallelReportGenerationService.processSectionsParallel()processes sections in dependency order. For each section: emitDOCUMENT_SECTION_STARTSSE → call AI sub-agent → parse JSON response → write section content → emitDOCUMENT_SECTION_COMPLETESSE.- On all sections complete: emit
DOCUMENT_GENERATION_COMPLETESSE → instance status =COMPLETED. GET /api/v1/generated-documents/:id/export?format=pdf→DocumentExportService.generatePdfFromAssembledHtml()(document-export.service.ts:45) triesPdfEngineService(Playwright, port 6450), falls back toPDFKit.- PDF streamed as
application/pdfattachment.
Clinician saves a diagnostic flow for a disease card
PUT /diagnosis-engine/disease-cards/:id/flow with { flowGraph: { nodes: [...], edges: [...] } } → DiagnosticFlowsService.saveFlow() → FlowValidatorService.validate(dto.flowGraph) (diagnostic-flows.service.ts:56); if invalid, throws UnprocessableEntityException with an errors array. If valid, upserts DiagnosticFlow Prisma row (Prisma-only, no FHIR sync).
Implicated Code
ddx-api/src/diagnosis-engine/curator/curator.controller.ts:73—CURATOR_RESOURCE+ACTION_ROLLBACKconstants — rollback requires separate permission from curate; doctors may NEVER rollback per RBAC pitfall noteddx-api/src/diagnosis-engine/curator/curator.controller.ts:116—setTimeout(100ms)fire-and-forget — lets HTTP response + SSE subscription mount before firstCURATOR_STEP_PROGRESSevent firesddx-api/src/diagnosis-engine/curator/curator.controller.ts:244—POST /curator/rag/guidelines/ingest— PDF-only (MIME validated), routed toGuidelinesRagService.ingestPdf()ddx-api/src/diagnosis-engine/curator/services/curation-run.service.ts:10—CurationStatus,CurationTriggerKindenums from@ddx/prisma-mainddx-api/src/diagnosis-engine/curator/services/curation-run.service.ts:17—knowledgeCuratorWorkflow— Mastra AI workflow for curation logicddx-api/src/diagnosis-engine/diagnostic-flows/services/diagnostic-flows.service.ts:13—DiagnosticFlowsService— Prisma-only (no FHIR); validates flow graph before upsertddx-api/src/diagnosis-engine/diagnostic-flows/services/diagnostic-flows.service.ts:56—FlowValidatorService.validate(dto.flowGraph)— throwsUnprocessableEntityExceptionwitherrorsarray on invalid graphddx-api/src/documents/document-generation/services/document-generation-orchestrator.service.ts:139—DocumentGenerationOrchestratorService— creates document instance from template;generateDocument()deprecated in favour ofParallelReportGenerationServiceddx-api/src/documents/document-generation/services/document-generation-orchestrator.service.ts:167—createDocumentInstance()— resolves PUBLISHED template bytemplateKey, creates input context + instance + pending sectionsddx-api/src/documents/document-generation/services/parallel-report-generation.service.ts:1—ParallelReportGenerationService—p-limitconcurrency; dependency-aware batching; 2-retry per section; SSE progressddx-api/src/documents/document-generation/services/document-export.service.ts:29—@Optional() pdfEngineService— graceful degradation: Playwright → PDFKit fallbackddx-api/src/documents/document-generation/services/document-export.service.ts:45—generatePdfFromAssembledHtml()— primary PDF export path; accepts pre-assembled branded HTMLddx-api/src/documents/document-generation/document-templates.controller.ts:44—@RequireRole(...DOCUMENT_MANAGE_ROLES)— DOCTOR/CLINIC_ADMIN/SUPER_ADMIN can create/patch templatesddx-api/src/documents/document-generation/rbac.ts:3— 4 constant arrays:DOCUMENT_MANAGE_ROLES,DOCUMENT_VIEW_ROLES,DOCUMENT_GENERATE_ROLES,DOCUMENT_REVIEW_ROLES
Operational Notes
- Curator SSE dependency: The
setTimeout(100ms)delay inCuratorController.startRun()is intentional — removes the race condition whereCURATOR_STEP_PROGRESSfires before the client's SSE subscription is established. Do NOT remove this delay. - Rollback is gated separately:
ACTION_ROLLBACK(diagnosis-engine-curator:rollback) must NOT be granted to the DOCTOR role. Only CLINIC_ADMIN and SUPER_ADMIN can revert curator mutations. This is called out as an RBAC pitfall atcurator.controller.ts:72. - Guidelines RAG ingest: Only
application/pdffiles are accepted atPOST /curator/rag/guidelines/ingest. MIME type is validated server-side. Qdrant wing:ddx-guidelinescollection. - Document templates are org-scoped:
DocumentTemplate.isGlobalTemplateallows platform-wide templates visible to all orgs. Org-specific templates are filtered byorganizationId. OnlyPUBLISHEDstatus templates can be instantiated. generateDocument()is deprecated:DocumentGenerationOrchestratorService.generateDocument()always throwsBadRequestExceptionwith aMETHOD_DEPRECATEDerror code. All new code must useParallelReportGenerationService.processSectionsParallel().- PDF Engine fallback:
DocumentExportServiceinjectsPdfEngineServicewith@Optional(). Ifddx-pdf-engine(port 6450) is unavailable, export falls back toPDFKit— lower visual fidelity but always available. Monitor pdf-engine uptime when document export is a critical workflow. - Template
[tag:field_name]placeholders: HTML templates use[tag:field_name]syntax for merge fields.ParallelReportGenerationServiceresolves these after section generation. Malformed placeholders silently produce blank fields — validate template HTML before publishing. - Cross-DB boundary: DiseaseCards live in
ddx_api_main(PrismaService); generated documents live inddx_api_ai(PrismaAiService). A service touching both must inject both Prisma services — never mix or alias them.
Related Topics
- Clinical Data — Conditions — PatientDiagnosis links to DiseaseCard; FHIR Condition records created on diagnosis
- Clinical Visits — Visit agent context feeds document generation for intake summaries
- Assessments — Assessment scores (PHQ-9, GAF) feed AI diagnostic evaluation
- Medical Cards — Patient summary aggregation feeds document template input contexts