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:
- Project header — title, description, lab name from
stellar.yaml. - Cohort vocabulary — distinct
cell_type+conditionvalues discovered fromcells_vso the model uses the names that actually exist in this atlas. - 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_genesruns live EnrichR. Use it after a DE pull to turn a top-gene set into pathways."
- DE — "DE comparisons are precomputed. Discover comparison_ids
with
- 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:
- 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", ...}]}
- 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}, ...]}
- 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":"...", ...}]}
- Turn 4 — answer. Claude streams the synthesised answer back as
tokenevents; the SSE stream ends withdone.
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:
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 reasonerror— 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.