Spaces:
Build error
Build error
| # HFU Multimodal Anatomy Chatbot — System Design, Evolution, and MCP Architecture | |
| **Document type:** Thesis technical design record | |
| **Scope:** Planning rationale, failed/iterative approaches, final architecture, and Model Context Protocol (MCP) integration | |
| **Repository:** `ThesisBackend` (FastAPI + Next.js + `anatomy_mcp/`) | |
| --- | |
| ## 1. Executive summary | |
| This project delivers a **multimodal anatomy learning assistant** with two deliberately separated capabilities: | |
| | Mode | User intent | Knowledge source | Output | | |
| |------|-------------|------------------|--------| | |
| | **RAG Chatbot** | Answer textbook questions from uploaded PDFs | Chroma + BM25 + optional CLIP figure index | Grounded text, ≤3 citations, optional figures | | |
| | **MCP 3D Anatomy** | Export and inspect a named body structure | Z-Anatomy `Startup.blend` + geometry-proven catalog | GLB + annotation JSON + Three.js viewer | | |
| The thesis contribution is not “one model that does everything,” but a **dual-pipeline architecture** where document grounding and 3D geometry export remain **evaluated independently**. The final 3D path implements the **official Model Context Protocol** (`stdio` transport, `ClientSession.call_tool`, FastMCP tool server) rather than ad-hoc Python function calls or prompt-only LLM hallucination. | |
| --- | |
| ## 2. Original research goals | |
| When the project started, the goals were: | |
| 1. **Grounded Q&A** — Students ask anatomy questions; answers must cite uploaded course PDFs, not unconstrained LLM knowledge. | |
| 2. **Multimodal support** — Textbook figures (diagrams, histology, radiology stills) should appear when relevant, not only text chunks. | |
| 3. **3D visualization** — When a structure is named, the system should produce inspectable 3D geometry with labels, not a static screenshot alone. | |
| 4. **Reproducible evaluation** — RAG quality (citation accuracy, abstention) and 3D export success (GLB URL, viewer load) must be measurable separately. | |
| 5. **Local-first deployment** — Run on a Windows developer machine: LM Studio, Blender 5.x, Z-Anatomy, optional MinIO. | |
| These goals drove every architectural decision below. | |
| --- | |
| ## 3. Initial approach (v0) and its flaws | |
| ### 3.1 What we built first | |
| The first iteration was a **monolithic FastAPI + single LLM** stack: | |
| ``` | |
| User question → Chroma similarity search → top-k chunks → LLM prompt → answer | |
| ``` | |
| Supporting pieces: | |
| - PDF upload → text chunking → MiniLM embeddings → Chroma persist directory | |
| - Optional local `/outputs` folder for extracted images | |
| - A separate experimental path: **prompt Blender to procedurally generate geometry** (`src/mcp/blender_generator.py`, `BlenderMCP.generate_3d_brain`) | |
| ### 3.2 Flaws discovered in practice | |
| | Flaw | Symptom | Root cause | | |
| |------|---------|------------| | |
| | **Weak retrieval** | Correct answer in PDF but wrong passage retrieved | Dense-only search; no BM25 for exact terms (e.g. “mitral valve”, “NMJ”) | | |
| | **Citation sprawl** | 10+ reference cards; duplicates from same page | No deduplication, no max-source cap, no evidence grading | | |
| | **Ungrounded synthesis** | Plausible anatomy not in corpus | LLM answered from parametric knowledge when retrieval was empty | | |
| | **Figure blindness** | Questions about labeled diagrams missed images | Text-only index; figures stored but not retrieved cross-modally | | |
| | **Fragile image URLs** | Broken images after restart or HF deploy | Local paths only; no object storage abstraction | | |
| | **Fake 3D success** | UI showed “Exported liver” with no GLB | LLM text claimed export; no tool verification | | |
| | **Wrong 3D source** | Procedural “brain” mesh unrelated to Z-Anatomy | `blender_generator.py` creates synthetic geometry, not curriculum-accurate labels | | |
| | **Name-only matching** | “kidney” export failed or picked wrong collection | Raw label index lists scene names, not exportable geometry | | |
| | **Parallel Blender corruption** | Random export failures on Windows | Multiple Blender subprocesses writing concurrently | | |
| | **Coupled evaluation** | Could not tell if RAG or 3D failed | Single chat endpoint mixed document answers and export URLs | | |
| These flaws motivated the phased redesign documented in Sections 4–7. | |
| --- | |
| ## 4. Approaches we tried (chronological evolution) | |
| ### 4.1 Overview timeline | |
| ```mermaid | |
| timeline | |
| title Architecture evolution | |
| section Retrieval | |
| v0 Dense Chroma only : Weak on exact terms | |
| v1 Hybrid BM25 + dense + RRF : Better keyword + semantic balance | |
| v2 Evidence grading + max 3 sources : Thesis-grade grounding | |
| section Multimodal | |
| v0 Text chunks only : Missed figures | |
| v1 CLIP multimodal Chroma : Figure retrieval added | |
| v2 PDF pipeline + MinIO/Supabase : Durable public image URLs | |
| section 3D visualization | |
| v0 Procedural Blender script : Not Z-Anatomy accurate | |
| v1 Remote render worker (asset PNG) : RAG sidebar preview only | |
| v2 Direct Python anatomy calls : Worked but not MCP-standard | |
| v3 True MCP stdio + catalog fast path : Final production design | |
| section LLM | |
| v0 Single local model : LM Studio only | |
| v1 Split providers : Groq for RAG speed, LM Studio as MCP tool host | |
| ``` | |
| --- | |
| ### 4.2 Approach A — Dense vector RAG only | |
| **Design:** `VectorStoreFactory` + `similarity_search(k=...)`. | |
| **Why we tried it:** Fastest path to a demo; LangChain/Chroma defaults. | |
| **Result:** Failed on anatomy terminology where embedding similarity diverges from exact nomenclature (Latin names, hyphenation, “left vs right”). | |
| **Replacement:** `HybridRetriever` in `src/retrieval/hybrid_retriever.py` — BM25 + dense + phrase matching fused with **Reciprocal Rank Fusion (RRF)**. | |
| --- | |
| ### 4.3 Approach B — Multimodal RAG (CLIP + text) | |
| **Design:** `MultimodalRAG` combines: | |
| - Text corpus (MiniLM Chroma, same as ingestion) | |
| - Multimodal retriever (`MultimodalRetriever` + `CLIPEmbedding`) | |
| - Up to 3 unique text passages (`_MAX_UNIQUE_TEXT_PASSAGES = 3`) | |
| **Why we tried it:** Anatomy learning is inherently visual; students ask about figures explicitly. | |
| **Improvements over A:** | |
| - Retrieves figure metadata and serves images from `/outputs` or presigned storage | |
| - Dedupes by PDF stem + page + text fingerprint | |
| **Remaining gap:** Still no 3D export; images are 2D textbook figures only. | |
| --- | |
| ### 4.4 Approach C — Strict grounding & thesis evaluation pipeline | |
| **Design:** `src/multimodal/thesis_rag_eval.py` — parallel experiment track: | |
| - **Baseline** — LLM without retrieval (measures hallucination rate) | |
| - **Strict RAG** — answer only from numbered passages | |
| - **Coherent synthesis** — stricter prompt with citation IDs | |
| - **Blind judge** — compares baseline vs strict | |
| **Why we tried it:** Quantify *how much* retrieval helps for thesis evaluation (`POST /rag/experiment/ask`). | |
| **Lesson:** Production `POST /rag/ask` and experiment pipeline stay separate so tuning does not break live UI. | |
| --- | |
| ### 4.5 Approach D — Procedural Blender generation (`BlenderMCP`) | |
| **Design:** `src/mcp/blender_mcp.py` + `blender_generator.py` | |
| ``` | |
| LLM prompt → blender --background --python blender_generator.py → synthetic GLB | |
| ``` | |
| **Why we tried it:** Quick 3D “something” without licensing a full anatomy asset library. | |
| **Flaws:** | |
| - Geometry is **not** Z-Anatomy; labels do not match course material | |
| - No annotation JSON sidecar | |
| - `POST /blender/generate-brain-3d` is a research stub, not curriculum export | |
| **Status:** Kept for optional brain demo; **not** the MCP catalog path. | |
| --- | |
| ### 4.6 Approach E — Remote Blender render worker (RAG adjunct) | |
| **Design:** `app/services/blender_service.py` → `render_related_anatomy()` | |
| ``` | |
| POST /rag/ask → detect keyword (brain, heart, spine…) → POST {BLENDER_SERVER_URL}/render-asset → PNG URL | |
| ``` | |
| **Why we tried it:** Attach a **related 3D preview** to RAG answers without blocking on full GLB export latency. | |
| **Characteristics:** | |
| - Fixed asset map (`ASSET_MAPPING`: brain, heart, hand, lung, spine) | |
| - Worker uploads to object storage; backend stores **no local render files** | |
| - Returns `render_3d_url` in RAG response (optional, best-effort) | |
| **Flaws:** | |
| - Only ~5 pre-authored assets; no arbitrary “left femur” from user query | |
| - Docker `blender` service in `docker-compose.yml` is a **different** worker from Z-Anatomy MCP export | |
| - Must not be confused with MCP panel exports in evaluation | |
| **Status:** Optional RAG enhancement; orthogonal to MCP. | |
| --- | |
| ### 4.7 Approach F — Label index without exportable catalog | |
| **Design:** Scan Z-Anatomy once → `z_anatomy_index.json` (all collection/object names in scene). | |
| **Why we tried it:** First automated bridge from natural language to Blender object names. | |
| **Flaws:** | |
| - Index proves **name exists**, not that mesh is exportable | |
| - Broad queries (`kidney`, `iris`) hit ambiguous or empty collections | |
| - Exports sometimes succeeded with **huge** selections (entire limb regions) | |
| **Replacement:** `build_exportable_catalog.py` → `exportable_catalog.json` — entries validated by geometry probe in Blender. | |
| --- | |
| ### 4.8 Approach G — Direct in-process Python calls (pre-MCP) | |
| **Design:** Import `anatomy_mcp/server.py` functions directly from FastAPI handlers. | |
| **Why we tried it:** Fastest integration before MCP SDK maturity on Windows. | |
| **Flaws:** | |
| - Not interoperable with LM Studio, Claude Desktop, or MCP Inspector | |
| - No standard tool discovery / schema contract | |
| - LLM could not participate in multi-step tool loops using a portable protocol | |
| - Harder to sandbox Blender side effects | |
| **Replacement:** True MCP stdio server + `MCPBridge` client (Approach H). | |
| --- | |
| ### 4.9 Approach H — True MCP with dual execution paths (FINAL) | |
| **Design:** See Section 6–7. This is the production 3D architecture. | |
| --- | |
| ### 4.10 Storage approaches tried | |
| | Approach | Mechanism | When used | Flaw / fix | | |
| |----------|-----------|-----------|------------| | |
| | Local `outputs/` | FastAPI static mount | Earliest dev | URLs break on multi-replica / HF Spaces | | |
| | MinIO | S3-compatible Docker (`docker-compose.yml`) | Local full stack | Requires Docker; good for dev | | |
| | Supabase Storage | `STORAGE_PROVIDER=supabase` | Production / HF | Needs service role key; auto-selected when env set | | |
| Image pipeline steps are observable via `GET /debug/storage` and upload response `pipeline` array. | |
| --- | |
| ### 4.11 LLM provider approaches | |
| | Role | Initial | Final | | |
| |------|---------|-------| | |
| | RAG synthesis | LM Studio local | **Groq** (`llama-3.1-8b-instant`) for latency | | |
| | MCP tool host | N/A | **LM Studio** OpenAI-compatible API at `:1234/v1` | | |
| | Evidence grading / judge | Groq | Groq (thesis eval) | | |
| **Rationale for split:** RAG needs fast repeated calls; MCP tool loop needs a model with reliable **function calling** locally without cloud cost for Blender waits. | |
| --- | |
| ## 5. Final architecture (what we reached) | |
| ### 5.1 System context | |
| ```mermaid | |
| flowchart TB | |
| subgraph User["User / Evaluator"] | |
| U1[Browser] | |
| end | |
| subgraph Frontend["Next.js Frontend :3000"] | |
| Chat[Main Chat UI] | |
| Upload[Upload Panel] | |
| MCPPanel[MCP 3D Panel + iframe viewer] | |
| end | |
| subgraph Backend["FastAPI Backend :8000"] | |
| RAGRoute["/rag/ask"] | |
| UploadRoute["/upload-pdf/"] | |
| MCPRoute["/anatomy/ask"] | |
| RAGSvc[rag_service] | |
| RAGEngine[MultimodalRAG] | |
| MCPChat[anatomy_mcp_chat] | |
| MCPBridge[MCPBridge stdio client] | |
| end | |
| subgraph RAGData["Document knowledge"] | |
| Chroma[(Chroma text index)] | |
| BM25[(BM25 pickle)] | |
| MMChroma[(Multimodal Chroma)] | |
| MinIO[(MinIO / Supabase / local outputs)] | |
| end | |
| subgraph MCPData["3D anatomy knowledge"] | |
| Catalog[(exportable_catalog.json)] | |
| Blend[Z-Anatomy Startup.blend] | |
| Exports[/anatomy-exports GLB + JSON/] | |
| Viewer[/anatomy-viewer Three.js/] | |
| end | |
| subgraph External["External runtimes"] | |
| Groq[Groq API — RAG LLM] | |
| LMStudio[LM Studio — MCP host LLM] | |
| Blender[Blender 5.x headless] | |
| end | |
| subgraph MCPServer["anatomy_mcp/server.py — stdio MCP"] | |
| SearchTool[search_anatomy_catalog] | |
| ExportPart[export_anatomy_part] | |
| ExportPkg[export_anatomy_package] | |
| end | |
| U1 --> Chat & Upload & MCPPanel | |
| Chat -->|POST question| RAGRoute | |
| Upload -->|POST PDF| UploadRoute | |
| MCPPanel -->|POST message| MCPRoute | |
| RAGRoute --> RAGSvc --> RAGEngine | |
| RAGEngine --> Chroma & BM25 & MMChroma | |
| RAGEngine --> Groq | |
| UploadRoute --> Chroma & MMChroma & MinIO | |
| RAGEngine --> MinIO | |
| RAGSvc -->|optional| RenderWorker[Remote render worker] | |
| MCPRoute --> MCPChat --> MCPBridge | |
| MCPChat -->|fast path or tool loop| LMStudio | |
| MCPBridge <-->|stdio JSON-RPC| MCPServer | |
| SearchTool --> Catalog | |
| ExportPart & ExportPkg --> Blender | |
| Blender --> Blend | |
| ExportPart & ExportPkg --> Exports | |
| MCPPanel -->|iframe src=viewer_url| Viewer | |
| Viewer --> Exports | |
| ``` | |
| ### 5.2 Separation of concerns (evaluation rule) | |
| ```mermaid | |
| flowchart LR | |
| subgraph RAGPath["Mode A — RAG Chat"] | |
| Q1[Textbook question] | |
| A1[Grounded answer + PDF citations] | |
| end | |
| subgraph MCPPath["Mode B — MCP 3D"] | |
| Q2[Structure name e.g. left femur] | |
| A2[GLB + annotations + viewer] | |
| end | |
| Q1 -.->|must NOT| A2 | |
| Q2 -.->|must NOT| A1 | |
| style RAGPath fill:#e8f4fc,stroke:#2563eb | |
| style MCPPath fill:#ecfdf5,stroke:#059669 | |
| ``` | |
| **Thesis rule:** Never score a RAG answer on GLB export success; never score an MCP export on PDF citation quality. | |
| --- | |
| ## 6. MCP deep dive — protocol, components, and guarantees | |
| ### 6.1 What “proper MCP” means in this project | |
| The implementation satisfies the MCP contract at three layers: | |
| | Layer | Component | Responsibility | | |
| |-------|-----------|----------------| | |
| | **Tool server** | `anatomy_mcp/server.py` | FastMCP registers tools; runs Blender; returns structured JSON | | |
| | **Transport** | `stdio` via `mcp.client.stdio` | Parent process spawns server; no HTTP port on MCP server | | |
| | **Host orchestration** | `app/services/anatomy_mcp_chat.py` | LM Studio chooses tools; backend executes via `ClientSession.call_tool` | | |
| **Non-MCP patterns explicitly rejected:** | |
| - LLM printing `http://.../anatomy.glb` without calling a tool | |
| - Direct Python import of export functions from FastAPI routes | |
| - Hard-coded export in frontend | |
| ### 6.2 MCP layer diagram | |
| ```mermaid | |
| flowchart TB | |
| subgraph Presentation["Presentation layer"] | |
| UI[AnatomyMcpPanel.js] | |
| Viewer[Three.js viewer iframe] | |
| end | |
| subgraph API["FastAPI REST layer"] | |
| Ask["POST /anatomy/ask"] | |
| Health["GET /anatomy/health"] | |
| Static["/anatomy-exports /anatomy-viewer"] | |
| end | |
| subgraph Orchestration["MCP host orchestration"] | |
| Agent[run_lmstudio_mcp_agent] | |
| FastPath[_try_catalog_fast_path] | |
| ToolLoop[LM Studio tool loop max 6 rounds] | |
| end | |
| subgraph Client["MCP client — MCPBridge"] | |
| Session[ClientSession] | |
| ListTools[list_tools → OpenAI schema] | |
| CallTool[call_tool] | |
| StdioTransport[stdio_client] | |
| end | |
| subgraph Server["MCP server — FastMCP"] | |
| T1[search_anatomy_catalog] | |
| T2[export_anatomy_part] | |
| T3[export_anatomy_package] | |
| Lock[_BLENDER_EXPORT_LOCK] | |
| Resolver[exportable_catalog resolver] | |
| end | |
| subgraph Execution["Execution layer"] | |
| BlenderCLI[Blender subprocess] | |
| ZAnatomy[Startup.blend] | |
| Cache[exports/cache v4 schema] | |
| end | |
| UI --> Ask | |
| Ask --> Agent | |
| Agent --> FastPath | |
| Agent --> ToolLoop | |
| FastPath --> CallTool | |
| ToolLoop --> ListTools | |
| ToolLoop --> CallTool | |
| CallTool --> StdioTransport --> Session | |
| Session --> T1 & T2 & T3 | |
| T2 & T3 --> Lock --> BlenderCLI --> ZAnatomy | |
| T2 & T3 --> Cache | |
| Static --> Viewer | |
| UI --> Viewer | |
| ``` | |
| ### 6.3 MCP tool catalog | |
| | Tool | Input | Output | When used | | |
| |------|-------|--------|-----------| | |
| | `search_anatomy_catalog` | `query`, `limit` | Ranked catalog entries, suggestions, ambiguity errors | Every export path starts here or equivalent resolution | | |
| | `export_anatomy_part` | `part_query`, `include_preview` | `model_url`, `annotations_url`, `viewer_url`, metadata | Single structure (e.g. `Femur.l`, `Liver`) | | |
| | `export_anatomy_package` | `part_query`, options | Study package with subparts for large regions | Brain, thalamus; fallback after part export timeout | | |
| **Safety boundaries** (from `anatomy_mcp/README.md`): | |
| - No arbitrary `.blend` paths from user input | |
| - No arbitrary Python injection | |
| - No full-scene export by default | |
| - Serialized Blender lock — one export at a time on Windows | |
| ### 6.4 Catalog resolution pipeline | |
| ```mermaid | |
| flowchart TD | |
| Input[User message e.g. left femur] | |
| Validate{Query valid?} | |
| Vague{Vague query?} | |
| Normalize[catalog_query_from_user_message] | |
| Search[search_anatomy_catalog] | |
| Match{Exact exportable match?} | |
| Ambiguous{Multiple matches?} | |
| Export[export_anatomy_part] | |
| Package[export_anatomy_package] | |
| Success[anatomy_export status ok] | |
| Clarify[Clarification message] | |
| Suggest[Return suggestion labels] | |
| Input --> Validate | |
| Validate -->|invalid chars / too long| Clarify | |
| Validate --> Vague | |
| Vague -->|show me the organ| Clarify | |
| Vague --> Normalize --> Search | |
| Search --> Match | |
| Match -->|yes Femur.l| Export | |
| Match -->|no| Ambiguous | |
| Ambiguous -->|kidney| Suggest | |
| Export -->|timeout| Package | |
| Export --> Success | |
| Package --> Success | |
| ``` | |
| **Key data artifact:** `exportable_catalog.json` — only entries with proven mesh geometry. This replaced naive `z_anatomy_index.json` matching. | |
| ### 6.5 MCP sequence — fast path (plain queries) | |
| Used when input matches `looks_like_plain_anatomy_query` (≤80 chars, alphanumeric + space/dot/dash). | |
| ```mermaid | |
| sequenceDiagram | |
| autonumber | |
| actor User | |
| participant UI as AnatomyMcpPanel | |
| participant API as POST /anatomy/ask | |
| participant Chat as anatomy_mcp_chat | |
| participant Bridge as MCPBridge | |
| participant Srv as anatomy_mcp/server.py | |
| participant Cat as exportable_catalog.json | |
| participant B as Blender | |
| participant V as anatomy-viewer iframe | |
| User->>UI: "left femur" | |
| UI->>API: { message, language } | |
| API->>Chat: run_lmstudio_mcp_agent() | |
| Note over Chat: Fast path — no LM Studio round-trip | |
| Chat->>Bridge: call_tool search_anatomy_catalog | |
| Bridge->>Srv: MCP CallTool | |
| Srv->>Cat: token + lateral resolution | |
| Srv-->>Chat: Femur.l match | |
| Chat->>Bridge: call_tool export_anatomy_part | |
| Bridge->>Srv: MCP CallTool | |
| Srv->>B: subprocess export_part.py | |
| B-->>Srv: anatomy.glb + annotations.json | |
| Srv-->>Chat: model_url, viewer_url | |
| Chat->>Chat: structured_to_anatomy_export | |
| Note over Chat: Fail if model_url missing — no fake success | |
| Chat-->>API: answer + anatomy_export + mcp_tools_used | |
| API-->>UI: JSON 200 | |
| UI->>V: iframe src = viewer_url | |
| V-->>User: labeled 3D mesh | |
| ``` | |
| **Why fast path exists:** Blender exports take 30–120s. Skipping LLM orchestration for `"Femur.l"`-style queries reduces failure modes and token cost. | |
| ### 6.6 MCP sequence — LLM tool loop (complex queries) | |
| ```mermaid | |
| sequenceDiagram | |
| autonumber | |
| participant Chat as anatomy_mcp_chat | |
| participant LM as LM Studio LLM | |
| participant Bridge as MCPBridge | |
| participant Srv as MCP server | |
| Chat->>Bridge: get_openai_tools() | |
| Bridge->>Srv: list_tools | |
| Srv-->>Bridge: tool schemas | |
| Bridge-->>Chat: OpenAI function definitions | |
| loop Up to MAX_TOOL_ROUNDS = 6 | |
| Chat->>LM: messages + tools + tool_choice auto | |
| LM-->>Chat: assistant message + tool_calls | |
| alt tool_calls present | |
| Chat->>Bridge: call_tool(name, args) | |
| Bridge->>Srv: MCP CallTool | |
| Srv-->>Bridge: structured JSON result | |
| Bridge-->>Chat: compact_tool_result_for_llm | |
| Chat->>Chat: append tool result to messages | |
| else no tool_calls | |
| Chat->>Chat: verify export or require tools | |
| end | |
| end | |
| Chat->>Chat: structured_to_anatomy_export | |
| alt model_url present | |
| Chat-->>Chat: status ok + mcp_tools_used logged | |
| else export error | |
| Chat-->>Chat: status error — UI shows failure, no empty iframe | |
| end | |
| ``` | |
| **Host prompt constraints** (`build_mcp_system_prompt` in `app/i18n/locale.py`): | |
| - Must call `export_anatomy_part` or `export_anatomy_package` before claiming success | |
| - Must not echo raw package manifest URLs to user | |
| - Must ask clarification on vague queries | |
| - Retry package export after part export timeout | |
| ### 6.7 How the final approach **ensures** MCP integrity | |
| | Guarantee | Mechanism | Code reference | | |
| |-----------|-----------|----------------| | |
| | **Real tool discovery** | `session.list_tools()` → OpenAI schemas | `MCPBridge.get_openai_tools()` | | |
| | **Real tool execution** | `session.call_tool(name, arguments)` | `MCPBridge.call_tool()` | | |
| | **Stdio isolation** | Separate Python process for `server.py` | `StdioServerParameters` | | |
| | **Structured tool results** | `structuredContent` + text fallback parsing | `extract_structured_from_tool_payload()` | | |
| | **No hallucinated exports** | `structured_to_anatomy_export` returns `null` without `model_url` | `anatomy_mcp_client.py` | | |
| | **UI honesty** | Frontend checks `anatomy_export.status === "ok"` before iframe | `AnatomyMcpPanel.js` | | |
| | **Audit trail** | Response includes `mcp_tools_used`, `mcp_tool_steps` | `_agent_result()` | | |
| | **URL rewriting** | Dev server `:8123` → public API base | `rewrite_local_urls()` | | |
| | **Concurrency safety** | `threading.RLock` around Blender | `anatomy_mcp/server.py` | | |
| | **Health probe** | `GET /anatomy/health` checks Blender, catalog, MCP import | `anatomy_mcp_health()` | | |
| ```mermaid | |
| flowchart TD | |
| Claim[LLM says Exported liver] | |
| Tools{mcp_tools_used includes export tool?} | |
| URL{model_url HTTP 200?} | |
| OK[Show viewer + GLB links] | |
| Fail[Show error panel] | |
| Claim --> Tools | |
| Tools -->|no| Fail | |
| Tools -->|yes| URL | |
| URL -->|no| Fail | |
| URL -->|yes| OK | |
| ``` | |
| --- | |
| ## 7. Final approach — what we implemented in the last iteration | |
| ### 7.1 Backend wiring (`app/main.py`) | |
| Routers and static mounts required for MCP UI: | |
| - `app.include_router(anatomy_mcp.router)` → `/anatomy/*` | |
| - `app.mount("/anatomy-exports", ...)` → GLB, packages, annotations | |
| - `app.mount("/anatomy-viewer", ...)` → Three.js viewer assets | |
| ### 7.2 Settings contract (`src/config/settings.py`) | |
| Environment-driven configuration for MCP: | |
| - `PUBLIC_API_BASE` — URL prefix for exported assets | |
| - `BLENDER_BIN`, `Z_ANATOMY_BLEND` — passed into MCP server env | |
| - `LLM_API_BASE` — LM Studio for tool host | |
| - `ANATOMY_MCP_ENABLED` — feature gate | |
| ### 7.3 Frontend MCP panel | |
| `frontend/app/components/AnatomyMcpPanel.js`: | |
| - Calls `POST /anatomy/ask` (not `/rag/ask`) | |
| - Renders `AnatomyExportPanel` with viewer link, GLB download | |
| - Salvage path parses URLs from verbose LLM dumps (legacy compatibility) | |
| - i18n EN/DE strings for errors including “restart backend if 404” | |
| ### 7.4 Caching & performance | |
| - Export cache schema **v4** under `anatomy_mcp/exports/cache/` | |
| - Repeated `Femur.l` queries hit cache → near-instant URLs | |
| - Package exports for large structures avoid timeout on single-part mesh | |
| ### 7.5 RAG path (unchanged responsibility) | |
| `POST /rag/ask` still uses `MultimodalRAG` + optional `render_3d_url` from remote worker — **does not** invoke MCP tools by default. This preserves evaluation separation. | |
| --- | |
| ## 8. RAG pipeline reference (Mode A detail) | |
| ```mermaid | |
| sequenceDiagram | |
| autonumber | |
| actor User | |
| participant UI as Next.js Chat | |
| participant API as POST /rag/ask | |
| participant RAG as MultimodalRAG | |
| participant IDX as Chroma + BM25 + Hybrid | |
| participant LLM as Groq | |
| User->>UI: Question + optional PDF context | |
| UI->>API: { question, allow_world_knowledge, language } | |
| API->>RAG: ask() | |
| RAG->>RAG: classify + typo correct + rewrite | |
| RAG->>IDX: hybrid retrieve | |
| RAG->>RAG: rerank + dedupe passages | |
| RAG->>LLM: evidence grading A/B/C | |
| RAG->>LLM: grounded synthesis | |
| RAG->>RAG: normalize citations, max 3 sources | |
| RAG-->>API: answer, sources, images, grounding | |
| API-->>UI: AskResponse | |
| ``` | |
| --- | |
| ## 9. Comparison matrix (for thesis discussion section) | |
| | Dimension | Initial v0 | Final system | | |
| |-----------|------------|--------------| | |
| | Retrieval | Dense only | Hybrid BM25 + dense + RRF + phrases | | |
| | Grounding | Prompt-only | Evidence grading + abstention + max 3 sources | | |
| | Figures | Ignored | CLIP multimodal index + storage URLs | | |
| | 3D source | Procedural script | Z-Anatomy catalog + Blender export | | |
| | 3D protocol | Direct Python / fake LLM text | MCP stdio + tool audit trail | | |
| | LLM | Single local | Groq (RAG) + LM Studio (MCP host) | | |
| | Storage | Local folder | MinIO / Supabase abstraction | | |
| | Evaluation | Mixed | Separated RAG vs MCP metrics | | |
| | Export correctness | Name in index | Geometry-proven catalog | | |
| | Parallelism | Unsafe | Blender export lock | | |
| --- | |
| ## 10. Known limitations & future work | |
| 1. **LM Studio dependency for complex MCP queries** — Fast path avoids it; natural-language disambiguation still needs tool-capable local model. | |
| 2. **Windows-only Blender path** — Linux/Mac require different Blender binary paths. | |
| 3. **Export latency** — Large packages (brain) may exceed user patience; cache mitigates repeat queries. | |
| 4. **RAG render worker** — Only five asset keys; not a general anatomy renderer. | |
| 5. **MCP + RAG fusion** — Deliberately not merged; future work could *link* citation text to MCP viewer via shared entity IDs without mixing retrieval corpora. | |
| --- | |
| ## 11. Verification checklist | |
| ### RAG (Mode A) | |
| ```powershell | |
| curl -X POST http://127.0.0.1:8000/rag/ask ` | |
| -H "Content-Type: application/json" ` | |
| -d '{"question":"What is the function of the mitral valve?","allow_world_knowledge":false,"language":"en"}' | |
| ``` | |
| Pass: answer cites PDF sources; no GLB URLs. | |
| ### MCP (Mode B) | |
| ```powershell | |
| curl -X POST http://127.0.0.1:8000/anatomy/ask ` | |
| -H "Content-Type: application/json" ` | |
| -d '{"message":"left femur","language":"en"}' | |
| ``` | |
| Pass: `anatomy_export.status == "ok"`, `mcp_tools_used` includes `export_anatomy_part`, viewer URL loads. | |
| ### MCP health | |
| ```powershell | |
| Invoke-RestMethod http://127.0.0.1:8000/anatomy/health | |
| ``` | |
| Pass: `ready: true`, `mcp_stdio_ok: true`, `blender_exists: true`, `exportable_catalog_exists: true`. | |
| --- | |
| ## 12. Key file index | |
| | Path | Role | | |
| |------|------| | |
| | `app/main.py` | FastAPI entry, routers, static mounts | | |
| | `app/services/rag_service.py` | RAG + optional render worker | | |
| | `app/services/anatomy_mcp_chat.py` | MCP host agent (fast path + tool loop) | | |
| | `app/services/anatomy_mcp_client.py` | MCPBridge stdio client | | |
| | `app/services/anatomy_mcp_service.py` | Health checks, env configuration | | |
| | `anatomy_mcp/server.py` | FastMCP tool server | | |
| | `anatomy_mcp/label_index/exportable_catalog.json` | Geometry-validated catalog | | |
| | `anatomy_mcp/viewer/` | Three.js annotation viewer | | |
| | `src/multimodal/multimodal_rag_chain.py` | MultimodalRAG core | | |
| | `src/retrieval/hybrid_retriever.py` | BM25 + dense hybrid | | |
| | `frontend/app/components/AnatomyMcpPanel.js` | MCP UI | | |
| | `docs/ARCHITECTURE_FOR_DIAGRAMS.md` | Diagram source pack | | |
| --- | |
| ## 13. Conclusion | |
| The project evolved from a **single-path RAG demo** into a **dual-mode architecture** grounded in evaluation needs: | |
| - **Documents** are handled by retrieval-augmented generation with strict citation limits. | |
| - **Geometry** is handled by a **standards-based MCP toolchain** connecting an LLM host, a stdio tool server, Blender, and a web viewer. | |
| The final MCP design is not “MCP-themed naming” — it implements discoverable tools, stdio transport, structured tool results, and verifiable export URLs. The **fast path** optimizes common classroom queries without bypassing MCP (`call_tool` is still used); the **LLM tool loop** handles ambiguous natural language while respecting the same server contract. | |
| This document should be cited alongside `docs/ARCHITECTURE_FOR_DIAGRAMS.md` when generating thesis figures: use **blue** styling for RAG paths and **green** styling for MCP paths in all diagrams. | |
| --- | |
| *Generated from repository state and architecture records. Update when new approaches are tried or MCP tools change.* | |