Skip to main content
Synced from the repo — do not edit here

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

PlatformKnowledge SourcesExternal ResourcesUse Case
Basis Flow WebKB + Documents + Patient DataPubMed, medical literatureStaff analyzing patient data
Basis HybridKB + Documents ONLYNo external resourcesClient 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)

KBPathPurpose
Exercise DBreference/exercise_db/exercisesExercise descriptions, instructions
Supplement DBreference/supplement_db/supplementsSupplement info, dosing
Protocol Templatesreference/protocol_templates/protocolsEvidence-based protocols
Drug DBreference/drug_db/medicationsMedication 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

ToolPurposeBasis Flow WebBasis Hybrid
get_health_metricsQuery wearable dataYesYes
get_lab_resultsRetrieve lab valuesYesYes
get_documentsSearch uploaded docsYesYes
get_appointmentsQuery appointmentsYesYes
search_knowledge_baseSearch clinic KBYesYes
search_medical_literatureQuery PubMedYesNo
prepare_appointmentDraft appointmentYesNo
prepare_protocolDraft care planYesNo
prepare_clinical_noteDraft clinical noteYesNo
generate_health_reportFull patient auditYesNo

Note: the table above is a curated subset. As of 2026-06 the agent registers ~37 tools (TOOLS list in functions_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

ToolWrites toNotes
save_clinical_noteclinic_users/{user}/notes/{id}Live note. Requires title+content.
generate_soap_from_transcriptclinic_users/{user}/notes/{id}AI Scribe; persists via downstream generate_soap_note()
prepare_protocolclinic_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_taskheartbeat_tasks/{id}Proactive scheduled task
create_sequence / update_sequence_status / enroll_leademail_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 actionAgent tool todayBackend handler (request_type)Status
Book appointmentprepare_appointment (PREPARE)book_appointment (functions_clinic.py:7547)Wiring gap — no execute tool
Reschedule appointmentnoneclient_reschedule_appointment (12817)Wiring gap
Report (labs+sleep+workout) → save to Documentsgenerate_health_report (draft → copilot_drafts)no clean "save to documents" handlerWiring + small build (final-save)
Update client emailnoneadmin_update_clinic_user_emailWiring gap
Update client membership/recordnoneupdate_clinic_user (membershipType, etc.)Wiring gap
Create / update membershipget_memberships (READ)add_membership_type / update_membership_typeWiring gap ⚠️ Stripe-coupled
Create / update productget_products (READ)add_product / update_productWiring gap ⚠️ Stripe-coupled
Create / update serviceget_services (READ)add_service / update_serviceWiring gap ⚠️ Stripe-coupled
Staff availability / unavailabilityget_staff (READ)update_staff_user (availability/blockedIntervals), create_staff_unavailableWiring gap
Assign protocol to clientprepare_protocol (EXECUTE create+assign)assign_protocolMostly covered (but bypasses handler — see above)
Update existing protocol activities (e.g. week 5–8 strength)noneupdate_protocol (habits/activities) (1609)Wiring gap
Create clinical notesave_clinical_note (EXECUTE)n/a (direct write)Supported

Considerations before wiring write/execute tools

  1. Call canonical handlers, never write Firestore directly. New execute tools must invoke the existing clinic_service handlers (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).
  2. Stripe-coupled writes (membership/product/service, membership assignment) touch Stripe product/price IDs and the ClinicUserDetails webhook model — naive writes can break payments. Route through the handler + confirmation.
  3. Human-in-the-loop confirmation + audit. Every execute action should render a confirmation card and log to transactions.
  4. 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

ComponentFilePurpose
clinical_assistantfunctions_ai.pyMain agent with tool calling
clinical_streamfunctions_ai.pyStreaming responses via RTDB
clinical_quickfunctions_ai.pyFast answers without full agent
kb_searchfunctions_ai.pySearch clinic knowledge base
copilot_searchfunctions_copilot_search.pyDocument search
functions_rag.pyRAG infrastructure, embeddings, vector search
functions_orchestrator.pyIntent classification (Haiku), model routing, tool filtering, patient-snapshot injection (NOTE: RAG pre-fetch was removed — see RAG section)
functions_heartbeat.pyProactive scheduled monitoring (dispatcher + worker)
functions_ai_memory.pyPatient memory (facts, profile, conversation summaries)
prompt_loader.pyModular prompt assembly from prompts/ markdown files
quick_answers.pyPattern matching for instant answers

RAG Pipeline (Retrieval-Augmented Generation)

The RAG system provides semantic search over all patient data using Firestore vector search.

Architecture

  1. Embedding: Vertex AI text-embedding-004 (768 dimensions)
  2. Storage: Firestore embeddings subcollection with vector index
  3. Retrieval: find_nearest() with cosine similarity
  4. Index: Defined in firestore.indexes.json on embeddings collection group

Auto-Indexing Triggers (live, deployed)

TriggerPathData Type
index_user_noteusers/{userId}/notes/{noteId}notes
index_user_labusers/{userId}/labs/{labId}labs
index_user_health_summaryusers/{userId}/healthSummaries/{id}health_summaries
index_clinic_noteclinicsv2/{clinicId}/clinic_users/{userId}/notes/{noteId}notes
index_clinic_labclinicsv2/{clinicId}/clinic_users/{userId}/labs/{labId}labs
index_clinic_documentclinicsv2/{clinicId}/clinic_users/{userId}/documentSummaries/{docId}documents
index_clinic_eventclinicsv2/{clinicId}/clinic_users/{userId}/events/{eventId}events
index_clinic_kbclinicsv2/{clinicId}/settings/copilotkb_entry

How RAG Connects to Atlas Agent

  1. Orchestrator pre-fetch — REMOVED (2026-06). orchestrate_query() used to call retrieve_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 the search_knowledge_base tool instead. Patient context is supplied by build_patient_snapshot() (deterministic Firestore reads) + patient memory facts, not by pre-fetch RAG. See the orchestrator source comment near the enhanced_query assembly.
  2. KB search tool: search_knowledge_base uses Firestore vector search on clinic-level embeddings (data_type=kb_entry) with keyword fallback.
  3. Manual indexing: index_patient_data() endpoint for bulk re-indexing existing patients.

Key Functions

FunctionPurpose
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

  1. Third-person only: Atlas always refers to patients in third person ("The patient has..." not "You have...")
  2. Knowledge base priority: Clinic KB checked BEFORE PubMed for protocols
  3. Tool-first approach: Agent always uses tools to get real data, never fabricates
  4. Client app restrictions: Basis Hybrid copilot cannot access external resources
  5. Reasoning framework: Agent follows PARSE -> ASSESS -> PLAN -> EXECUTE -> SYNTHESIZE
  6. Clarification behavior: Agent asks for clarification when ambiguous - never guesses

Reasoning Framework

  1. PARSE - Understand what the clinician is literally asking and their actual intent
  2. ASSESS - Identify any gaps (ambiguous query? missing context? multiple interpretations?)
  3. PLAN - Decide which tools to use and in what order
  4. EXECUTE - Call the tools and gather information
  5. 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

FunctionFilePurpose
clinical_agent_streamfunctions_ai_agent.pyMain streaming endpoint from Basis Flow Web
orchestrate_queryfunctions_orchestrator.pyIntent classification + memory injection
run_claude_agentfunctions_ai_agent.pyClaude-specific implementation (primary agent)
run_gemini_agentfunctions_ai_agent.pyGemini-specific implementation
run_openai_agentfunctions_ai_agent.pyOpenAI-specific implementation
quick_answer_routerquick_answers.pyFast-path pattern matching (runs first!)
kb_searchfunctions_ai.pyKnowledge base search
get_patient_memoryfunctions_ai_memory.pyLoad 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:

IntentTools
CLINICALget_health_metrics, get_lab_results, get_clinical_notes, get_patient_profile, generate_health_report, get_documents
OPERATIONSget_appointments, prepare_appointment, get_services, get_products, get_memberships, get_staff
RESEARCHsearch_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 instructions
  • style / styleGuardrails - Response style guidelines
  • operations / 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:

PermissionAccess
supportPlatform help only, no patient/clinic data
clinicalPatient health data (PHI - requires BAA)
adminAll access

Before deploying Copilot changes, verify:

  1. Run test suite - 6/7 must pass
  2. Firestore access uses fire.db - Never create separate clients
  3. Check RTDB debug output - debug.actions shows which path was taken
  4. All new DuckDB queries - Verify column names against terra_adapter.py

Common Mistakes to Avoid

MistakeConsequencePrevention
Using firestore.client()AttributeErrorAlways use fire.db
Not checking quick_answer_router flowKB bypassedKB search must be at TOP of router
Hardcoded SYSTEM_PROMPTCustom prompts ignoredBuild effective_system_prompt dynamically
Only fixing one providerWorks on Claude but not GeminiUpdate 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 userId crash + mis-attribution. log_ai_usage was being passed the patient id as userId (a required str), which both mis-attributed cost and threw a Pydantic validation 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.userId now defaults to "" to never crash billing. Sites: functions_orchestrator.py classifier log, functions_ai_agent.py _log_claude_usage (+ both call sites pass caller_uid).
  • "Stuck on Thinking…" perceived latency. Root cause: classify/context phases wrote no debug.toolStatus, and a sticky reasoning field ("Analyzing your question…") persisted in RTDB and masked every later tool-status update (frontend checked reasoning last). Fixed: on_status now emits friendly phase labels ("Understanding your question…", "Reviewing context…") and clears reasoning; on_tool_call emits friendly per-tool labels ("Looking up platform help…") and clears reasoning; CopilotDrawer.tsx now lets toolStatus win over stale reasoning.
  • Latency. classify_intent now runs concurrently with build_patient_snapshot (was serialized).

Known Issues / Open Gaps

  • Background-thread execution model (see Execution model above) — the biggest latent reliability risk; move to on_request SSE.
  • generate_health_report saves to copilot_drafts, not Documents — no final "save to client Documents" step.
  • prepare_protocol writes Firestore directly instead of calling assign_protocol — bypasses validation/mirroring/audit.
  • Opus→Sonnet downgrade is invisible in the UI (label still says Opus).
  • get_platform_help is 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:

  1. Prompt caching (system+tools+patient context) — cost + latency, do first.
  2. 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.
  3. Invert model routing — Opus + extended/interleaved thinking for clinical; Sonnet for ops. Drop or cheapen the classifier round-trip.
  4. Grounding + safety — inline citations; dosing/interactions tool-verified, never free-generated; calibrated "I don't know"; eval + red-team gate on deploy.
  5. Agency — wire execute tools to the existing clinic_service handlers (see capability matrix) behind confirmation cards + audit.
  6. 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

AspectStaff Copilot (Atlas)Client AI Assistant
UserClinician viewing patientClient viewing own data
PerspectiveThird-person ("The patient has...")First/second-person ("You have...")
External ResourcesPubMed, literatureNone
Clinical ActionsDraft notes, protocolsNone
Data AccessAll patient dataOnly user's own data
Knowledge BaseClinic KB + documentsClinic KB only

Implementation Details

Main Service: ServiceChatAIBackend (stub_service_chat_ai_backend.dart)

Backend Calls:

  1. clinical_quick - Fast KB-only answers (6s timeout)
  2. classify_user_intent - LLM intent classification (8s timeout)
  3. runPythonAgent - Full agent with tools (for complex queries)

Local Processing:

  • ServiceDuckDb - Queries local health data (steps, sleep, HR, etc.)
  • ServiceLabsAssistant - Lab result lookups
  • ServiceMetricsAssistant - 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)

PatternDetected 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

FilePurpose
stub_service_chat_ai_backend.dartMain AI service implementation
route_chat.dartChat UI and message handling
ai_chat_services.dartChat service wrappers
service_duckdb.dartLocal health data queries
service_labs_assistant.dartLab data lookups
service_metrics_assistant.dartMetric 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:

  1. Check clinic's aiDefaultFallbackPrompt setting
  2. If set -> Return clinic's custom fallback message
  3. 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 chat
  • aiDefaultFallbackPrompt - Custom fallback message when KB has no answer
  • aiReplyScopes - What data AI can access