Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# Changelog

## 1.1.0 — 2026-09-28

Full coverage of the current `/api/v1` (Agent platform epic, cognichem#458; covers the Python-client follow-up cognichem#269). Additive: existing 1.0 calls keep working.

### Added

- **Workflows** (`client.workflows`): `validate`, `estimate`, curated `templates` / `template`, catalog `node_ports`.
- `workflows.runs`: `create`, `list`, `get(include_spec=)`, `artifacts`, `resume`, `cancel`, `delete`, `wait`, `run`. `wait` stops on `completed | failed | cancelled | paused`.
- `workflows.definitions`: CRUD, JSON `download`, link `share` / `unshare`, `get_shared` / `fork_shared`, `versions` / `get_version` / `restore_version`.
- **Artifacts** (`client.artifacts`): `list` (filters, paging), `usage`, `upload` (`cognichem_zip` or `raw_file` multipart), `get` (manifest), `download` (whole zip or one `record=`), `delete`. `artifact_ref(id, port)` builds `$artifact` bindings.
- **Completion events** (`client.events`): `list` and a long-polling, de-duplicating `listen` generator.
- **Wallet** (`client.wallet.balance`), **science lookups** (`client.lookup`: PubChem, ChEMBL, Open Targets, UniProt, RCSB PDB, PubChem properties), and the **reference KG** (`client.reference`: `search`, `get`, `neighborhood`).
- **CogniChem Assistant chat** (`client.chat`, JWT only): sessions, message history, turn `estimate`, SSE `stream` / collected `send`, `stop`, thread `spend_cap` / `continue_spend_cap`, and run `proposals` (`list`, `get`, `approve`, `reject`).
- Jobs: `estimate`; `list` pagination and filters (`limit`, `offset`, `status`, `job_type`, `q`, `sort`) with full `items` rows; `status().result_artifact_id`.
- Inference / utils: `cancel`.
- API keys: `create(scopes=, expires_at=, spend_ceiling_usd=, allow_structure_search=)`, `update(name=, allow_structure_search=)`; new key metadata fields.
- `client.health()` / `client.ready()`.
- Errors: `BadRequestError` (400), `PaymentRequiredError` (402), `GoneError` (410), `PayloadTooLargeError` (413), `ServerError` (5xx); `retryability` attribute; `code` is now the problem type slug (e.g. `missing-idempotency-key`, `session-spend-cap`).
- Catalog constants: 45 submittable job types (was 23), utilities `molecule_standardize`, `druglikeness_filter`, `coolprop`, workflow run statuses.

### Fixed

- Mutating calls (`jobs.submit`, `submit_many`, `inference.submit*`, `utils.submit`, run / definition / upload writes) now send an auto-generated `Idempotency-Key` when none is given. The API rejects API-key mutations without one (400 `missing-idempotency-key`), so 1.0 submits failed with API keys.
- `api_keys.delete` returns `None`; it raised on the API's 204 response.

### Changed

- `cognichem_client.types` is now a package (same import path; every model is still importable from it).
- `scripts/sync_routes.py` checks keys **and paths** against backend `ROUTE_MAP` (`scripts/route_map.txt`).

## 1.0.0 — 2026-07-27

Clean-break redesign of the public SDK against the current CogniChem `/api/v1` compute API.
Expand Down
95 changes: 84 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ if status.status == "completed":
| Mode | How |
|------|-----|
| **API key** (recommended for scripts) | `CogniChem(api_key=...)` → `X-Api-Key` header |
| **JWT** (API key management) | `client.auth.login_email(email, password)` then `client.api_keys.*` |
| **JWT** (API key management, Assistant chat) | `client.auth.login_email(email, password)` or `CogniChem(access_token=...)` |

API keys carry `read` and/or `write` scopes. Submitting jobs, workflow runs, or uploads needs `write`. API-key mutations must send an `Idempotency-Key`; the client generates one per call unless you pass `idempotency_key=` (pass your own to make retries safe).

Environment variables:

Expand All @@ -67,28 +69,97 @@ Create and manage keys in your [CogniChem account](https://cognichem.com). Colle

## API surface

| Resource | Examples |
|----------|----------|
| `client.jobs` | `submit`, `submit_many`, `wait`, `run`, `result`, `result_many`, `info`, `list`, `cancel`, `delete` |
| `client.inference` | `submit`, `submit_batch`, `wait`, `run`, `result`, `list`, `delete` |
| Resource | Methods |
|----------|---------|
| `client.jobs` | `submit`, `submit_many`, `estimate`, `wait`, `run`, `result`, `result_many`, `info`, `status`, `list` (paginated, filters), `cancel`, `delete` |
| `client.workflows` | `validate`, `estimate`, `templates`, `template`, `node_ports` |
| `client.workflows.runs` | `create`, `list`, `get`, `artifacts`, `resume`, `cancel`, `delete`, `wait`, `run` |
| `client.workflows.definitions` | `create`, `list`, `get`, `update`, `delete`, `download`, `share`, `unshare`, `get_shared`, `fork_shared`, `versions`, `get_version`, `restore_version` |
| `client.artifacts` | `list`, `usage`, `upload`, `get`, `download` (whole zip or one `record=`), `delete` |
| `client.events` | `list`, `listen` (long-poll completion events for jobs and workflow runs) |
| `client.wallet` | `balance` |
| `client.lookup` | `pubchem`, `chembl`, `targets` (Open Targets), `uniprot`, `pdb`, `properties` |
| `client.reference` | `search`, `get`, `neighborhood` (literature / method papers) |
| `client.chat` (JWT) | `estimate`, `stream`, `send`, `stop`, `spend_cap`, `continue_spend_cap`, plus `sessions.*` and `proposals.*` |
| `client.inference` | `submit`, `submit_batch`, `wait`, `run`, `result`, `list`, `cancel`, `delete` |
| `client.inference.models.mpnn` | `public()`, `user(name=None)`, `delete(name)` |
| `client.utils` | `submit`, `wait`, `run`, `result`, `list`, `delete` |
| `client.utils` | `submit`, `wait`, `run`, `result`, `list`, `cancel`, `delete` |
| `client.usage` | `limits(month=None)` |
| `client.auth` / `client.api_keys` | login, check, refresh, logout; list/create/rotate/delete keys |
| `client.auth` / `client.api_keys` | login, check, refresh, logout; list/create/get/update/rename/rotate/delete keys |
| `client` | `health()`, `ready()` |

Async twin: `AsyncCogniChem` with the same method names (`await client.jobs.run(...)`, `async for event in client.chat.stream(...)`).

## Workflows

```python
from cognichem_client import CogniChem, artifact_ref

client = CogniChem.from_env()

spec = client.workflows.template("smiles-embed-dock").spec
target = client.artifacts.upload("target.pdb", data_kind="protein_structure", data_format="pdb")
params = {
"ligands": ["CCO", "c1ccccc1O"],
"target_structure": artifact_ref(target.id, "structure"),
}

assert client.workflows.validate(spec, params=params).valid
estimate = client.workflows.estimate(spec, params=params) # USD hold at your tier

run = client.workflows.runs.run("dock-demo", spec, params=params, max_run_cost=estimate.total * 1.5)
# run.status is "completed" (or "paused" if it hit max_run_cost; call runs.resume)
for item in client.workflows.runs.artifacts(run.id).items:
print(item.step_key, item.port, item.artifact_id, item.record_count)
```

Optional `idempotency_key=` on mutating POSTs (`jobs`, `inference`, `utils` submit).
`runs.wait` stops on `completed`, `failed`, `cancelled`, or `paused` (a paused run needs you to resume it). `runs.run` raises `ProcessFailedError` / `ProcessCancelledError` on failure.

Job and run outputs are durable **artifacts**: `client.jobs.status(pid).result_artifact_id` points at one. Inspect the manifest with `client.artifacts.get(...)` and fetch single records with `download(..., record=...)` instead of whole zips. Bind artifacts into workflow params or job payloads with `artifact_ref(id, port)`.

## Completion events

```python
for event in client.events.listen(kind="workflow_run"):
print(event.subject_id, event.status) # re-read the run for detail
```

## CogniChem Assistant (JWT)

```python
client = CogniChem(access_token=token)
session = client.chat.sessions.create("Docking plan")
reply = client.chat.send(session.id, "Dock aspirin against COX-1", reasoning_effort="low")
print(reply.content, reply.billed_usd)
for proposal in client.chat.proposals.list(session.id).items:
client.chat.proposals.approve(session.id, proposal.id, proposal.estimate_usd)
```

Async twin: `AsyncCogniChem` with the same method names (`await client.jobs.run(...)`).
Each turn holds wallet funds at your tier's token rate and bills the metered tokens. A thread at its spend cap raises `ConflictError` (`code == "session-spend-cap"`); `continue_spend_cap` raises the cap.

## Errors

API failures raise subclasses of `CogniChemError` parsed from RFC 7807 problem details (`detail`, `title`, `status`, `type`, `request_id`, `errors` on 422).
API failures raise subclasses of `CogniChemError` parsed from RFC 7807 problem details (`detail`, `title`, `status`, `type`, `request_id`, `retryability`, `errors`). `code` is the problem type slug (for example `missing-idempotency-key`, `insufficient-scope`, `spend-ceiling-exceeded`, `insufficient-wallet`, `session-spend-cap`, `estimate-changed`), and `body` holds any extra fields.

| Status | Exception |
|--------|-----------|
| 400 | `BadRequestError` |
| 401 | `AuthenticationError` |
| 402 | `PaymentRequiredError` |
| 403 | `ForbiddenError` |
| 404 | `NotFoundError` |
| 409 | `ConflictError` |
| 410 | `GoneError` |
| 413 | `PayloadTooLargeError` |
| 422 | `ValidationError` (WorkflowSpec diagnostics in `errors`) |
| 429 | `RateLimitError` |
| 5xx | `ServerError` |

Polling helpers raise `PollTimeoutError`, `ProcessFailedError`, or `ProcessCancelledError` when appropriate.

## Examples

See [`examples/`](examples/) for scripts covering utilities, inference, and job submission.
See [`examples/`](examples/) for scripts covering utilities, inference, job submission, workflow templates, artifact records, science lookups, and the Assistant.

Live OpenAPI docs: [https://api.cognichem.com/docs](https://api.cognichem.com/docs)

Expand All @@ -102,6 +173,8 @@ ruff check src tests
mypy
pytest
python scripts/sync_routes.py
# refresh the ROUTE_MAP snapshot from a monorepo checkout:
python scripts/sync_routes.py --backend-config ../cognichem/apps/back-end-api/src/core/config.py --write-snapshot
```

## Migrating from 0.1.x
Expand Down
45 changes: 45 additions & 0 deletions examples/artifact_records.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Inspect a job's result artifact and download one record instead of the zip."""

from __future__ import annotations

import os
import sys

from cognichem_client import CogniChem


def main(process_id: str) -> None:
client = CogniChem(api_key=os.environ["COGNICHEM_API_KEY"])

status = client.jobs.status(process_id)
if not status.result_artifact_id:
print("no stored artifact for", process_id, status.status)
return

artifact = client.artifacts.get(status.result_artifact_id)
for port, spec in artifact.manifest.get("ports", {}).items():
print(port, spec.get("kind"), spec.get("format"), spec.get("record_count"))

usage = client.artifacts.usage()
print(f"storage {usage.used_bytes}/{usage.quota_bytes} bytes")

ports = artifact.manifest.get("ports", {})
first = next(
(
(port, record["id"])
for port, spec in ports.items()
if port != "archive"
for record in spec.get("records", [])
),
None,
)
if first:
port, record_id = first
result = client.artifacts.download(
artifact.id, record=record_id, port=port, save_path="."
)
print("saved", result.filename, len(result.content), "bytes")


if __name__ == "__main__":
main(sys.argv[1])
36 changes: 36 additions & 0 deletions examples/assistant_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Ask the CogniChem Assistant a question and review any run proposals.

Chat is JWT-only: set ``COGNICHEM_ACCESS_TOKEN`` (or sign in on a client with
``client.auth.login_email(email, password)``, which stores the token).
"""

from __future__ import annotations

import os

from cognichem_client import CogniChem


def main() -> None:
client = CogniChem(access_token=os.environ["COGNICHEM_ACCESS_TOKEN"])

session = client.chat.sessions.create("Docking plan")
question = "How should I dock aspirin against COX-1? Estimate the cost."
quote = client.chat.estimate(session.id, question)
print(f"turn hold ${quote.hold_usd:.4f} ({quote.reasoning_effort})")

for event in client.chat.stream(session.id, question):
if event.event == "token":
print(event.data["delta"], end="", flush=True)
elif event.event == "done":
print(f"\nbilled ${event.data['billed_usd']:.4f}")

for proposal in client.chat.proposals.list(session.id).items:
print(proposal.id, proposal.kind, proposal.status, proposal.estimate_usd)
# To run it: client.chat.proposals.approve(
# session.id, proposal.id, proposal.estimate_usd
# )


if __name__ == "__main__":
main()
30 changes: 30 additions & 0 deletions examples/science_lookup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Resolve a compound, look up target activities, and find method papers."""

from __future__ import annotations

import os

from cognichem_client import CogniChem


def main() -> None:
client = CogniChem(api_key=os.environ["COGNICHEM_API_KEY"])

compounds = client.lookup.pubchem(["aspirin"])
for compound in compounds.compounds:
print(compound.name, compound.status, compound.cid, compound.smiles)

activities = client.lookup.chembl("target_activities", ["EGFR"], limit=5)
for row in activities.results:
print(row.status, row.record_id, row.title, row.url)
print(activities.attribution)

papers = client.reference.search("protonation state pKa", paper_set="method")
for paper in papers.papers:
print(paper.year, paper.title, paper.url)

print("wallet", client.wallet.balance())


if __name__ == "__main__":
main()
49 changes: 49 additions & 0 deletions examples/workflow_template_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Run a curated workflow template: upload a target, validate, estimate, run."""

from __future__ import annotations

import os
import sys
import uuid

from cognichem_client import CogniChem, artifact_ref


def main(pdb_path: str) -> None:
client = CogniChem(api_key=os.environ["COGNICHEM_API_KEY"])

for template in client.workflows.templates():
print(f"{template.id:32} {template.complexity:9} {template.summary}")
spec = client.workflows.template("smiles-embed-dock").spec

# Structure params take an $artifact ref, so upload the PDB first.
target = client.artifacts.upload(
pdb_path, data_kind="protein_structure", data_format="pdb"
)
port = next(iter(target.manifest.get("ports", {})), "structure")
params = {
"ligands": ["CCO", "c1ccccc1O"],
"target_structure": artifact_ref(target.id, port),
}

check = client.workflows.validate(spec, params=params)
if not check.valid:
for error in check.errors:
print("invalid:", error.code, error.message)
return
estimate = client.workflows.estimate(spec, params=params)
print(f"estimated hold ${estimate.total:.4f} at tier {estimate.tier}")

run = client.workflows.runs.run(
f"example-dock-{uuid.uuid4().hex[:8]}",
spec,
params=params,
max_run_cost=estimate.total * 1.5,
)
print("run", run.id, run.status, f"charged ${run.charged_amount:.4f}")
for item in client.workflows.runs.artifacts(run.id).items:
print(" ", item.step_key, item.port, item.artifact_id, item.record_count)


if __name__ == "__main__":
main(sys.argv[1])
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "cognichem-client"
version = "1.0.0"
version = "1.1.0"
description = "Python client for the CogniChem API"
authors = [
{ name = "Travis Kessler", email = "travis@cognichem.com" }
Expand Down
Loading
Loading