genomic-intelligence
Predicts regulatory features, gene structure, and expression directly from DNA sequence using Genomic Intelligence's hosted transformer DNA language models — no local GPU or model weights. Six tasks over a REST API and a hosted MCP server (keyless public demo): promoter regions, splice donor/acceptor sites, enhancer activity, chromatin state, sequence-to-expression (log TPM), and de-novo gene…
npx degit K-Dense-AI/claude-scientific-skills/skills/genomic-intelligence .claude/skills/genomic-intelligenceSKILL.md
# Genomic Intelligence — DNA Sequence Models
Genomic Intelligence (GI) serves transformer DNA language models over six
sequence-analysis tasks on managed GPUs. Give it a **gene symbol**, a **genomic
region**, or a **DNA/FASTA sequence**; it returns structured predictions —
promoter regions, splice sites, enhancer activity, chromatin state, expression
(log TPM), and de-novo gene annotation. Nothing runs locally: no model weights,
no GPU, no heavy Python stack. It is a thin client over a hosted, versioned
inference API.
**Official docs:** [docs.genomicintelligence.ai](https://docs.genomicintelligence.ai) ·
REST contract at [api.genomicintelligence.ai/v1/openapi.json](https://api.genomicintelligence.ai/v1/openapi.json) ·
hosted MCP server at `https://mcp.genomicintelligence.ai/mcp`
## When to use this skill
Use GI when the user has DNA and wants a model prediction:
- **Find promoters** in a genomic region (`promoter`)
- **Predict splice** donor/acceptor sites (`splice`)
- **Score enhancer activity** — developmental & housekeeping (`enhancer`)
- **Annotate chromatin state** across hundreds of tracks (`chromatin`)
- **Predict expression** as log(TPM+1) from a sequence + cell-type context (`expression`)
- **Annotate genes/transcripts** de novo, no reference needed (`annotation`)
- **Find the genes in a region and predict each one's expression** (composite)
Not for local alignment, variant calling, or file I/O — use a local tool
(BioPython, bcftools) for those. GI is for **model inference from sequence**.
> Research and development use. Not for clinical or diagnostic decisions.
## Two ways to call GI
### Hosted MCP server (keyless; preferred on MCP hosts)
GI hosts an MCP server at `https://mcp.genomicintelligence.ai/mcp` (Streamable
HTTP). When your agent host supports MCP, prefer it: it works **keyless** against
a rate- and concurrency-limited public demo tier, and an optional `gi_` bearer
key raises those limits. It exposes acquisition tools that return a **sequence handle**
(`sequence_ref`) and `predict_*` tools that take that handle, so large sequences
stay out of the context. See [MCP workflow](#mcp-workflow-handle-based) below and
`references/mcp.md`.
### REST API (universal)
Plain HTTP with `requests` against `https://api.genomicintelligence.ai/v1`. The
REST path **requires** a `GI_API_KEY` (a `gi_` bearer). Use it on any host, in
scripts, or when you need the raw envelope. See [Core REST workflow](#core-rest-workflow).
## Access and authentication
1. The **hosted MCP demo is keyless** — try it with nothing set.
2. REST prediction and job operations need a key, sent as `Authorization: Bearer <key>`.
Public `GET /v1/tasks/{task}/models` discovery needs no key and is rate-limited
by source IP; inspect model windows and bounds before requesting access.
See the [current authentication contract](https://docs.genomicintelligence.ai/).
Request a prediction key at [contact@genomicintelligence.ai](mailto:contact@genomicintelligence.ai).
3. **Never hardcode the key.** Read it from the `GI_API_KEY` environment variable
(or a `.env` via `python-dotenv`). Never commit keys.
```bash
export GI_API_KEY="gi_yourkeyhere" # optional for MCP; required for REST
export GI_BASE_URL="https://api.genomicintelligence.ai" # override for staging
```
Keys are scoped to a partner tier with concurrency and per-minute caps. A `429`
means you hit a cap — back off and retry, or ask GI to raise your tier.
## The six tasks
Each task is **its own published operation** with its own request schema, its own
minimum length, and its own closed `options` object — `POST
/v1/tasks/promoter/predict`, `/v1/tasks/splice/predict`,
`/v1/tasks/enhancer/predict`, `/v1/tasks/chromatin/predict`,
`/v1/tasks/annotation/predict`, `/v1/tasks/expression/predict`. Each path is a
literal string, so nothing needs to be constructed, and there is no shared
`PredictRequest` schema. Body is `{sequence, sequence_name?, model?,
options?}`, returning a `{data, meta}` envelope. What differs per task:
| Task | Recommended mode | Accepted length | `context_window_bp` | Notes |
|---|---|---|---|---|
| `promoter` | sync | 300–500,000 bp | 2,000 bp | sliding-window promoter regions |
| `splice` | sync | 100–500,000 bp | 15,000 bp | donor/acceptor sites (long-context BigBird); strand-specific — feed transcript orientation |
| `enhancer` | sync | 50–500,000 bp | 249 bp | dev + housekeeping scores (DeepSTARR, *Drosophila*) |
| `chromatin` | sync | 200–500,000 bp | 1,000 bp | hundreds of tracks (DeepSEA) |
| `expression` | sync | **9,198–500,000 bp** | n/a (`trained_window_bp` 9,198) | log(TPM+1); needs `tss_index` unless exactly 9,198 bp, plus a cell-type `description` |
| `annotation` | async | 1,000–500,000 bp | n/a | de-novo transcripts; submit + poll; sync JSON above 200,000 bp is `413 sync_too_large` |
`Recommended mode` is guidance, not a constraint — every task accepts both. Omit `Prefer` for a synchronous `200`; send `Prefer: respond-async` for a `202` plus `GET /v1/tasks/jobs/{job_id}`. The one enforced limit is per operation: where `/v1/openapi.json` publishes `x-sync-limit-bp` on a `POST`, a synchronous JSON request above that length is `413 sync_too_large` — 200,000 bp on `annotation` and 50,000 bp on the composite workflow in contract revision 16. Read the field rather than memorising the numbers. Annotation BED/GFF3 stays synchronous at any admitted length and can time out; the other five predict tasks have no hard sync cap.
**The minimum is admission control, not regime.** A request above the floor but
shorter than the selected model's `bio_spec.context_window_bp` is *accepted and
scored* — against a window padded out to the context window. Enhancer is the
sharp case: the floor is 50 bp but the context window is 249 bp, so 50–248 bp is
scored mostly on padding. Compare your length against
`context_window_bp` from `GET /v1/tasks/{task}/models` to know whether the model
saw real sequence. Longer-than-context input is fine — the scanner steps a
prediction window at a time and pads only the final partial window.
Under the floor and over the 500,000 bp cap are **both `422 validation_failed`**
at `loc ["body","sequence"]`; over-length is *not* a `413`. All lengths are
measured after whitespace is stripped, so a line-wrapped FASTA body can be pasted
verbatim (a `>` header line still fails the alphabet check).
`options` is typed and **closed** (`additionalProperties: false`) per task — an
unknown key is a hard `422 validation_failed` with `type: "extra_forbidden"`,
never ignored:
| Task | `options` keys |
|---|---|
| promoter | `threshold` (0–1, default 0.5) |
| splice | `threshold` (0–1, default 0.5), `site_types` (subset of `["donor","acceptor"]`, default both) |
| enhancer | *(none)* |
| chromatin | `threshold` (0–1, default 0.5) |
| annotation | `batch_size` (1–128, default 8), `shift_coordinates`, `reverse_complement` (default true) |
| expression | `description` — **required**, and the only key |
`Prefer: respond-async` is a declared header on **all six** predict operations
and on the composite, not just `annotation` — see [Async](#async-any-task-recommended-for-annotation).
**Omit `model` and the API uses the task's default** — that is the recommended
call. Default model IDs are intentionally **not** documented here: defaults
change and retired IDs fail hard, so never hardcode one. To pin a model, or to
pick a non-human one (Drosophila, yeast, and Arabidopsis models exist for several
tasks), discover IDs at call time with `GET /v1/tasks/{task}/models` (REST) or
`list_models` (MCP) — and **never invent one**. Full per-task output shapes are
in `references/tasks.md`.
`expression` is the strictest of the six: alone among them its schema requires
`options` as well as `sequence`. Three hard rules it enforces — every violation
is a `422`, nothing is padded or clamped, and there is no opt-out flag, header,
or query parameter:
- **It always scores exactly one 9,198 bp TSS-centred window** —
`sequence[tss_index-4599 : tss_index+4599]`. The endpoint itself accepts
**9,198–500,000 bp**; anything below 9,198 bp is rejected outright.
- **`tss_index` is required unless the sequence is exactly 9,198 bp.** It is the
0-based TSS offset into the **whitespace-stripped** sequence, bounded by
`4599 ≤ tss_index ≤ len(sequence) − 4599`. At exactly 9,198 bp it defaults to
4,599, the only legal value there. So you may submit a whole locus (up to
500 kb) and let the server cut the window — but the server does **not**
discover the TSS for you (that is the composite workflow's job), and does
**not** reverse-complement: submit gene-sense sequence.
- **`options.description`** — a cell-type / assay string (e.g. `"K562 cells"`) —
is required, and is the **only** key `expression` accepts inside `options`.
Unknown top-level body fields are rejected too.
> Note: the legal `tss_index` range is wide, so an offset that is merely
> *wrong* (counted over raw FASTA characters including newlines, or relative to
> a locus start rather than the submitted slice) does not error — it returns a
> confident `200` for the wrong window. Assert on
> `meta.task_specific_counts.scored_window` / `.tss_index` in the response.
> The submitted length is `meta.sequence_length`; the scored width is always
> 9,198, i.e. `scored_window[1] - scored_window[0]`. In revision 16,
> `data.input` contains only `sequence_name`, `description`, and `tss_index`;
> it does not contain the submitted length or scored window.
>
> Both `tss_index` violations — "required unless exactly 9,198 bp" and the range
> check — come from a whole-model validator, so they surface at the body level
> rather than under `tss_index`. Match on `error.code == "validation_failed"`
> and use the message for display only. Any `loc` tuple quoted in this skill is
> illustrative of that shape, not part of the contract: it is not published in
> the schema and must not be branched on.
## Sequence acquisition
You rarely start from a raw 9,198 bp string. Acquire sequence first:
- **From a gene symbol** → MCP `fetch_ensembl_sequence(gene=...)`; **from
coordinates** → `fetch_region(region=...)`. Both acquire public reference sequence (no key), using a bundled coordinate
catalog, cache, UCSC, or Ensembl; retain the returned provenance. REST users can query Ensembl REST directly. (`find_genes` is
the annotation task, not an acquisition tool.)
- **For `expression`** → use the TSS-centred fetch so the window is exactly
9,198 bp. MCP: `fetch_gene_for_expression` (handles the centring). Otherwise
fetch a wider locus and pass the TSS as `tss_index` so the server cuts the
window — but compute that offset on the stripped nucleotide string, not on
file characters.
- **From a local FASTA** → MCP `store_inline_sequence`, or read the file yourself
for REST. (`load_local_fasta` exists only in local deployments, not on the
hosted server.)
- **A demo sequence** → MCP `load_demo_sequence(name=...)` returns a ready handle
for a keyless smoke test; `name` is required.
See `references/sequence-acquisition.md` for the exact Ensembl calls and the
expression-window math.
## Core REST workflow
The following transport recipe was tested with mocked responses, not authenticated
inference. Supply a task-appropriate `seq` before calling it. Use the exact
expression-context wording consistently when comparing predictions.
```python
import os
import time
import requests
BASE = os.environ.get("GI_BASE_URL", "https://api.genomicintelligence.ai").rstrip("/")
HEADERS = {"Authorization": f"Bearer {os.environ['GI_API_KEY']}"}
TASKS = {"promoter", "splice", "enhancer", "chromatin", "annotation", "expression"}
def predict(task, sequence, sequence_name, model=None, options=None, tss_index=None):
if task not in TASKS:
raise ValueError("Unknown GI task")
body = {"sequence": sequence, "sequence_name": sequence_name}
if model is not None:
body["model"] = model
if options is not None:
body["options"] = options
if tss_index is not None:
if task != "expression":
raise ValueError("tss_index is expression-only")
body["tss_index"] = tss_index
r = requests.post(f"{BASE}/v1/tasks/{task}/predict", headers=HEADERS,
json=body, timeout=(10, 300))
r.raise_for_status()
if r.status_code != 200:
raise RuntimeError(f"Unexpected prediction status {r.status_code}")
return r.json()
# After acquiring and checking an appropriate promoter sequence:
# out = predict("promoter", seq, "TP53_region")
# print(out["meta"]["task_specific_counts"]["regions_found"])
# A validated gene-sense expression window, or longer locus with known TSS:
# out = predict("expression", locus_seq, "HBB", tss_index=tss_offset,
# options={"description": "polyA plus RNA-seq; Homo sapiens K562"})
# assert out["meta"]["sequence_length"] == len("".join(locus_seq.split()))
# assert out["meta"]["task_specific_counts"]["scored_window"] == [tss_offset-4599, tss_offset+4599]
# print(out["data"]["prediction"]["expression_log_tpm"])
```
`data.summary` is for display: its keys may change without a contract revision.
Use the declared fields in `data` and `meta.task_specific_counts` for computation.
A timeout or proxy error may have a non-JSON body; it does not establish that the
inference never ran. Preserve the request ID and avoid blind POST resubmission.
### Async (any task; recommended for annotation)
Send `Prefer: respond-async` on any of the six tasks or the composite. A `202`
is `{data: {job_id, status: "accepted", links}, meta}`. `Content-Location` and
`X-Job-Id` identify the same job. Async is JSON-only; text format plus async is
`400`. Save the job ID before polling. This bounded polling example surfaces
HTTP failures (including `429` and `410`) for the caller to handle:
```python
def submit_annotation(sequence, sequence_name):
r = requests.post(f"{BASE}/v1/tasks/annotation/predict",
headers={**HEADERS, "Prefer": "respond-async"},
json={"sequence": sequence, "sequence_name": sequence_name},
timeout=(10, 30))
r.raise_for_status()
if r.status_code != 202:
raise RuntimeError(f"Unexpected submission status {r.status_code}")
return r.json()["data"]["job_id"]
def wait_for_job(job_id, max_polls=120):
if max_polls < 1:
raise ValueError("max_polls must be positive")
for attempt in range(max_polls):
r = requests.get(f"{BASE}/v1/tasks/jobs/{job_id}", headers=HEADERS,
timeout=(10, 30))
r.raise_for_status() # failed job -> its underlying 4xx/5xx, not 200
if r.status_code == 200:
return r.json()
if r.status_code != 202:
raise RuntimeError(f"Unexpected polling status {r.status_code}")
if attempt + 1 < max_polls:
time.sleep(5)
raise TimeoutError(f"Polling stopped; resume this job rather than resubmit: {job_id}")
# job_id = submit_annotation(seq, "TP53_region") # persist this ID
# result = wait_for_job(job_id)
# assert result["data"]["task"] == "annotation"
# transcripts = result["data"]["transcripts"]
```
`200` is completion; `202` contains `data.status` and `data.progress`.
Unknown/not-owned jobs are `404`; expired jobs are `410 job_expired`.
Results are documented as retained 24 hours from last activity; save results
locally. Job listing is a recent, bounded list, not a paginated archive.
## MCP workflow (handle-based)
On an MCP host, acquire a handle, then predict against it — sequences stay out of
the context:
```
# 1. Acquire a sequence handle (each returns data.ref, passed as sequence_ref):
load_demo_sequence(name="promoter_tp53") # keyless smoke test; name is required
fetch_ensembl_sequence(gene="TP53", flank_bp=5000) # include regulatory context
fetch_region(region="chr11:5,225,000-5,235,000") # coordinates -> handle
fetch_gene_for_expression(gene="HBB") # TSS-centred 9,198 bp handle for expression
# 2. Predict against the handle:
predict_promoter(sequence_ref=<ref>)
predict_expression(sequence_ref=<ref>, description="K562 cells")
predict_splice(sequence_ref=<ref>) # + predict_enhancer / predict_chromatin
# 3. Annotation on MCP is `find_genes` (there is no predict_annotation).
# It takes a handle, not a region, and runs async internally:
find_genes(sequence_ref=<ref>) # wait=True (default) returns the result
find_genes(sequence_ref=<ref>, wait=False) # own key only -> job_id; poll get_job(job_id)
# Acquisition returns data.ref; use that value as sequence_ref.
# Discover models with list_models(task); reference context lives in the
# gi://models, gi://docs/tasks, and gi://account MCP resources.
```
The shared demo disables `get_job`, `list_jobs`, and detached `wait=False`.
Keep `wait=True` there; a wait timeout is an error, not a recoverable job handle.
See [MCP details](references/mcp.md) for resources, result envelopes and lifetimes.
## Composite: find genes, then predict expression
To answer "what genes are in this region and how are they expressed?", use the
composite:
- **MCP:** `find_genes_and_predict_expression(sequence_ref=..., description=...)`
— takes a **handle, not a region** (acquire one with `fetch_region` first);
`description` is required. Finds genes in the sequence and returns an
expression prediction for each.
- **REST:** one call — `POST /v1/workflows/find-genes-and-predict-expression`,
body `{sequence, options}` with `sequence` 1,000–500,000 bp and
`options.description` (cell type / assay) required; a missing or empty
description is a `422 validation_failed`. It annotates, centres a 9,198 bp
window on each discovered gene's TSS (padding with `N` up to half the window
rather than dropping an edge gene), and returns a prediction per gene.
`meta.task_specific_counts` = `{genes_found, genes_predicted, genes_skipped}`
with `genes_predicted + genes_skipped == genes_found`; per-gene causes in
`data.expression_predictions[].skip_reason`. Above **50,000 bp** (its `x-sync-limit-bp`) it forces
async: a synchronous request over that size is `413 sync_too_large` with
`error.details = {sequence_length, threshold}` — retry the same body with
`Prefer: respond-async`.
The API also publishes a separate, under-development VCF workflow. Its outputs
are not established model results when `meta.model` is absent; see
[the bounded contract note](references/api-and-auth.md#vcf-workflow-boundary).
## Errors
| Code | `error.code` | Meaning | Action |
|---|---|---|---|
| 400 | `bad_request` | Malformed request | Check the body shape |
| 401 / 403 | `unauthorized` / `forbidden` | Missing/invalid key (REST) | Set `GI_API_KEY`; or use the keyless MCP demo |
| 404 | `not_found` | **Unknown task** (`/v1/tasks/bogus/predict`) or unknown job | Check the task name — an unrecognised task is a 404, not a 422 |
| 413 | `payload_too_large` | Raw request body over **16 MiB** | Split the input — this is the body cap, not the sequence cap |
| 410 | `job_expired` | Result retention elapsed | Recover saved results or deliberately submit new work |
| 413 | `sync_too_large` | Synchronous JSON request above the operation's `x-sync-limit-bp` (200,000 bp on `annotation`, 50,000 bp on the composite) | Retry with `Prefer: respond-async` |
| 415 | `unsupported_format` | Unsupported `format` query value | Use a format the task supports; there is no silent fallback to JSON |
| 422 | `validation_failed` | The most common failure: sequence **under the task floor or over 500,000 bp**, expression below 9,198 bp, a missing/out-of-range `tss_index`, a missing `options.description`, or **any unknown body or `options` key**; also the splice response cap | Read the message; fix the body |
| 429 | `rate_limited` / `too_many_requests` | Rate / concurrency cap | Back off (honour `Retry-After`); ask GI to raise your tierShown under the source repo’s MIT license. Read the full folder on GitHub.