Skip to content

Copilot — AI chat

Claude-backed chat over the live atlas. Tools are auto-collected from every other enabled module — there's no per-project copilot wiring. An optional PubMed-grounded literature lookup adds external context with a project-configurable disease scope.

extras_key copilot
config_key copilot
install pip install 'stellar-atlas[copilot]'
frontend tab Chat

Enable

modules:
  copilot:
    enabled: true
    # Configurable env var name (default: ANTHROPIC_API_KEY). Pick a
    # project-specific name when running multiple atlases on one host.
    anthropic_api_key_env: ANTHROPIC_API_KEY
    # Configurable model (default: claude-opus-4-7). Set
    # `claude-sonnet-4-7` for cost-sensitive deploys.
    model: claude-opus-4-7
    literature:
      enabled: true                               # opt-in per project
      ncbi_api_key_env: NCBI_API_KEY              # optional, bumps rate limit
      scope:
        diseases:   [Alzheimer]                   # see "Disease scope" below
        techniques: [single-cell, snRNA-seq, scRNA-seq, single-nucleus]
        min_year:   2018

Configuration keys

Key Default Purpose
modules.copilot.enabled false Master switch.
modules.copilot.anthropic_api_key_env "ANTHROPIC_API_KEY" Env var (or .env key) holding the Anthropic key. Lets one host run multiple atlases with distinct keys.
modules.copilot.model "claude-opus-4-7" Claude model ID. Override to claude-sonnet-4-7 etc. for cost.
modules.copilot.literature.enabled false Master switch for PubMed lookups.
modules.copilot.literature.ncbi_api_key_env "NCBI_API_KEY" Env var for the optional NCBI key (raises rate limit 3→10 req/s).
modules.copilot.literature.scope.diseases [] Disease focus, AND-ed into every PubMed query. Empty = no disease filter.
modules.copilot.literature.scope.techniques (see Enable block) Technique focus, AND-ed into every query.
modules.copilot.literature.scope.min_year 2018 Minimum publication year, AND-ed into every query.

Keys

The Anthropic key is read from the env var named by anthropic_api_key_env (default ANTHROPIC_API_KEY) — either the process environment or <project_root>/.env. Without it, POST /api/copilot/ask returns HTTP 503 with a clear error.

The NCBI key is optional. Without it the literature tool uses the 3 req/s anonymous limit; with it, 10 req/s. Grab one from NCBI account settings.

Composable system prompt

No disease-specific vocabulary in the base prompt

The model never sees a project-specific hand-written prompt. The base prompt has zero disease-specific vocabulary — every domain term (cell types, condition labels, disease focus) is discovered at boot time from your stellar.yaml and the cohort data.

At server boot the prompt is assembled from four sources:

  1. Project header — title, description, lab name from stellar.yaml.
  2. Cohort vocabulary — distinct cell_type + condition values discovered from cells_v so the model uses the names that actually exist in this atlas.
  3. Module fragments — each enabled module contributes a short claude_system_prompt() paragraph. Examples (concrete):
    • DE"DE comparisons are precomputed. Discover comparison_ids with list_de_comparisons … never invent a comparison_id."
    • hdWGCNA"Co-expression modules are precomputed. module_id follows <cell_type>:<color> … always discover real ids."
    • CellChat"CellChat signaling is precomputed per group … probabilities are CellChat's communication probability."
    • Milo"A run is a single comparison; log2fc + spatial_fdr are per-neighborhood (not per-cluster)."
    • Enrichment"enrich_genes runs live EnrichR. Use it after a DE pull to turn a top-gene set into pathways."
  4. Literature rules — when literature is on, the atlas-first behaviour rule + a summary of the configured scope clause.

A module that ships with its own claude_system_prompt() is the canonical way to teach the model how to call that module's tools — it's the alternative to hardcoding tool-specific guidance in the copilot itself.

Tool surface

Auto-unioned across enabled modules. Each module's claude_dispatch(stores) returns a {tool_name: callable} map; the copilot collects them via tool_registry.collect_tools and hands the union to Claude.

Module Tools
de list_de_comparisons, compare_groups
hdwgcna list_modules, get_module
cellchat list_cellchat_groups, get_cellchat_pathways, compare_cellchat_groups
milo list_milo_runs, get_milo_da
enrichment enrich_genes
copilot.literature search_literature (added when literature is on)

A module that ships its own tools but isn't enabled in stellar.yaml contributes nothing — disabled modules are inert.

Worked example — the agent loop

User question (typed into the Chat tab):

"What's enriched among the top up-regulated genes for the T-cell disease-vs-control comparison?"

What happens, turn by turn:

  1. Turn 1 — tool call. Claude knows comparison IDs are opaque (the DE system-prompt fragment tells it so). It emits a tool call list_de_comparisons(cell_type="T").
event: tool_start   data: {"name":"list_de_comparisons", ...}
event: tool_result  data: {"name":"list_de_comparisons",
                           "result":[{"comparison_id":"T_disease_vs_control", ...}]}
  1. Turn 2 — tool call. Claude picks the matching comparison ID and pulls the top hits:
