Running it yourself
Every version you train is yours to keep: download it any time, during the trial, while you subscribe, or after. Run it on your own servers, a laptop, or offline, with the same request and answer format as the hosted endpoint.
curl -L -o bank-support@3.zip \
-H "Authorization: Bearer $CANONOPY_API_KEY" \
https://api.canonopylabs.com/v1/models/bank-support@3/download@latest works too. The zip holds three files, plus a short README.txt on how to run it:
| File | What's in it |
|---|---|
model.json | runtime settings: questions, options, state fields, the text reader it uses, and its safety bar |
heads.onnx | the trained model, in the open ONNX format (the runtime runs it with ONNX Runtime) |
rules.json | your rules, as a table of allowed and blocked actions |
The runtime
The runtime is the one package you install to run a model yourself; using the hosted API needs nothing installed. It's served from this site:
# models that read text (about 100 languages)
pip install "canonopy-runtime[text] @ https://canonopylabs.com/dl/canonopy_runtime-0.6.3-py3-none-any.whl"
# numbers and categories only
pip install https://canonopylabs.com/dl/canonopy_runtime-0.6.3-py3-none-any.whlfrom canonopy_runtime import load
model = load("bank-support@3.zip")
res = model.decide({"state": {"message": "I lost my card", "amount": 40, "fraud_flag": False}})
print(res["decision"]) # {'question': 'action', 'action': 'block-card', 'confidence': 0.98, 'blocked_by_rules': [], ...}From the command line:
python -m canonopy_runtime bank-support@3.zip request.jsonYour rules are enforced on every decision, exactly as on the hosted endpoint.
Models downloaded before September 26 (with weights.npz instead of heads.onnx) keep working with the same runtime.
Unsure messages, offline
The runtime runs the same two checks as the hosted endpoint: a message in a language the model isn't strong in, or below its safety bar, is double-checked. With CANONOPY_API_KEY set, it hands that message to us (translated to English and decided again, then your day-one backend). Offline, or when we can't be reached, it goes to your own fallback if you gave one; otherwise it is held for review ("route": "review"). It never guesses, unless you turn on best guess (below). load(path, service=None) never calls out. See Languages.
Best guess, for games
For games, turn on best guess: the model always acts, and tells you how sure it was.
model = load("snake@1.zip", best_guess=True) # or model.decide(request, best_guess=True) for one call
res = model.decide({"state": state})
res["decision"] # {'action': 'right', 'confidence': 0.62, ..., 'route': 'act', 'best_guess': True}In a game, "unsure" usually means two moves are about as good, not a real doubt. With best guess on, an answer below the safety bar acts on the model's most likely answer instead of being routed, and is marked "best_guess": true (on the answer and on the decision), with its confidence and probabilities as usual. A confident answer has no best_guess field, so you can always tell the two apart.
- Your rules still apply first: a blocked option is never the guess. When your rules allow no option at all, the decision still says
"action": null,"route": "ask". - Yes/no and score questions work the same way: the most likely answer, marked.
- A best guess doesn't call us, doesn't call your fallback, and isn't saved for sync.
- A message in a language the model isn't strong in is still routed as above: best guess covers "unsure", never "can't read this language".
- Off by default: business decisions keep routing unsure cases to a person.
model.decide(request, best_guess=False)turns it off for one call.
Your own fallback
Pass a function, and the runtime calls it for the questions it would have handed to us, whenever we can't be reached (or no key is set). When we can be reached, we answer first and your function isn't called.
from canonopy_runtime import load
def my_fallback(state, questions):
"""state: the case. questions: {question id: its definition}, only the ones that need a second look.
Return {question id: answer}, or None to hold them for review."""
if "action" in questions and state.get("amount", 0) > 500:
return {"action": "escalate"} # your own rule of thumb
return None
model = load("refunds@3.zip", offline_fallback=my_fallback)
res = model.decide({"state": {"message": "Mi pedido llegó roto", "amount": 900}})
res["answers"]["action"] # {'type': 'choice', 'choice': 'escalate', 'confidence': 1.0, ..., 'source': 'local_fallback', 'route': 'act'}- An answer is the usual shape (
{"type": "choice", "choice": ..., "confidence": ..., "probabilities": {...}}, a Score'sprobabilitiesorscore, a Noul'snoul), or just the value: an option label, a level number, ortrue/false(confidence 1). - It comes back marked
"source": "local_fallback". Your rules still apply after it: an action your rules block is never returned. - None, an error, or an answer that isn't one of the options means
"route": "review", as without a fallback. - One call can use its own:
model.decide(request, fallback=other_function).
A small local model works the same way:
def small_model(state, questions):
if "topic" not in questions:
return None
label, p = my_classifier.predict(state["message"]) # any model you run yourself
return {"topic": {"type": "choice", "choice": label, "confidence": p}} if p >= 0.9 else NoneSave and sync later
Every case that needed a second look while we couldn't be reached is saved on your machine, whether your fallback answered it or not, so a person can answer it later and your next version learns from it. The response then carries an id.
- Where:
offline-cases.jsonl(one JSON object per line: the case, the questions, the answers and theirsource, the time, the model version and theid), inCANONOPY_OFFLINE_DIR, else your user data folder:~/Library/Application Support/canonopy/offlineon macOS,%LOCALAPPDATA%\canonopy\offlineon Windows,~/.local/share/canonopy/offlineon Linux. It's your file: read it, copy it or delete it any time. - Bounded: the newest 10,000 cases or 50 MB, whichever comes first; past that, the oldest go.
- Off:
load(path, queue=None)(orCANONOPY_OFFLINE_QUEUE=off) saves nothing;queue="some/folder"picks the folder.
Sync when you're back online:
res = model.sync() # {'sent': 12, 'accepted': 12, 'already_there': 0, 'rejected': [], 'remaining': 0, 'decision_ids': {...}, ...}canonopy-runtime sync # or: canonopy sync (reads CANONOPY_API_KEY)
canonopy-runtime queue # where the file is, and how many cases waitIt also happens by itself, in the background, after the next call that reaches us (load(..., auto_sync=False) turns that off).
- Cases are sent in batches to
POST /v1/domains/{domain}/offline-casesand leave the file only once they are stored. Sending one twice is harmless. - They join your model as normal cases: the ones still held wait in the Unsure view, marked from an offline device. Our hosted backup may answer some of them first; the rest wait for you. Every answer is learned by the next version.
- Report outcomes on them with the same
id, once synced:POST /v1/outcomes {"decision_id": "loc_…", "action": "refund"}. - Syncing needs an active subscription or trial, like hosted decisions. After the trial it's refused (
402) and your cases stay in the file until you sync again.
Size and speed
A decision model is well under 1 MB; models that read text also use a shared text reader (reader-en-1, about 34 MB, or reader-multi-1, about 113 MB; fetched once and shared by every model; set CANONOPY_READER_PATH to a folder holding it to run fully offline). Their licences and attribution are in the THIRD_PARTY_NOTICES file that comes with the runtime. In the bank benchmark, on a laptop CPU, a decision took about 0.5 ms once the text was read and about 39 ms including reading the text.
To free the space, canonopy-runtime reader remove (runtime 0.6.2 and later) deletes every downloaded reader; a model that reads text downloads its reader again the next time it needs it. canonopy-runtime reader list shows what's there.
Godot
Godot 4 games use the Canonopy addon instead: no Python, no ONNX Runtime, the same decisions, from GDScript. See Use your model in Godot.
Phones and other runtimes coming soon
The runtime is Python and Godot today. Other runtimes follow.