Canonical source: docs/claude/copilot.md. This page is generated by docs/scripts/sync-handbook.mjs. Edit the source file in the repo; changes appear here on the next build.
Copilot (Atlas AI) + Client AI Assistant
Atlas is the AI clinical assistant. It has different capabilities depending on which app is using it.
Design Philosophy
The platform's core job for Copilot is twofold:
1. Structure ALL data for optimal LLM consumption — Make every piece of available data structured and accessible so the LLM can generate valuable outputs.
2. Make outputs digestible and interactable — Scannable, actionable, contextual, interactive.
The goal: Clinician asks a question -> Copilot has access to ALL relevant data -> Returns structured, actionable insight -> Staff can immediately act on it.
Platform Differences
| Platform | Knowledge Sources | External Resources | Use Case |
|---|---|---|---|
| Basis Flow Web | KB + Documents + Patient Data | PubMed, medical literature | Staff analyzing patient data |
| Basis Hybrid | KB + Documents ONLY | No external resources | Client asking about their own data |
Knowledge Base Structure
1. Clinic Knowledge Base (FAQs, Protocols)
Storage: clinicsv2/{clinic}/settings/copilot.knowledgeBase
interface KnowledgeBaseEntry {
title: string;
content: string;
keywords?: string[];
appliesTo?: string[];
source?: string;
url?: string;
fileName?: string;
}
2. Document Summaries (Uploaded Documents)
Storage:
clinicsv2/{clinic}/clinic_users/{uid}/documentSummaries(clinic-scoped)users/{uid}/documentSummaries(user-scoped)
3. Reference Knowledge Bases (Global)
| KB | Path | Purpose |
|---|---|---|
| Exercise DB | reference/exercise_db/exercises | Exercise descriptions, instructions |
| Supplement DB | reference/supplement_db/supplements | Supplement info, dosing |
| Protocol Templates | reference/protocol_templates/protocols | Evidence-based protocols |
| Drug DB | reference/drug_db/medications | Medication information |
4. RAG (Retrieval-Augmented Generation)
Uses Vertex AI text-embedding-004 (768 dimensions) for semantic search:
- Patient notes, labs, health summaries are embedded
- Vector search finds relevant context for questions
- Indexed in Firestore with vector fields
Available Tools
| Tool | Purpose | Basis Flow Web | Basis Hybrid |
|---|---|---|---|
get_health_metrics | Query wearable data | Yes | Yes |
get_lab_results | Retrieve lab values | Yes | Yes |
get_documents | Search uploaded docs | Yes | Yes |
get_appointments | Query appointments | Yes | Yes |
search_knowledge_base | Search clinic KB | Yes | Yes |
search_medical_literature | Query PubMed | Yes | No |
prepare_appointment | Draft appointment | Yes | No |
prepare_protocol | Draft care plan | Yes | No |
prepare_clinical_note | Draft clinical note | Yes | No |
generate_health_report | Full patient audit | Yes | No |
Note: the table above is a curated subset. As of 2026-06 the agent registers ~37 tools (
TOOLSlist infunctions_ai_agent.py). The full action-capability picture is below.
Agent Action Capabilities (what Atlas can actually DO)
Atlas is read-first by design: ~24 of ~37 tools are READ; the remainder mostly stage drafts in clinicsv2/{clinic}/clinic_users/{user}/copilot_drafts/{id} for a human to confirm. Only a few tools execute real writes. Classify every tool as one of: READ (returns data) · PREPARE (stages a draft, no real change) · EXECUTE (persists a real change).
Tools that actually EXECUTE (write) today
| Tool | Writes to | Notes |
|---|---|---|
save_clinical_note | clinic_users/{user}/notes/{id} | Live note. Requires title+content. |
generate_soap_from_transcript | clinic_users/{user}/notes/{id} | AI Scribe; persists via downstream generate_soap_note() |
prepare_protocol | clinic_users/{user}/protocols/{id} + users/{user}/protocols/{id} | ⚠️ Misnamed — despite "prepare", it creates + assigns immediately. Also ⚠️ writes Firestore directly instead of calling the canonical assign_protocol handler (risks bypassing validation/mirroring/audit). |
create_heartbeat_task | heartbeat_tasks/{id} | Proactive scheduled task |
create_sequence / update_sequence_status / enroll_lead | email_sequences/... | Full Growth sequence CRUD |
Capability matrix vs. requested clinician actions
Backend write handlers (used by the web UI via clinic_service + REQUEST_MODEL_MAP) already exist for nearly everything. The gap is overwhelmingly wiring the agent to existing handlers, not building backend capability.
| Clinician action | Agent tool today | Backend handler (request_type) | Status |
|---|---|---|---|
| Book appointment | prepare_appointment (PREPARE) | book_appointment (functions_clinic.py:7547) | Wiring gap — no execute tool |
| Reschedule appointment | none | client_reschedule_appointment (12817) | Wiring gap |
| Report (labs+sleep+workout) → save to Documents | generate_health_report (draft → copilot_drafts) | no clean "save to documents" handler | Wiring + small build (final-save) |
| Update client email | none | admin_update_clinic_user_email | Wiring gap |
| Update client membership/record | none | update_clinic_user (membershipType, etc.) | Wiring gap |
| Create / update membership | get_memberships (READ) | add_membership_type / update_membership_type | Wiring gap ⚠️ Stripe-coupled |
| Create / update product | get_products (READ) | add_product / update_product | Wiring gap ⚠️ Stripe-coupled |
| Create / update service | get_services (READ) | add_service / update_service | Wiring gap ⚠️ Stripe-coupled |
| Staff availability / unavailability | get_staff (READ) | update_staff_user (availability/blockedIntervals), create_staff_unavailable | Wiring gap |
| Assign protocol to client | prepare_protocol (EXECUTE create+assign) | assign_protocol | Mostly covered (but bypasses handler — see above) |
| Update existing protocol activities (e.g. week 5–8 strength) | none | update_protocol (habits/activities) (1609) | Wiring gap |
| Create clinical note | save_clinical_note (EXECUTE) | n/a (direct write) | ✅ Supported |
Considerations before wiring write/execute tools
- Call canonical handlers, never write Firestore directly. New execute tools must invoke the existing
clinic_servicehandlers (or their internal functions) so they inherit validation, mirroring, Stripe sync, and audit.prepare_protocol's direct write is the anti-pattern to avoid (and to fix). - Stripe-coupled writes (membership/product/service, membership assignment) touch Stripe product/price IDs and the
ClinicUserDetailswebhook model — naive writes can break payments. Route through the handler + confirmation. - Human-in-the-loop confirmation + audit. Every execute action should render a confirmation card and log to
transactions. - Permission scoping. Execute tools must respect
get_permitted_tools(caller_perms, TOOLS)— the agent already filters tools by the caller's role; write tools must be gated the same way (e.g. billing/clinic-admin for memberships).
Key Components
| Component | File | Purpose |
|---|---|---|
clinical_assistant | functions_ai.py | Main agent with tool calling |
clinical_stream | functions_ai.py | Streaming responses via RTDB |
clinical_quick | functions_ai.py | Fast answers without full agent |
kb_search | functions_ai.py | Search clinic knowledge base |
copilot_search | functions_copilot_search.py | Document search |
functions_rag.py | RAG infrastructure, embeddings, vector search | |
functions_orchestrator.py | Intent classification (Haiku), model routing, tool filtering, patient-snapshot injection (NOTE: RAG pre-fetch was removed — see RAG section) | |
functions_heartbeat.py | Proactive scheduled monitoring (dispatcher + worker) | |
functions_ai_memory.py | Patient memory (facts, profile, conversation summaries) | |
prompt_loader.py | Modular prompt assembly from prompts/ markdown files | |
quick_answers.py | Pattern matching for instant answers |
RAG Pipeline (Retrieval-Augmented Generation)
The RAG system provides semantic search over all patient data using Firestore vector search.
Architecture
- Embedding: Vertex AI
text-embedding-004(768 dimensions) - Storage: Firestore
embeddingssubcollection with vector index - Retrieval:
find_nearest()with cosine similarity - Index: Defined in
firestore.indexes.jsononembeddingscollection group
Auto-Indexing Triggers (live, deployed)
| Trigger | Path | Data Type |
|---|---|---|
index_user_note | users/{userId}/notes/{noteId} | notes |
index_user_lab | users/{userId}/labs/{labId} | labs |
index_user_health_summary | users/{userId}/healthSummaries/{id} | health_summaries |
index_clinic_note | clinicsv2/{clinicId}/clinic_users/{userId}/notes/{noteId} | notes |
index_clinic_lab | clinicsv2/{clinicId}/clinic_users/{userId}/labs/{labId} | labs |
index_clinic_document | clinicsv2/{clinicId}/clinic_users/{userId}/documentSummaries/{docId} | documents |
index_clinic_event | clinicsv2/{clinicId}/clinic_users/{userId}/events/{eventId} | events |
index_clinic_kb | clinicsv2/{clinicId}/settings/copilot | kb_entry |
How RAG Connects to Atlas Agent
- Orchestrator pre-fetch — REMOVED (2026-06).
orchestrate_query()used to callretrieve_patient_context()to inject the top-K semantic chunks before the agent loop. This was removed because the Vertex embedding + Firestore vector search added 3–10s of blocking latency on every request. RAG context is now available on demand via thesearch_knowledge_basetool instead. Patient context is supplied bybuild_patient_snapshot()(deterministic Firestore reads) + patient memory facts, not by pre-fetch RAG. See the orchestrator source comment near theenhanced_queryassembly. - KB search tool:
search_knowledge_baseuses Firestore vector search on clinic-levelembeddings(data_type=kb_entry) with keyword fallback. - Manual indexing:
index_patient_data()endpoint for bulk re-indexing existing patients.
Key Functions
| Function | Purpose |
|---|---|
get_embedding(text) | Generate 768-dim embedding via Vertex AI |
index_patient_document(...) | Chunk + embed + store in Firestore |
retrieve_patient_context(user_id, clinic_id, query, top_k) | Semantic search over patient embeddings |
assemble_context(patient_results, reference_results) | Format for LLM injection |
Configuration
Copilot settings stored in clinicsv2/{clinic}/settings/copilot:
knowledgeBase: Array of FAQ/protocol entries- Custom system prompts
- Guardrails and response style
- Report templates
- Feature toggles
Billing
AI usage is tracked and billed per clinic:
- Token usage logged in
functions_ai_billing.py - Plans: Free tier, Standard, Pro
- Usage tracked by service type (copilot, client app, etc.)
Important Behavioral Notes
- Third-person only: Atlas always refers to patients in third person ("The patient has..." not "You have...")
- Knowledge base priority: Clinic KB checked BEFORE PubMed for protocols
- Tool-first approach: Agent always uses tools to get real data, never fabricates
- Client app restrictions: Basis Hybrid copilot cannot access external resources
- Reasoning framework: Agent follows PARSE -> ASSESS -> PLAN -> EXECUTE -> SYNTHESIZE
- Clarification behavior: Agent asks for clarification when ambiguous - never guesses
Reasoning Framework
- PARSE - Understand what the clinician is literally asking and their actual intent
- ASSESS - Identify any gaps (ambiguous query? missing context? multiple interpretations?)
- PLAN - Decide which tools to use and in what order
- EXECUTE - Call the tools and gather information
- SYNTHESIZE - Combine findings into actionable clinical response
Critical: If ASSESS reveals ambiguity, the agent MUST ask for clarification before proceeding. Never guess.
Implementation Technical Details
Firestore Access Pattern
CRITICAL: Always use fire.db from the shared fire module. Never create your own Firestore client.
# CORRECT
from . import fire
doc = fire.db.collection('clinicsv2').document(clinic_id).get()
# WRONG - Don't create your own client
from google.cloud import firestore as _fs
fs = _fs.Client() # Creates new client instead of using shared one
Request Flow & Priority
Frontend (CopilotDrawer.tsx)
| calls clinical_agent_stream
quick_answer_router() <- Runs FIRST! Can bypass everything
| if no match
orchestrate_query() <- Intent classification + memory injection
|
run_claude_agent() / run_gemini_agent() / run_openai_agent()
Key insight: quick_answer_router() in quick_answers.py runs BEFORE the full LLM agent. It's designed for simple, deterministic queries (list active protocols, show recent labs). It does NOT:
- Synthesize answers using an LLM
- Fetch patient context for personalization
- Ask clarifying questions
IMPORTANT: KB queries must NOT go through quick_answer_router. They need the full agent.
Protocol queries: try_answer_protocols_active ONLY matches explicit list/show requests like "list my protocols" or "show active protocols". All other protocol-related queries go to the full agent.
Key Function Locations
| Function | File | Purpose |
|---|---|---|
clinical_agent_stream | functions_ai_agent.py | Main streaming endpoint from Basis Flow Web |
orchestrate_query | functions_orchestrator.py | Intent classification + memory injection |
run_claude_agent | functions_ai_agent.py | Claude-specific implementation (primary agent) |
run_gemini_agent | functions_ai_agent.py | Gemini-specific implementation |
run_openai_agent | functions_ai_agent.py | OpenAI-specific implementation |
quick_answer_router | quick_answers.py | Fast-path pattern matching (runs first!) |
kb_search | functions_ai.py | Knowledge base search |
get_patient_memory | functions_ai_memory.py | Load patient memory/facts |
Orchestrator Architecture
The orchestrator layer (functions_orchestrator.py) sits between the entry point and the agent:
clinical_agent_stream
|
quick_answer_router (fast path for simple queries)
| if no match
orchestrate_query (intent classification + memory)
|-- build_patient_snapshot + classify_intent (run CONCURRENTLY, 2026-06)
|-- Model routing (Opus -> Sonnet downgrade for CLINICAL/OPS)
|-- Filter tools to relevant domain (intent ∩ permissions)
|-- Inject patient-snapshot + memory context into prompt
|
run_claude_agent (with focused toolset, streaming SSE -> RTDB)
|
Extract new facts (async)
Execution model (important). clinical_agent_stream is an @https_fn.on_call (request/response — it cannot stream to the client). It returns immediately with {status: 'processing', requestId} and runs the whole agent in a detached daemon thread (threading.Thread(..., daemon=True)), writing tokens/status to RTDB at vertex/{request_id}; the browser (CopilotDrawer.tsx) subscribes to RTDB. Config: memory=GB_1, cpu=1, min_instances=1, timeout_sec=300. ⚠️ Cloud Run throttles CPU after the HTTP response returns unless CPU-is-always-allocated is set, so the post-response background thread can run degraded — a likely contributor to long/hung responses. The frontend has a 3-minute staleness timeout as a backstop. Moving to an on_request SSE endpoint (agent runs in-request) is the recommended fix.
Intent Classification uses Claude Haiku (claude-haiku-4-5-20251001, max_tokens=20) for routing. It is a real network LLM call (typically ~1s, but observed up to ~10s under API congestion — httpx timeout is 10s, falls back to MIXED). As of 2026-06 it runs in parallel with build_patient_snapshot so it no longer serializes on the critical path. Categories:
- CLINICAL: Health data, labs, metrics, notes, documents
- OPERATIONS: Scheduling, appointments, services, staff
- RESEARCH: Knowledge base, protocols, literature
- MIXED: Multi-domain queries (gets all tools, stays on Opus)
Model routing: for model=claude-opus-4-6 with CLINICAL or OPERATIONS intent, the orchestrator downgrades to claude-sonnet-4-6 for speed. The UI still labels it "Opus." (Trade-off flagged for review: clinical reasoning is exactly where you may want the stronger model + extended thinking.)
Tool Filtering by Intent:
| Intent | Tools |
|---|---|
| CLINICAL | get_health_metrics, get_lab_results, get_clinical_notes, get_patient_profile, generate_health_report, get_documents |
| OPERATIONS | get_appointments, prepare_appointment, get_services, get_products, get_memberships, get_staff |
| RESEARCH | search_knowledge_base, search_medical_literature, get_protocols, match_patient_protocols, check_drug_interactions |
Patient Memory is loaded from clinicsv2/{clinic}/clinic_users/{user}/settings/memory and injected into the prompt.
Custom Prompts Loading
Custom prompts are stored in clinicsv2/{clinicId}/settings/copilot:
custom_prompt- Clinic-specific instructionsstyle/styleGuardrails- Response style guidelinesoperations/operationalDirectives- Operational rules
The agent functions receive clinic_settings parameter and MUST incorporate these into the system prompt.
Testing Copilot Changes
Test Tool: basis-functions/functions/test/test_copilot.py
export ATLAS_API_KEY="basis_K-bnQ8OAwAp48zlPOqRVm7jsVDAAlPF0sZ_1Tbcf7Mw"
cd /Users/G/basis/basis-functions/functions/test
# Run test suite
python test_copilot.py --suite -c axuk-khwf-prkr -u GoOU3d2QcyRZMD83IhbQeIclpwh2
# Single query with verbose output
python test_copilot.py -q "How much exercise?" -c axuk-khwf-prkr -u USER_ID -v
Test Categories:
- [Quick] Tests - Hit the DuckDB fast path. Catch column name errors, strftime errors, etc.
- [Agent] Tests - Go through the full orchestrator + tool chain. Test intent routing.
API Key Permission Levels:
| Permission | Access |
|---|---|
support | Platform help only, no patient/clinic data |
clinical | Patient health data (PHI - requires BAA) |
admin | All access |
Before deploying Copilot changes, verify:
- Run test suite - 6/7 must pass
- Firestore access uses
fire.db- Never create separate clients - Check RTDB debug output -
debug.actionsshows which path was taken - All new DuckDB queries - Verify column names against
terra_adapter.py
Common Mistakes to Avoid
| Mistake | Consequence | Prevention |
|---|---|---|
Using firestore.client() | AttributeError | Always use fire.db |
Not checking quick_answer_router flow | KB bypassed | KB search must be at TOP of router |
Hardcoded SYSTEM_PROMPT | Custom prompts ignored | Build effective_system_prompt dynamically |
| Only fixing one provider | Works on Claude but not Gemini | Update ALL three agent functions |
Recent Fixes (2026-06-20)
Latency/UX audit of the staff copilot (clinical_agent_stream). Changes landed in source (pending deploy of clinical_agent_stream):
- Billing
userIdcrash + mis-attribution.log_ai_usagewas being passed the patient id asuserId(a requiredstr), which both mis-attributed cost and threw a Pydanticvalidation error ... userId: Input should be a valid string, input_value=None(twice per query) on non-patient queries. Fixed:userId=caller_uid(clinician),patientId=user_id;AIUsageEntry.userIdnow defaults to""to never crash billing. Sites:functions_orchestrator.pyclassifier log,functions_ai_agent.py_log_claude_usage(+ both call sites passcaller_uid). - "Stuck on Thinking…" perceived latency. Root cause: classify/context phases wrote no
debug.toolStatus, and a stickyreasoningfield ("Analyzing your question…") persisted in RTDB and masked every later tool-status update (frontend checkedreasoninglast). Fixed:on_statusnow emits friendly phase labels ("Understanding your question…", "Reviewing context…") and clearsreasoning;on_tool_callemits friendly per-tool labels ("Looking up platform help…") and clearsreasoning;CopilotDrawer.tsxnow letstoolStatuswin over stalereasoning. - Latency.
classify_intentnow runs concurrently withbuild_patient_snapshot(was serialized).
Known Issues / Open Gaps
- Background-thread execution model (see Execution model above) — the biggest latent reliability risk; move to
on_requestSSE. generate_health_reportsaves tocopilot_drafts, not Documents — no final "save to client Documents" step.prepare_protocolwrites Firestore directly instead of callingassign_protocol— bypasses validation/mirroring/audit.- Opus→Sonnet downgrade is invisible in the UI (label still says Opus).
get_platform_helpis a static dict (FAQ blobs), not retrieval — agents sometimes call it twice for one question.- No prompt caching — system prompt + ~37 tool defs + history are resent uncached on every one of up to 8 iterations (cost + TTFT; relevant to the cost run-rate).
Toward a Tier-A Physician Copilot (direction)
Priority order is trust > context completeness > agency > immediacy (clinical, not coding-assistant, priorities). High-leverage moves, by sequence not time:
- Prompt caching (system+tools+patient context) — cost + latency, do first.
- Clinical Context Packet — deterministic, structured, cached per-patient assembly (labs with units/reference-ranges/trends, active meds + interaction matrix, allergies, problem list, vitals, notes summary, protocols). Stops the agent from fishing for data one tool at a time.
- Invert model routing — Opus + extended/interleaved thinking for clinical; Sonnet for ops. Drop or cheapen the classifier round-trip.
- Grounding + safety — inline citations; dosing/interactions tool-verified, never free-generated; calibrated "I don't know"; eval + red-team gate on deploy.
- Agency — wire execute tools to the existing
clinic_servicehandlers (see capability matrix) behind confirmation cards + audit. - SSE streaming substrate; then proactivity (clinical signals on patient open) and ambient scribe.
Client AI Assistant
The client-facing AI assistant in Basis Hybrid is different from the staff Copilot (Atlas).
Key Differences from Staff Copilot
| Aspect | Staff Copilot (Atlas) | Client AI Assistant |
|---|---|---|
| User | Clinician viewing patient | Client viewing own data |
| Perspective | Third-person ("The patient has...") | First/second-person ("You have...") |
| External Resources | PubMed, literature | None |
| Clinical Actions | Draft notes, protocols | None |
| Data Access | All patient data | Only user's own data |
| Knowledge Base | Clinic KB + documents | Clinic KB only |
Implementation Details
Main Service: ServiceChatAIBackend (stub_service_chat_ai_backend.dart)
Backend Calls:
clinical_quick- Fast KB-only answers (6s timeout)classify_user_intent- LLM intent classification (8s timeout)runPythonAgent- Full agent with tools (for complex queries)
Local Processing:
ServiceDuckDb- Queries local health data (steps, sleep, HR, etc.)ServiceLabsAssistant- Lab result lookupsServiceMetricsAssistant- Metric aggregations
Query Flow
1. User sends message
2. Classify intent via classify_user_intent (LLM-assisted)
3. Route based on intent:
- Metrics query -> Local DuckDB processing
- Labs query -> Local labs assistant
- KB question -> clinical_quick endpoint
- Complex -> Full agent mode
4. If KB returns nothing -> Use clinic's aiDefaultFallbackPrompt
5. Return response to chat
Intent Detection (Local)
| Pattern | Detected Metric |
|---|---|
| "max hr", "heart rate" | heart rate |
| "steps", "step count" | steps |
| "hrv", "rmssd" | hrv |
| "bedtime", "sleep start" | bedtime |
| "sleep" | sleep |
| "exercise", "active minutes" | exercise time |
| "blood pressure", "bp" | blood pressure |
| "fasting" | fasting duration |
| "glucose", "blood sugar" | glucose |
Key Files
| File | Purpose |
|---|---|
stub_service_chat_ai_backend.dart | Main AI service implementation |
route_chat.dart | Chat UI and message handling |
ai_chat_services.dart | Chat service wrappers |
service_duckdb.dart | Local health data queries |
service_labs_assistant.dart | Lab data lookups |
service_metrics_assistant.dart | Metric aggregations |
Chat Roles
enum ChatRole {
ai, // AI assistant response
coach, // Staff/clinician
patient, // Client/user
query, // Query mode (full agent)
expert, // Expert mode (clinical_assistant)
daySummary, // Day summary generation
}
Fallback Behavior
When Knowledge Base returns no answer:
- Check clinic's
aiDefaultFallbackPromptsetting - If set -> Return clinic's custom fallback message
- If not set -> Return: "Sorry, I can only answer from your knowledge base or your data."
Configuration
Clinic settings in clinicsv2/{clinic}:
aiChatEnabled- Enable client AI chataiDefaultFallbackPrompt- Custom fallback message when KB has no answeraiReplyScopes- What data AI can access