Instructions to use HuggingFaceBio/Carbon-X with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use HuggingFaceBio/Carbon-X with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="HuggingFaceBio/Carbon-X") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("HuggingFaceBio/Carbon-X") model = AutoModelForCausalLM.from_pretrained("HuggingFaceBio/Carbon-X", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use HuggingFaceBio/Carbon-X with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "HuggingFaceBio/Carbon-X" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "HuggingFaceBio/Carbon-X", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/HuggingFaceBio/Carbon-X
- SGLang
How to use HuggingFaceBio/Carbon-X with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "HuggingFaceBio/Carbon-X" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "HuggingFaceBio/Carbon-X", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "HuggingFaceBio/Carbon-X" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "HuggingFaceBio/Carbon-X", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use HuggingFaceBio/Carbon-X with Docker Model Runner:
docker model run hf.co/HuggingFaceBio/Carbon-X
Model card: move the agent-tool section before Limitations; add examples and the carbon-dna skill
Browse filesFollow-up to #1, which was merged before these changes:
- Moves **Using Carbon-X as a tool for an agent** before Limitations (and in the table of contents).
- Keeps only the short `ask_carbon` function inline and links ready-to-run examples instead of inlining them.
- Removes the A/C/G/T tip.
New files:
- `examples/agent_tool.py`: an OpenAI-style tool that passes DNA by reference, plus a minimal agent loop, for any chat model with tool calling.
- `examples/inspect_agent.py`: an Inspect `@tool` and `react()` agent, with each sample's DNA kept in metadata.
- `skills/carbon-dna/`: a skill for Claude Code and Codex CLI. `SKILL.md` plus a standard-library script with `ask` and `compare`, configured with `CARBON_BASE_URL`.
- README.md +41 -79
- examples/agent_tool.py +65 -0
- examples/inspect_agent.py +67 -0
- skills/carbon-dna/SKILL.md +42 -0
- skills/carbon-dna/scripts/carbon.py +105 -0
|
@@ -26,10 +26,10 @@ An open 14B language model for DNA understanding and generation, from the **Carb
|
|
| 26 |
2. [How to use](#how-to-use)
|
| 27 |
3. [Evaluation](#evaluation)
|
| 28 |
4. [Training](#training)
|
| 29 |
-
5. [
|
| 30 |
-
6. [
|
| 31 |
-
7. [
|
| 32 |
-
8. [
|
| 33 |
|
| 34 |
## Model Summary
|
| 35 |
|
|
@@ -228,31 +228,10 @@ Both rounds use 16 samples per prompt, completions of up to 4,096 tokens, a lear
|
|
| 228 |
|
| 229 |
- **Post-training:** [TRL](https://github.com/huggingface/trl) for supervised fine-tuning and asynchronous GRPO, with [vLLM](https://github.com/vllm-project/vllm) for generation.
|
| 230 |
|
| 231 |
-
## Limitations
|
| 232 |
-
|
| 233 |
-
Carbon-X is a starting point for research on connecting DNA and language. It has **not been clinically validated**, and it should not be used to make medical decisions.
|
| 234 |
-
|
| 235 |
-
- ⚠️ **Genetic data is highly sensitive.** Depending on how you use this model (local download, inference API or endpoints, third-party inference providers, Spaces demos or others), input and output data may be processed or handled differently by different providers or Space owners. Make sure you understand and agree with how your data is handled before using the model.
|
| 236 |
-
- **Exact sequence operations.** The model can miscount bases, confuse coordinates or make incorrect edits, particularly when it locates exact positions in long sequences. For tasks that need exact operations, pair it with tools that perform them.
|
| 237 |
-
- **Missing context and hallucinations.** The model may invent a sequence's identity or function when the prompt does not give enough information. Include known annotations and context where available, and verify its biological claims.
|
| 238 |
-
- **Uneven task coverage.** Splice-site and histone-mark questions remain weak (29.8% and 34.6% on GenomeQA). Carbon-X is also less reliable on short DNA exercises and textbook biology than on its core variant-effect and sequence-recovery tasks.
|
| 239 |
-
- **Long sequence generation.** Quality degrades when the model is asked to generate long DNA sequences. Carbon-X reads and explains existing sequences. It was not trained to design new ones.
|
| 240 |
-
- **Long inputs.** Mid-training used 8,192-token sequences (up to ≈ 49 kbp of DNA), and post-training examples were shorter than 16k tokens. The model accepts up to 40,960 tokens, but we have not evaluated inputs much longer than those seen in training.
|
| 241 |
-
- **Thinking mode.** The chat template supports `enable_thinking=True`, but Carbon-X was post-trained and evaluated with thinking disabled.
|
| 242 |
-
|
| 243 |
-
## License
|
| 244 |
-
|
| 245 |
-
Apache 2.0.
|
| 246 |
-
|
| 247 |
-
## Acknowledgements
|
| 248 |
-
|
| 249 |
-
Carbon is a joint collaboration between the research teams at Hugging Face, Zhongguancun Academy, and TIGEM/University of Naples “Federico II”.
|
| 250 |
-
|
| 251 |
## Using Carbon-X as a tool for an agent
|
| 252 |
|
| 253 |
-
A general-purpose assistant can call Carbon-X when a question involves DNA. Serve Carbon-X with vLLM
|
| 254 |
-
|
| 255 |
-
**1. Wrap Carbon-X in a function.** The function takes a question and named DNA sequences, and returns Carbon-X's answer.
|
| 256 |
|
| 257 |
```python
|
| 258 |
from openai import OpenAI
|
|
@@ -273,63 +252,46 @@ def ask_carbon(question: str, sequences: dict[str, str], context: str = "") -> s
|
|
| 273 |
return response.choices[0].message.content
|
| 274 |
```
|
| 275 |
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
```python
|
| 279 |
-
import json
|
| 280 |
-
|
| 281 |
-
sequences = {"reference.fa": "ATGGTGCATCTGACTCCTGAGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGG",
|
| 282 |
-
"variant.fa": "ATGGTGCATCTGACTCCTGTGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGG"}
|
| 283 |
-
|
| 284 |
-
carbon_tool = {
|
| 285 |
-
"type": "function",
|
| 286 |
-
"function": {
|
| 287 |
-
"name": "ask_carbon",
|
| 288 |
-
"description": (
|
| 289 |
-
"Ask Carbon-X, a DNA language model, a question about DNA sequences: variant effects, "
|
| 290 |
-
"regulatory elements, organism of origin, sequence features. Refer to sequences by name; "
|
| 291 |
-
"do not paste DNA. Its answers are model predictions, not verified facts."
|
| 292 |
-
),
|
| 293 |
-
"parameters": {
|
| 294 |
-
"type": "object",
|
| 295 |
-
"properties": {
|
| 296 |
-
"question": {"type": "string", "description": "One self-contained question."},
|
| 297 |
-
"sequences": {"type": "array", "items": {"type": "string", "enum": list(sequences)}},
|
| 298 |
-
"context": {"type": "string", "description": "Known gene, coordinates or annotations, if any."},
|
| 299 |
-
},
|
| 300 |
-
"required": ["question", "sequences"],
|
| 301 |
-
},
|
| 302 |
-
},
|
| 303 |
-
}
|
| 304 |
-
```
|
| 305 |
-
|
| 306 |
-
**3. Run the agent loop.** Any OpenAI-compatible chat model with tool calling works as the agent.
|
| 307 |
|
| 308 |
-
```
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
]
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
message = reply.choices[0].message
|
| 317 |
-
messages.append(message)
|
| 318 |
-
if not message.tool_calls:
|
| 319 |
-
break
|
| 320 |
-
for call in message.tool_calls:
|
| 321 |
-
args = json.loads(call.function.arguments)
|
| 322 |
-
answer = ask_carbon(args["question"], {name: sequences[name] for name in args["sequences"]}, args.get("context", ""))
|
| 323 |
-
messages.append({"role": "tool", "tool_call_id": call.id, "content": answer})
|
| 324 |
-
print(message.content)
|
| 325 |
-
```
|
| 326 |
|
| 327 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
-
Tips for agent use
|
| 330 |
|
| 331 |
- **Pass DNA by reference.** Keep sequences in files or a lookup table and let the agent name them.
|
| 332 |
- **Ask for Carbon-X explicitly.** Agents often answer DNA questions on their own instead of calling a tool. If you want Carbon-X's view, say so in the system prompt.
|
| 333 |
-
- **Send only A, C, G and T.** Trim padding such as leading or trailing `N`s. Other characters become `<oov>` tokens.
|
| 334 |
- **Leave exact operations to code.** Counting, coordinates and edits are more reliable in Python than in either model. For variants, compute the position and the reference and alternate bases in code and pass them as `context`.
|
| 335 |
- **Cap generation length.** For next-base and continuation questions, set `max_tokens` to what you need and a client timeout, since long DNA generations are slow (see [Limitations](#limitations)).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
2. [How to use](#how-to-use)
|
| 27 |
3. [Evaluation](#evaluation)
|
| 28 |
4. [Training](#training)
|
| 29 |
+
5. [Using Carbon-X as a tool for an agent](#using-carbon-x-as-a-tool-for-an-agent)
|
| 30 |
+
6. [Limitations](#limitations)
|
| 31 |
+
7. [License](#license)
|
| 32 |
+
8. [Acknowledgements](#acknowledgements)
|
| 33 |
|
| 34 |
## Model Summary
|
| 35 |
|
|
|
|
| 228 |
|
| 229 |
- **Post-training:** [TRL](https://github.com/huggingface/trl) for supervised fine-tuning and asynchronous GRPO, with [vLLM](https://github.com/vllm-project/vllm) for generation.
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
## Using Carbon-X as a tool for an agent
|
| 232 |
|
| 233 |
+
A general-purpose assistant can call Carbon-X when a question involves DNA. Serve Carbon-X with vLLM
|
| 234 |
+
([Serving with vLLM](#serving-with-vllm)) and wrap it in a function that takes a question and named DNA sequences:
|
|
|
|
| 235 |
|
| 236 |
```python
|
| 237 |
from openai import OpenAI
|
|
|
|
| 252 |
return response.choices[0].message.content
|
| 253 |
```
|
| 254 |
|
| 255 |
+
Ready-to-run examples in this repository build on this function:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
+
- **Any agent with tool calling:** [`examples/agent_tool.py`](examples/agent_tool.py) describes `ask_carbon` as an
|
| 258 |
+
OpenAI-style tool and runs a minimal agent loop. The agent refers to sequences by name and your code looks up the DNA,
|
| 259 |
+
because language models often miscopy long sequences.
|
| 260 |
+
- **Inspect:** [`examples/inspect_agent.py`](examples/inspect_agent.py) wraps Carbon-X in an `@tool` for an
|
| 261 |
+
[Inspect](https://inspect.aisi.org.uk/) `react()` agent, with each sample's DNA kept in metadata.
|
| 262 |
+
- **Coding agents (Claude Code, Codex CLI):** [`skills/carbon-dna/`](skills/carbon-dna/) is a ready-made skill. Its
|
| 263 |
+
standard-library script has two commands: `ask` (a question about named DNA files) and `compare` (rank equal-length
|
| 264 |
+
variants by sequence likelihood).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
+
```bash
|
| 267 |
+
hf download HuggingFaceBio/Carbon-X --include "skills/*" --local-dir carbon-x
|
| 268 |
+
cp -r carbon-x/skills/carbon-dna ~/.claude/skills/ # Codex CLI: ~/.codex/skills/
|
| 269 |
+
export CARBON_BASE_URL=http://localhost:8000/v1 # your Carbon-X server
|
| 270 |
+
```
|
| 271 |
|
| 272 |
+
### Tips for agent use
|
| 273 |
|
| 274 |
- **Pass DNA by reference.** Keep sequences in files or a lookup table and let the agent name them.
|
| 275 |
- **Ask for Carbon-X explicitly.** Agents often answer DNA questions on their own instead of calling a tool. If you want Carbon-X's view, say so in the system prompt.
|
|
|
|
| 276 |
- **Leave exact operations to code.** Counting, coordinates and edits are more reliable in Python than in either model. For variants, compute the position and the reference and alternate bases in code and pass them as `context`.
|
| 277 |
- **Cap generation length.** For next-base and continuation questions, set `max_tokens` to what you need and a client timeout, since long DNA generations are slow (see [Limitations](#limitations)).
|
| 278 |
+
|
| 279 |
+
## Limitations
|
| 280 |
+
|
| 281 |
+
Carbon-X is a starting point for research on connecting DNA and language. It has **not been clinically validated**, and it should not be used to make medical decisions.
|
| 282 |
+
|
| 283 |
+
- ⚠️ **Genetic data is highly sensitive.** Depending on how you use this model (local download, inference API or endpoints, third-party inference providers, Spaces demos or others), input and output data may be processed or handled differently by different providers or Space owners. Make sure you understand and agree with how your data is handled before using the model.
|
| 284 |
+
- **Exact sequence operations.** The model can miscount bases, confuse coordinates or make incorrect edits, particularly when it locates exact positions in long sequences. For tasks that need exact operations, pair it with tools that perform them.
|
| 285 |
+
- **Missing context and hallucinations.** The model may invent a sequence's identity or function when the prompt does not give enough information. Include known annotations and context where available, and verify its biological claims.
|
| 286 |
+
- **Uneven task coverage.** Splice-site and histone-mark questions remain weak (29.8% and 34.6% on GenomeQA). Carbon-X is also less reliable on short DNA exercises and textbook biology than on its core variant-effect and sequence-recovery tasks.
|
| 287 |
+
- **Long sequence generation.** Quality degrades when the model is asked to generate long DNA sequences. Carbon-X reads and explains existing sequences. It was not trained to design new ones.
|
| 288 |
+
- **Long inputs.** Mid-training used 8,192-token sequences (up to ≈ 49 kbp of DNA), and post-training examples were shorter than 16k tokens. The model accepts up to 40,960 tokens, but we have not evaluated inputs much longer than those seen in training.
|
| 289 |
+
- **Thinking mode.** The chat template supports `enable_thinking=True`, but Carbon-X was post-trained and evaluated with thinking disabled.
|
| 290 |
+
|
| 291 |
+
## License
|
| 292 |
+
|
| 293 |
+
Apache 2.0.
|
| 294 |
+
|
| 295 |
+
## Acknowledgements
|
| 296 |
+
|
| 297 |
+
Carbon is a joint collaboration between the research teams at Hugging Face, Zhongguancun Academy, and TIGEM/University of Naples “Federico II”.
|
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Use Carbon-X as a tool for any chat model with OpenAI-style tool calling.
|
| 2 |
+
|
| 3 |
+
Serve Carbon-X first: vllm serve HuggingFaceBio/Carbon-X --trust-remote-code
|
| 4 |
+
Then set the agent client and model below and run: python agent_tool.py
|
| 5 |
+
"""
|
| 6 |
+
import json
|
| 7 |
+
|
| 8 |
+
from openai import OpenAI
|
| 9 |
+
|
| 10 |
+
carbon = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def ask_carbon(question: str, sequences: dict[str, str], context: str = "") -> str:
|
| 14 |
+
"""Ask Carbon-X a question about one or more named DNA sequences."""
|
| 15 |
+
parts = [context, question] if context else [question]
|
| 16 |
+
parts += [f"{name}: <dna>{seq}</dna>" for name, seq in sequences.items()]
|
| 17 |
+
response = carbon.chat.completions.create(
|
| 18 |
+
model="HuggingFaceBio/Carbon-X",
|
| 19 |
+
messages=[{"role": "user", "content": "\n\n".join(parts)}],
|
| 20 |
+
max_tokens=4096,
|
| 21 |
+
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
|
| 22 |
+
)
|
| 23 |
+
return response.choices[0].message.content
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
sequences = {"reference.fa": "ATGGTGCATCTGACTCCTGAGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGG",
|
| 27 |
+
"variant.fa": "ATGGTGCATCTGACTCCTGTGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGG"}
|
| 28 |
+
|
| 29 |
+
carbon_tool = {
|
| 30 |
+
"type": "function",
|
| 31 |
+
"function": {
|
| 32 |
+
"name": "ask_carbon",
|
| 33 |
+
"description": (
|
| 34 |
+
"Ask Carbon-X, a DNA language model, a question about DNA sequences: variant effects, "
|
| 35 |
+
"regulatory elements, organism of origin, sequence features. Refer to sequences by name; "
|
| 36 |
+
"do not paste DNA. Its answers are model predictions, not verified facts."
|
| 37 |
+
),
|
| 38 |
+
"parameters": {
|
| 39 |
+
"type": "object",
|
| 40 |
+
"properties": {
|
| 41 |
+
"question": {"type": "string", "description": "One self-contained question."},
|
| 42 |
+
"sequences": {"type": "array", "items": {"type": "string", "enum": list(sequences)}},
|
| 43 |
+
"context": {"type": "string", "description": "Known gene, coordinates or annotations, if any."},
|
| 44 |
+
},
|
| 45 |
+
"required": ["question", "sequences"],
|
| 46 |
+
},
|
| 47 |
+
},
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
agent = OpenAI() # the agent model's provider
|
| 51 |
+
messages = [
|
| 52 |
+
{"role": "system", "content": "For questions about DNA, consult Carbon-X with the ask_carbon tool before answering."},
|
| 53 |
+
{"role": "user", "content": "variant.fa differs from reference.fa by one base. Is the change likely benign or pathogenic?"},
|
| 54 |
+
]
|
| 55 |
+
while True:
|
| 56 |
+
reply = agent.chat.completions.create(model="<agent model>", messages=messages, tools=[carbon_tool])
|
| 57 |
+
message = reply.choices[0].message
|
| 58 |
+
messages.append(message)
|
| 59 |
+
if not message.tool_calls:
|
| 60 |
+
break
|
| 61 |
+
for call in message.tool_calls:
|
| 62 |
+
args = json.loads(call.function.arguments)
|
| 63 |
+
answer = ask_carbon(args["question"], {name: sequences[name] for name in args["sequences"]}, args.get("context", ""))
|
| 64 |
+
messages.append({"role": "tool", "tool_call_id": call.id, "content": answer})
|
| 65 |
+
print(message.content)
|
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Use Carbon-X as a tool for an Inspect react() agent.
|
| 2 |
+
|
| 3 |
+
Serve Carbon-X first: vllm serve HuggingFaceBio/Carbon-X --trust-remote-code
|
| 4 |
+
Then run: python inspect_agent.py
|
| 5 |
+
"""
|
| 6 |
+
from inspect_ai import Task, eval, task
|
| 7 |
+
from inspect_ai.agent import as_solver, react
|
| 8 |
+
from inspect_ai.dataset import Sample
|
| 9 |
+
from inspect_ai.scorer import includes
|
| 10 |
+
from inspect_ai.solver import solver
|
| 11 |
+
from inspect_ai.tool import ToolError, tool
|
| 12 |
+
from inspect_ai.util import store
|
| 13 |
+
from openai import AsyncOpenAI
|
| 14 |
+
|
| 15 |
+
carbon = AsyncOpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@tool
|
| 19 |
+
def ask_carbon():
|
| 20 |
+
async def execute(question: str, sequences: list[str], context: str = "") -> str:
|
| 21 |
+
"""Ask Carbon-X, a DNA language model, about this task's DNA sequences. Its answers are predictions.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
question: One self-contained question.
|
| 25 |
+
sequences: Names of the sequences to send, as given in the task.
|
| 26 |
+
context: Known facts, such as the gene, positions or alleles.
|
| 27 |
+
"""
|
| 28 |
+
dna = store().get("sequences")
|
| 29 |
+
if missing := [name for name in sequences if name not in dna]:
|
| 30 |
+
raise ToolError(f"Unknown sequence names: {missing}. Available: {list(dna)}")
|
| 31 |
+
parts = [context, question] if context else [question]
|
| 32 |
+
parts += [f"{name}: <dna>{dna[name]}</dna>" for name in sequences]
|
| 33 |
+
response = await carbon.chat.completions.create(
|
| 34 |
+
model="HuggingFaceBio/Carbon-X",
|
| 35 |
+
messages=[{"role": "user", "content": "\n\n".join(parts)}],
|
| 36 |
+
max_tokens=4096,
|
| 37 |
+
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
|
| 38 |
+
)
|
| 39 |
+
return response.choices[0].message.content
|
| 40 |
+
|
| 41 |
+
return execute
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@solver
|
| 45 |
+
def load_sequences():
|
| 46 |
+
async def solve(state, generate):
|
| 47 |
+
store().set("sequences", state.metadata["sequences"]) # the tool reads DNA from here
|
| 48 |
+
return state
|
| 49 |
+
|
| 50 |
+
return solve
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@task
|
| 54 |
+
def carbon_agent():
|
| 55 |
+
sample = Sample(
|
| 56 |
+
input="Sequence `variant` differs from `reference` at position 20 (A>T). Is the variant benign or pathogenic?",
|
| 57 |
+
target="pathogenic",
|
| 58 |
+
metadata={"sequences": {
|
| 59 |
+
"reference": "ATGGTGCATCTGACTCCTGAGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGG",
|
| 60 |
+
"variant": "ATGGTGCATCTGACTCCTGTGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGG",
|
| 61 |
+
}},
|
| 62 |
+
)
|
| 63 |
+
agent = react(prompt="Consult Carbon-X with the ask_carbon tool before you answer.", tools=[ask_carbon()])
|
| 64 |
+
return Task(dataset=[sample], solver=[load_sequences(), as_solver(agent)], scorer=includes())
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
eval(carbon_agent(), model="openai/gpt-5") # any Inspect model with tool calling as the agent
|
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: carbon-dna
|
| 3 |
+
description: "Consult Carbon-X, a DNA language model, about DNA sequences in files: variant effects, regulatory elements, organism of origin, sequence features, and likelihood comparisons between variants."
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Consult Carbon-X about DNA
|
| 7 |
+
|
| 8 |
+
Carbon-X is a DNA language model. Use it for biological interpretation of DNA alongside your own analysis. Its answers
|
| 9 |
+
are model predictions, not verified facts or clinical classifications.
|
| 10 |
+
|
| 11 |
+
`scripts/carbon.py` (next to this file) talks to a server running Carbon-X. It needs only Python 3.10+. The user
|
| 12 |
+
configures the server with `CARBON_BASE_URL` (default `http://localhost:8000/v1`), `CARBON_MODEL` and, if needed,
|
| 13 |
+
`CARBON_API_KEY`. Resolve the script's absolute path from this skill's directory.
|
| 14 |
+
|
| 15 |
+
Put each sequence in its own file (plain DNA or one FASTA record) and refer to files by name. Do not retype DNA into
|
| 16 |
+
commands: copying long sequences by hand introduces errors.
|
| 17 |
+
|
| 18 |
+
Ask a question about one or more sequences:
|
| 19 |
+
|
| 20 |
+
```sh
|
| 21 |
+
python3 /path/to/carbon-dna/scripts/carbon.py ask \
|
| 22 |
+
--sequence reference=reference.fa --sequence variant=variant.fa \
|
| 23 |
+
--question "variant differs from reference at one base. Is the change likely benign or pathogenic?" \
|
| 24 |
+
--context "Position 20 (1-based): A>T. Gene and coordinates, if known."
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
Rank equal-length variants by how typical Carbon-X finds each sequence (a likelihood, not a pathogenicity score):
|
| 28 |
+
|
| 29 |
+
```sh
|
| 30 |
+
python3 /path/to/carbon-dna/scripts/carbon.py compare --reference reference.fa --alternative variant=variant.fa
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
From Python, add `scripts/` to `sys.path` and call `ask(question, {name: dna}, context)` or
|
| 34 |
+
`compare(reference_dna, {name: dna})`.
|
| 35 |
+
|
| 36 |
+
Guidance:
|
| 37 |
+
|
| 38 |
+
- Work out exact facts in code first (positions, reference and alternate bases, reading frame, lengths) and pass them
|
| 39 |
+
as `--context`. Carbon-X can miscount positions in long sequences.
|
| 40 |
+
- Ask one self-contained question per call, and keep any answer choices from the user's question.
|
| 41 |
+
- Carbon-X reads A/C/G/T only. The script trims `N` padding at the ends and refuses other characters.
|
| 42 |
+
- Check `truncated` in the result; for next-base or continuation questions, ask for only the length you need.
|
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ask Carbon-X about DNA files, or compare DNA variants by model likelihood.
|
| 2 |
+
|
| 3 |
+
Talks to any OpenAI-compatible server running Carbon-X, such as `vllm serve HuggingFaceBio/Carbon-X --trust-remote-code`.
|
| 4 |
+
Configure it with environment variables:
|
| 5 |
+
CARBON_BASE_URL server URL including /v1 (default http://localhost:8000/v1)
|
| 6 |
+
CARBON_MODEL served model name (default HuggingFaceBio/Carbon-X)
|
| 7 |
+
CARBON_API_KEY bearer token, if the server needs one (for example an HF token for an Inference Endpoint)
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python carbon.py ask --question "..." --sequence reference=ref.fa --sequence variant=alt.fa [--context "..."]
|
| 11 |
+
python carbon.py compare --reference ref.fa --alternative alt1=alt1.fa [--alternative alt2=alt2.fa]
|
| 12 |
+
Only the Python standard library is needed. Output is JSON.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
import re
|
| 19 |
+
import sys
|
| 20 |
+
import urllib.request
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
BASE_URL = os.environ.get("CARBON_BASE_URL", "http://localhost:8000/v1").rstrip("/")
|
| 24 |
+
MODEL = os.environ.get("CARBON_MODEL", "HuggingFaceBio/Carbon-X")
|
| 25 |
+
API_KEY = os.environ.get("CARBON_API_KEY", "")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def read_dna(path: str) -> str:
|
| 29 |
+
"""Read plain DNA or one FASTA record; keep only A/C/G/T after trimming N padding at the ends."""
|
| 30 |
+
lines = [line.strip() for line in Path(path).read_text().splitlines() if line.strip()]
|
| 31 |
+
if lines and lines[0].startswith(">"):
|
| 32 |
+
lines = lines[1:]
|
| 33 |
+
if any(line.startswith(">") for line in lines):
|
| 34 |
+
sys.exit(f"{path}: one sequence per file, please (multi-record FASTA found).")
|
| 35 |
+
dna = "".join(lines).upper().strip("N")
|
| 36 |
+
if not re.fullmatch("[ACGT]+", dna):
|
| 37 |
+
sys.exit(f"{path}: Carbon-X reads A/C/G/T; remove other characters first.")
|
| 38 |
+
return dna
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def post(route: str, payload: dict) -> dict:
|
| 42 |
+
request = urllib.request.Request(f"{BASE_URL}/{route}", json.dumps({"model": MODEL, **payload}).encode(),
|
| 43 |
+
{"Content-Type": "application/json", **({"Authorization": f"Bearer {API_KEY}"} if API_KEY else {})})
|
| 44 |
+
with urllib.request.urlopen(request, timeout=600) as response:
|
| 45 |
+
return json.load(response)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def ask(question: str, sequences: dict[str, str], context: str = "", max_tokens: int = 4096) -> dict:
|
| 49 |
+
"""Ask a question about named DNA sequences. Returns Carbon-X's answer, a model prediction."""
|
| 50 |
+
parts = [context, question] if context else [question]
|
| 51 |
+
parts += [f"{name}: <dna>{dna}</dna>" for name, dna in sequences.items()]
|
| 52 |
+
response = post("chat/completions", {"messages": [{"role": "user", "content": "\n\n".join(parts)}],
|
| 53 |
+
"max_tokens": max_tokens, "chat_template_kwargs": {"enable_thinking": False}})
|
| 54 |
+
choice = response["choices"][0]
|
| 55 |
+
return {"answer": choice["message"]["content"], "truncated": choice["finish_reason"] == "length"}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def log_likelihood(dna: str) -> float:
|
| 59 |
+
"""Total log-likelihood (nats) of the DNA tokens of `<dna>{dna}</dna>` under Carbon-X."""
|
| 60 |
+
response = post("completions", {"prompt": f"<dna>{dna}</dna>", "max_tokens": 1, "temperature": 1.0,
|
| 61 |
+
"prompt_logprobs": 0, "add_special_tokens": False, "return_token_ids": True})
|
| 62 |
+
choice = response["choices"][0]
|
| 63 |
+
ids, logprobs = choice["prompt_token_ids"], choice["prompt_logprobs"]
|
| 64 |
+
n = (len(dna) + 5) // 6 # one token per 6 bases; a shorter tail gets its own token
|
| 65 |
+
if len(ids) != n + 2: # <dna>, n DNA tokens, </dna>
|
| 66 |
+
sys.exit("The server did not tokenize the DNA as expected; is it serving Carbon-X with --trust-remote-code?")
|
| 67 |
+
return sum(logprobs[i][str(ids[i])]["logprob"] for i in range(1, n + 1))
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def compare(reference: str, alternatives: dict[str, str]) -> dict:
|
| 71 |
+
"""Rank equal-length alternatives by log-likelihood relative to the reference (higher is more typical DNA).
|
| 72 |
+
|
| 73 |
+
This is sequence likelihood, not a calibrated pathogenicity score.
|
| 74 |
+
"""
|
| 75 |
+
if any(len(dna) != len(reference) for dna in alternatives.values()):
|
| 76 |
+
sys.exit("compare needs equal-length sequences; for insertions or deletions, use ask.")
|
| 77 |
+
base = log_likelihood(reference)
|
| 78 |
+
rows = [{"name": name, "delta_log_likelihood": log_likelihood(dna) - base,
|
| 79 |
+
"substitutions": sum(a != b for a, b in zip(reference, dna))} for name, dna in alternatives.items()]
|
| 80 |
+
return {"reference_log_likelihood": base, "alternatives": sorted(rows, key=lambda r: -r["delta_log_likelihood"])}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def named(items: list[str]) -> dict[str, str]:
|
| 84 |
+
pairs = [item.split("=", 1) for item in items]
|
| 85 |
+
if any(len(pair) != 2 for pair in pairs):
|
| 86 |
+
sys.exit("Use NAME=FILE for each sequence.")
|
| 87 |
+
return {name: read_dna(path) for name, path in pairs}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
if __name__ == "__main__":
|
| 91 |
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 92 |
+
sub = parser.add_subparsers(dest="command", required=True)
|
| 93 |
+
a = sub.add_parser("ask")
|
| 94 |
+
a.add_argument("--question", required=True)
|
| 95 |
+
a.add_argument("--sequence", action="append", default=[], metavar="NAME=FILE")
|
| 96 |
+
a.add_argument("--context", default="")
|
| 97 |
+
c = sub.add_parser("compare")
|
| 98 |
+
c.add_argument("--reference", required=True, metavar="FILE")
|
| 99 |
+
c.add_argument("--alternative", action="append", required=True, metavar="NAME=FILE")
|
| 100 |
+
args = parser.parse_args()
|
| 101 |
+
if args.command == "ask":
|
| 102 |
+
result = ask(args.question, named(args.sequence), args.context)
|
| 103 |
+
else:
|
| 104 |
+
result = compare(read_dna(args.reference), named(args.alternative))
|
| 105 |
+
print(json.dumps(result, indent=2))
|