06-ai-tucanwave: W3filled9 citations

AI Medical Tools — Clinical Tool Catalog

Audiences: doctor, developer, investor

AI Medical Tools — Clinical Tool Catalog

TUCAN's reach into the EMR is a catalog of typed Mastra tools — ~350 createTucanTool definitions across ~35 categories — each one a thin wrapper around a NestJS service call that mutates Prisma or FHIR or publishes a UI side effect over SSE.

Business Purpose

The tool catalog is what TUCAN can actually do. A "clinical assistant" is only as useful as the actions it can take in the real system; Dudoxx's edge is that every clinical, scheduling, billing, and document action the doctor can do by clicking is also available to TUCAN by call.

Because every tool is the same code path as the UI (a NestJS service call through the Trusted Gateway), every safeguard the UI has — RBAC, tenant isolation, audit log, FHIR partition scoping — applies automatically to the LLM. The LLM is not a privileged actor; it is a typed remote control of the existing controllers.

Tools also serve a second business purpose: they are the surface through which TUCAN renders UI. Modals, notifications, info-containers, patient drawer focus, document previews, navigation — all happen by the LLM calling a category: 'ui' tool that publishes a ui:action SSE event instead of mutating data.

Audiences

  • Investor: The catalog is the moat made concrete. ~350 typed surfaces — write a prescription, book an appointment, draft a referral letter, open a modal — each one a one-week-to-add unit of product scope. Competitors must redo the integration work clinic by clinic; Dudoxx amortizes it across tenants.
  • Clinical buyer (doctor / nurse / receptionist): Whatever you do in the UI, you can ask for in plain language. Order labs, prescribe, schedule, draft letters, review interactions, summarize a chart, search ICD-10 — the same RBAC rules apply, the same audit trail is written, the same patient is scoped.
  • Developer / partner: Adding a tool is createTucanTool({ id, description, inputSchema, metadata, execute }) plus a registry entry. The Mastra DI surface (createDIContainer from @mastra/core) gives the execute function direct access to every injected NestJS service via the context.services object — no parallel API stack.
  • Internal (ops / support): Every tool call writes a ChatMessage{ role:'tool' } row and a TucanUsageRecord row. "Did the AI do X?" becomes a SQL query on chat_messages.tool_calls.

Architecture

The catalog has three layers:

