# تبيان الطبي — 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`: ```python @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