رغد
feat: complete platform — auth, deployment, hardening
344e369

تبيان الطبي — Agent System

Overview

The multi-agent pipeline in backend/services/agents/ processes each medical file through five sequential agents. Each agent reads from and writes to a shared AgentContext dataclass, making the full pipeline observable and independently testable.


Base Classes (agents/base.py)

AgentContext

Shared state object threaded through all agents. Key fields:

Field Type Set by
file_bytes bytes Coordinator
file_type "pdf" | "image" Coordinator
raw_text str OCRAgent
findings list[dict] ExtractionAgent
panel_code str ClassificationAgent
rag_context str MedicalReasoningAgent
report dict MedicalReasoningAgent
logs list[AgentLogEntry] All agents

AgentBase

Abstract base with:

  • Retry logic: up to max_retries=2 attempts with 0.3 s × 2^attempt backoff
  • _on_failure(ctx, exc): each subclass overrides to provide a safe fallback when all retries fail
  • Timing: each run() call records duration_ms in AgentLogEntry

Agents

1. OCRAgent

File: agents/ocr_agent.py

Input: ctx.file_bytes, ctx.file_type Output: ctx.raw_text

Strategy:

  • PDF: extracts text with PyMuPDF (fitz); falls back to EasyOCR page-by-page if text layer is empty
  • Image: tries Google Cloud Vision first (higher accuracy for Arabic); falls back to EasyOCR with contrast/sharpness preprocessing

Failure mode: sets raw_text = "" — downstream agents handle empty text gracefully.


2. ExtractionAgent

File: agents/extraction_agent.py

Input: ctx.raw_text Output: ctx.findings (list of {name, value, unit, range, status})

Strategy:

  1. Regex patterns matching common Arabic/English lab report formats
  2. LLM extraction via Groq if regex yields < 2 findings
  3. Physiological bounds filter (_validators.py) removes impossible values (e.g., hemoglobin = 400)
  4. Deduplication by normalized test name

Failure mode: sets findings = [].


3. ClassificationAgent

File: agents/classification_agent.py

Input: ctx.findings, ctx.raw_text Output: ctx.panel_code, ctx.panel_confidence

Strategy: Uses services/classifier.py which scores text against panel-specific keyword sets. Falls back to detect_panel() heuristic if primary classifier returns low confidence.

Panels: cbc, thyroid, liver, kidney, lipid, diabetes, urine, mixed

Failure mode: sets panel_code = "mixed" (general analysis).


4. MedicalReasoningAgent

File: agents/reasoning_agent.py

Input: ctx.findings, ctx.panel_code, ctx.analysis_type Output: ctx.rag_context, ctx.report

Strategy:

  1. Checks rag_cache (TTL 5 min) for identical query
  2. Retrieves relevant medical knowledge via Retriever (BM25 + pgvector + Cohere rerank)
  3. Selects panel-specific prompt template from prompts/
  4. Calls Groq llama-3.3-70b-versatile with findings + RAG context
  5. Parses JSON response into structured report

Failure mode: generates a fallback report from raw findings without LLM, appends disclaimer.


5. SafetyAgent

File: agents/safety_agent.py

Input: ctx.report Output: ctx.report (filtered in-place)

Strategy: Applies services/safety.filter_analysis_report() which:

  • Removes diagnostic certainty claims ("you have diabetes")
  • Adds standard medical disclaimer
  • Detects emergency patterns (very high/low critical values) and prepends urgent notice

Failure mode: appends DISCLAIMER_AR manually to ensure minimum safety even if filter itself errors.


AgentCoordinator (agents/coordinator.py)

Instantiates all five agents and runs them in sequence. Returns CoordinatorResult:

@dataclass
class CoordinatorResult:
    findings:   list[dict]
    summary:    str
    report:     dict
    panel_code: str
    logs:       list[dict]   # exposed in dev mode via _agents field
    ok:         bool
    error:      str
    total_ms:   float

The coordinator is loaded once via @lru_cache and reused across requests. Agent instances are stateless — all state lives in the per-request AgentContext.


Adding a New Agent

  1. Create agents/my_agent.py, subclass AgentBase
  2. Implement _execute(self, ctx: AgentContext) -> AgentContext
  3. Implement _on_failure(self, ctx, exc) with a safe fallback
  4. Add new fields to AgentContext if needed
  5. Register in AgentCoordinator.__init__() agent list at the correct position