diagram
┌──────────────────────────────────────────────────────────────┐
│  Tool definition                                              │
│    createTucanTool({                                          │
│      id: 'create-prescription',                               │
│      description: '...',                                      │
│      inputSchema: z.object({ ... }),  ← Zod v4               │
│      metadata: {                                              │
│        category: 'medications',                               │
│        ragDescription: '...',                                 │
│        ragKeywords: [...],                                    │
│        mutating: true,                                        │
│        alwaysInclude: false,                                  │
│      },                                                       │
│      outputInstructions: 'NEVER show UUIDs...',              │
│      execute: async (input, ctx) => { /* call service */ },   │
│    })                                                         │
└──────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌──────────────────────────────────────────────────────────────┐
│  Tool registry  (ddx-api/src/ai/tucan/tools/registry/*)       │
│   · workflow-tool-capabilities — what each tool needs        │
│   · workflow-tool-map — which workflows can use it            │
│   · agent-type catalog → category allow-list                  │
└──────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌──────────────────────────────────────────────────────────────┐
│  Per-agent tool selection                                     │
│   AgentRegistryService binds agent → tools at construction   │
│   (static category filter, not per-turn semantic reduction)   │
│   tool-converter.ts adapts createTucanTool → Mastra Tool      │
└──────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌──────────────────────────────────────────────────────────────┐
│  Execution at LLM call time                                  │
│   Mastra Agent.generate() dispatches the LLM's chosen tool   │
│   execute() calls context.services.* (NestJS DI)              │
│   side effects:                                              │
│     · Prisma mutation                                        │
│     · FHIR write via FHIR_CLIENT / ResourceFacade            │
│     · SSE ui:action / tool:start|progress|complete           │
└──────────────────────────────────────────────────────────────┘

Tool categories (~35 directories under tools/)

CategoryPurpose
core/create-tool.ts factory + Mastra primitives
_shared/, common/shared schemas + helpers
patients/patient CRUD + lookup
visit/visit lifecycle, active visit context
clinical/, clinical-notes/progress notes, examinations
medications/, prescriptions/order-entry, drug interaction checks
allergies/, conditions/, immunizations/, vitals/structured clinical data
diagnosis/, diagnostics/diagnostic reports, lab orders
service-requests/FHIR ServiceRequest
assessment/intake + assessment forms
intake/patient-intake workflow tools
scheduling/slots, appointments, recurring
fhir/12 read-only FHIR R4 tools
document/doxing, templates, generation, OCR-bound
communication/, messenger/email / SMS / WhatsApp / Telegram
tasks/task assignment + status
insurance/insurance coverage lookup
rag/, deep-search/, knowledge/semantic search + Qdrant
flowagent/scenario step orchestration (voice)
voice/reply-to-operator (voice-only)
utility/get-current-time, search-icd10, calculate-bmi
session/link-patient, get-session-context, rename-session
ui/UI-events tools — see next section

UI-events tools — the publisher-side contract

UI tools are the way TUCAN drives the frontend without leaking display concerns into clinical mutations. Every tool under tools/ui/ has category: 'ui', mutating: false, calls no FHIR / Prisma, and publishes one or more ui:action SSE events via context.services.eventBus.publishUIAction(...).

Implemented today (verified 2026-05-18):

Tool IDAction typePurpose
show-modalshow_modal9 modal variants — alert, confirmation, info, html_content, patient_info, fhir_resource, medication_list, icd10_result, drug_interaction
show-notificationshow_notificationtoast / banner, 1-30s, no DB
show-document-viewershow_document_viewerdocument preview, no DB
update-info-containerupdate_info_container11 sidebar variants, DB-upsert (the only mutating UI tool, conceptually)
clear-info-containersDB onlyclears the sidebar table — does NOT publish SSE (the DB change is silent to the frontend; a regression to file)
navigate-tonavigate10 page types, no DB
play-audio-feedbackplay_audio_feedbackvoice channel chime
focus-patient-draweropen-drawerpatient context drawer
open-patient-card-modalopen-modal (open_patient_card)rich patient overview
open-clinical-summary-modalopen-modal (open_clinical_summary)recent visits / vitals / meds rollup
open-document-previewopen-previewone-shot doc preview
open-table-viewopen-modal (open_table)data-grid for lists

Hallucinated / not yet implemented (the LLM may try them; the frontend dispatcher logs handler:dropped):

  • open_panel / close_panel — no backend tool nor frontend case
  • render_component — no frontend switch case for arbitrary components yet
  • get_clinical_notes — wrong name; real tool is list-clinical-notes

UI tools that emit a modal must include a modalId: randomUUID() in the published payload — the frontend useTUCANUIActions hook dedupes by modalId and silently drops actions without one (tools/ui/show-modal.tool.ts:117).

The full SSE wire format for ui:action is owned by [[sse-event-engine]] (W1). On the consumer side, the React hooks and AG-UI bridge live in ddx-web (see W5 frontend page once filled; for now the contract anchor is ddx-web/src/lib/sse/global-manager.ts and the useTucanSSE / useTUCANUIActions hooks documented in the ddx-ai-ui-events-frontend skill).

Tech Stack & Choices

ChoiceRationale
createTucanTool({...}) typed factorySingle point to enforce metadata (category, ragDescription, ragKeywords, mutating, alwaysInclude) — review checklist (~/.claude/rules/shared/review-checklists/mastra.md) bans tools without ragDescription.
Zod v4 (zod/v4) input schemasBackend rule: import { z } from 'zod/v4'. Bare 'zod' is reserved for ddx-web (Next.js).
LLM-perspective ragDescription"USE WHEN / DO NOT USE WHEN / REQUIRES / PRODUCES" four-line shape — see show-modal.tool.ts:79-84. Drives tool-RAG retrieval ([[tucan-rag-tools]]).
Multilingual ragKeywords (EN + DE minimum)Dudoxx is German-first; the tool-RAG embedding space must match the operator's language.
`mutating: truefalse` flag
`alwaysInclude: truefalse`
outputInstructions field with "NEVER show UUIDs"Mandatory when the output contains IDs; mastra checklist rule.
Per-agent static category filterThe agent gets its tool list at new Agent({ tools: filterByCategory([...]) }) time. No per-turn semantic reduction — see [[tucan-rag-tools]] for the dormant SkillSearchProcessor pitfall.
Hyphenated tool IDsMastra auto-converts 'show-modal' to show_modal for the LLM. Non-hyphenated IDs collide with this conversion.
Frontend deduping by modalId / containerIdSSE has at-least-once semantics; the consumer must dedupe. The publisher must provide a stable ID.

Data Flow

Business outcome: A doctor types "Check drug interactions for the current med list before I add metformin." TUCAN reads the patient's active medications from FHIR, runs the drug-interaction tool, and opens a drug-interaction modal with severity colors — no data mutation, just a UI side effect.

Technical mechanism:

  1. Dispatcher resolves agent 'clinical', which has the medications, clinical, fhir, ui categories bound at construction (ddx-api/src/ai/tucan/registry/agent-type.registry.ts).
  2. LLM emits a tool call: list-active-medications (category medications).
  3. The tool's execute(input, ctx) calls ctx.services.medicationsService.listActive(...) — the real NestJS service, with the doctor's tenant header forwarded.
  4. SSE tool:starttool:complete events stream (ddx-api/src/ai/tucan/sse/event-bus.service.ts).
  5. LLM emits a second tool call: check-drug-interactions(input).
  6. The tool's execute calls a downstream interaction service + returns structured { pairs: [...] }.
  7. LLM emits a third tool call: show-modal({ variant:'drug_interaction', title:'Interactions', data: pairs }).
  8. show-modal.execute (tools/ui/show-modal.tool.ts:113) generates a modalId, calls ctx.services.eventBus.publishUIAction(sessionId, 'show_modal', 'modal', { modalId, variant, title, ... }).
  9. ddx-web useTUCANUIActions hook receives the SSE, dedupes on modalId, renders the modal.

Implicated Code

  • ddx-api/src/ai/tucan/tools/core/create-tool.tscreateTucanTool factory; the typed seam every tool flows through.
  • ddx-api/src/ai/tucan/tools/ui/show-modal.tool.ts:11 — canonical UI tool implementation; 9-variant modal.
  • ddx-api/src/ai/tucan/tools/ui/show-modal.tool.ts:77metadata.category: 'ui' discriminator.
  • ddx-api/src/ai/tucan/tools/ui/show-modal.tool.ts:79-84ragDescription LLM-perspective shape (USE WHEN / DO NOT USE WHEN / REQUIRES / PRODUCES).
  • ddx-api/src/ai/tucan/tools/ui/show-modal.tool.ts:104mutating: false (UI tool — no DB mutation).
  • ddx-api/src/ai/tucan/tools/ui/show-modal.tool.ts:113execute publishes ui:action over SSE.
  • ddx-api/src/ai/tucan/tools/ui/show-modal.tool.ts:117 — mandatory modalId: randomUUID() for frontend dedupe.
  • ddx-api/src/ai/tucan/tools/ui/show-notification.tool.ts — toast/banner.
  • ddx-api/src/ai/tucan/tools/ui/update-info-container.tool.ts — the one UI tool that also writes a Prisma row (info-container table).
  • ddx-api/src/ai/tucan/tools/ui/clear-info-containers.tool.ts — DB-only; does NOT publish SSE (regression to file).
  • ddx-api/src/ai/tucan/tools/ui/navigate-to.tool.ts — 10 page types.
  • ddx-api/src/ai/tucan/tools/ui/index.ts — barrel export of all 13 UI tools.
  • ddx-api/src/ai/tucan/tools/registry/workflow-tool-capabilities.ts — capabilities per tool (which workflows can use it).
  • ddx-api/src/ai/tucan/tools/registry/workflow-tool-map.ts — workflow → allowed-tools map.
  • ddx-api/src/ai/tucan/registry/tool-converter.ts — adapter from createTucanTool to Mastra Tool.
  • ddx-api/src/ai/tucan/registry/tool-selector.ts — per-agent tool filtering by category.
  • ddx-api/src/ai/tucan/registry/agent-type.registry.ts — agent → category allow-list.
  • ddx-api/src/ai/tucan/registry/tool-choice-guard.ts — guard that prevents the LLM from picking a tool outside the bound catalog.

Operational Notes

  • The catalog count drifts. As of 2026-05-18, rg -c "createTucanTool" ddx-api/src/ai/tucan/tools reports 354 calls across 168 files. Project memory previously said "130 tools" (memory shard MEMORY_TUCAN_AI 2026-04-24) and "233 tools" (dispatch brief). The single source of truth is the registry walk at runtime — do not quote a static number in marketing copy without re-counting.
  • Mastra hyphen↔underscore rule. Tool id: 'show-modal' is exposed to the LLM as show_modal. Mixed forms cause silent dispatch failures. The frontend action type uses the underscore form (e.g. show_modal, show_notification, update_info_container).
  • get_clinical_notes is a hallucination. Real tool: list-clinical-notes.
  • open_panel / close_panel / render_component are unimplemented — the LLM may emit them, the frontend dispatcher logs handler:dropped. Either implement them, or strip them from any agent prompts that reference them.
  • clear-info-containers does NOT publish SSE — the DB change is invisible to the frontend until the next page mount. Track as a known defect; do not write a follow-up tool that depends on the frontend learning the table was cleared.
  • Mastra review checklist applies to every new tool: ragDescription, ragKeywords (EN + DE), mutating flag, outputInstructions ("NEVER show UUIDs" when output contains IDs), error result includes suggestion field. See ~/.claude/rules/shared/review-checklists/mastra.md.
  • Composite tools cannot call other tools. Mastra tools cannot invoke each other from execute() without a registry shim — leave composite logic in the NestJS service path ([[ddx-mastra-specialist_memory.md]]).
  • ragKeywords must distinguish siblings. Identical ragDescription + ragKeywords produces identical embeddings; retrieval picks the wrong tool. Add anti-synonyms ("NOT encounter", "NOT fhir-encounter") and a canonical TOOL_ID suffix when two tools serve adjacent intents — pitfall confirmed in commit f6eef8a18.
  • [[tucan-assistant]] — orchestration entry point that dispatches tools.
  • [[tucan-dispatcher]] — the per-stage state machine that calls DispatcherStage which finally invokes Mastra Agent.generate.
  • [[tucan-rag-tools]] — Qdrant-backed tool discovery (build-time only, not per-turn).
  • [[tucan-intent-filtering]] — intent-classifier that narrows the agent before per-agent category filtering.
  • [[agent-templates]] — agent template factory consumed by agent-type.registry.
  • [[tucan-context-builder]] — context.services injection (the DI bridge from NestJS into Mastra tool execute).
  • [[sse-event-engine]] (W1) — ui:action envelope owner.
  • docs/claude-context/CLAUDE_TUCAN_API_INJECTION.md — tool registry + RAG filtering pattern.
  • ~/.claude/rules/shared/review-checklists/mastra.md — mandatory pre-commit checklist for new tools.
  • ~/.claude/skills/ddx-ai-ui-events-frontend/SKILL.md — frontend consumer side.
  • ~/.claude/skills/ddx-ai-ui-events-backend/SKILL.md — publisher side reference.
    AI Medical Tools — Clinical Tool Catalog — Dudoxx Docs | Dudoxx