event: tool_start   data: {"name":"compare_groups",
                           "input":{"comparison_id":"T_disease_vs_control","top_n":25}}
event: tool_result  data: {"name":"compare_groups",
                           "result":[{"gene":"GZMB","log2fc":2.1,"padj":1e-12}, ...]}
  1. Turn 3 — tool call. With the gene list in hand Claude calls the Enrichment module:
event: tool_start   data: {"name":"enrich_genes",
                           "input":{"genes":["GZMB","PRF1","IFNG",...]}}
event: tool_result  data: {"name":"enrich_genes",
                           "result":[{"library":"GO_BP", "term":"...", ...}]}
  1. Turn 4 — answer. Claude streams the synthesised answer back as token events; the SSE stream ends with done.

Each tool_start / tool_result event renders inline in the chat panel so the user sees exactly which atlas tables Claude consulted.

Disease scope (literature)

The literature scope is per-project configurable and locked at the server. Pick whichever disease (or set of diseases) your atlas is about:

literature:
  scope:
    diseases: [Alzheimer]
literature:
  scope:
    diseases: [Parkinson]
literature:
  scope:
    diseases: [Alzheimer, "Frontotemporal Dementia"]
literature:
  scope:
    diseases: []

The model can't bypass the scope

Server-side AND clause — un-bypassable

Whatever the model emits as the query parameter, the backend silently AND-s the configured scope clause before hitting PubMed. The model cannot bypass this — it's enforced after the tool call returns, not in the prompt. A prompt-injection attack telling Claude "ignore your scope" has no effect; the scope is applied at the HTTP-call layer, not in the LLM's reasoning.

user query           "DAM microglia markers"
backend rewrites to  "(DAM microglia markers)
                       AND ("Alzheimer"[Title/Abstract])
                       AND ("single-cell"[Title/Abstract] OR
                            "snRNA-seq"[Title/Abstract] OR …)
                       AND 2018:3000[pdat]"
sent to NCBI

Changing scope is a config edit, not a model-prompt change.

Routes

POST /api/copilot/ask

POST /api/copilot/ask
content-type: application/json

{
  "prompt":  "user question",
  "focus":   {"gene": "APOE"},
  "history": [{"role": "user", "content": "..."}],
  "include_literature": true
}

The response is a Server-Sent Events stream with the events:

  • token — text delta (one event per Claude chunk)
  • tool_start — Claude invoked a tool; payload is {name, id}
  • tool_result — tool returned; payload is {id, name, input, result}
  • done — terminal event with the final stop reason
  • error — terminal failure; renders inline in the chat panel

Example — minimal curl

curl -N -X POST http://localhost:18901/api/copilot/ask \
     -H 'content-type: application/json' \
     -d '{"prompt": "Which cell types are in this atlas?"}'

Example — with focus + history

curl -N -X POST http://localhost:18901/api/copilot/ask \
     -H 'content-type: application/json' \
     -d '{
       "prompt":  "What about its top markers?",
       "focus":   {"cell_type": "T"},
       "history": [
         {"role": "user",      "content": "Which cell types are in this atlas?"},
         {"role": "assistant", "content": "T, B, Neuron, ..."}
       ]
     }'

Example — literature mode

curl -N -X POST http://localhost:18901/api/copilot/ask \
     -H 'content-type: application/json' \
     -d '{
       "prompt": "What does the literature say about GZMB in T cells?",
       "include_literature": true
     }'

If modules.copilot.literature.enabled: false, this returns HTTP 400 with literature not enabled in stellar.yaml. See Troubleshooting.

Cost / latency

Token usage scales with the tool surface (each module's schemas live in the system prompt) and the conversation length. With prompt caching enabled (cache_control: ephemeral on the system block) the per-turn incremental cost stays modest. Adaptive thinking is on by default. The default model is claude-opus-4-7; switch to claude-sonnet-4-7 via modules.copilot.model for ~3× cheaper turns when Opus's reasoning isn't required.

See also

  • Extending — implement claude_tools() + claude_dispatch() to add your own module's tools to this surface.
  • Troubleshooting — 503 / 400 fixes.
  • FAQ — what leaves the box.

FAQ

How do I keep the chat off PubMed entirely?

Set modules.copilot.literature.enabled: false. The search_literature tool is not registered and the system prompt omits the literature-rules fragment.

Can I add my own tools?

Yes — any third-party module whose claude_tools() returns schemas and whose claude_dispatch() returns callables will be folded in automatically. See Extending.

Can I run multiple atlases on one host with different Anthropic keys?

Yes — give each project its own anthropic_api_key_env:

# /srv/atlases/atlas_a/stellar.yaml
modules:
  copilot:
    enabled: true
    anthropic_api_key_env: ANTHROPIC_KEY_ATLAS_A

The systemd unit's EnvironmentFile= (see Deploy) can then load distinct keys per project.