danaaubakirova HF Staff commited on
Commit
b3f11f8
·
verified ·
1 Parent(s): 6702e01

Model card: move the agent-tool section before Limitations; add examples and the carbon-dna skill

Browse files

Follow-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 CHANGED
@@ -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. [Limitations](#limitations)
30
- 6. [License](#license)
31
- 7. [Acknowledgements](#acknowledgements)
32
- 8. [Using Carbon-X as a tool for an agent](#using-carbon-x-as-a-tool-for-an-agent)
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 as above, wrap it in a function, and give that function to any chat model that supports tool calling.
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
- **2. Describe it to the agent as a tool.** Let the agent refer to sequences by name, and have your code look up the DNA. Language models often miscopy long sequences, so the agent should never retype them.
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
- ```python
309
- agent = OpenAI() # the agent model's provider
310
- messages = [
311
- {"role": "system", "content": "For questions about DNA, consult Carbon-X with the ask_carbon tool before answering."},
312
- {"role": "user", "content": "variant.fa differs from reference.fa by one base. Is the change likely benign or pathogenic?"},
313
- ]
314
- while True:
315
- reply = agent.chat.completions.create(model="<agent model>", messages=messages, tools=[carbon_tool])
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
- Coding agents such as Claude Code or Codex CLI can use the same function as a skill: a short `SKILL.md` that says when to consult Carbon-X, plus a script that calls `ask_carbon` on sequence files in the workspace.
 
 
 
 
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”.
examples/agent_tool.py ADDED
@@ -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)
examples/inspect_agent.py ADDED
@@ -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
skills/carbon-dna/SKILL.md ADDED
@@ -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.
skills/carbon-dna/scripts/carbon.py ADDED
@@ -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))