AmbertraceAI Python SDK
Python client for the Ambertrace neurosymbolic AI platform API.
Machine-readable OpenAPI spec: https://app.ambertrace.ai/api/v1/openapi.json
(public, unauthenticated; also served at /openapi.json and /openapi/spec).
Interactive API docs (ReDoc): https://app.ambertrace.ai/docs.
Agent/LLM orientation: see AGENTS.md for a one-page entry-point covering auth, the golden-path workflow, async jobs, error contracts, and API boundaries.
Capability index — what can I do, and which method produces it?
The one authoritative map from a CAPABILITY to the method that reaches it (and the
example that shows it). If you're unsure whether something is author(),
build_ontology, or query, read this first.
| Capability | Reach it via | Example |
|---|---|---|
| Verified query / decision (proof-carrying answer) | platforms.query(pid, query=..., facts=...) |
30, 38 |
| Certified search — prove a mechanism's universal property (e.g. strategy-proofness) over a finite space: exhaustive HOLDS, a certified counterexample, or an explicit ABSTAIN | platforms.verify_property(pid, property="strategy_proof", space={…, "bound": N}) — dedicated method, see Certified search |
69 |
| Certified regime-grid totality + exclusivity — for every cell of a grid you declare, EXACTLY ONE verdict derives from the platform's own decision rules (VIOLATED names the uncovered / double-covered cell) | platforms.verify_property(pid, property="exactly_one_verdict", space={"variables": [{"field": …, "domain": […]}, …], "bound": N}) — same dedicated method, see Certified search |
70 |
| N-class / multi-class classifier (decision = winning label) | domains.build_ontology → platforms.create(verified_profile=True) → platforms.query — NOT author() |
38 |
| Custom decision vocabulary (verbs beyond permit/deny) | phrase the build_ontology / author description with your verbs; read query().decision |
19, 24, 38 |
| Build a VERIFIED platform | platforms.create(verified_profile=True, verified_min_confidence=…, invariant_manifest=…) |
10, 11, 14 |
| Open-textured SCORED determination (an LLM-τ score for a judgment predicate deduction can't decide) | platforms.create(scored_determinations={…}) — the platform's own runtime LLM scores the predicate; admitted as a τ-gated fact (≥τ supports a permit; sub-τ / abstain / OOD → escalate). SERVER-computed, deductive-first, fail-closed |
41 |
| Cross-domain cueing / relational join inside the proof | build_ontology(relations=…) + platforms.query(relations=…) (existsRelated) |
31 |
| Prediction → Decision (verified forecast feeds a decision, by reference) | predictions.symbolic_forecast(verified=True, …) → platforms.query(predictions={role: {model_id, as_of}}) |
36, 37 |
| Prediction → Decision INTO the Agent Policy Gate | predictions.symbolic_forecast(verified=True) → agent_policy.authorize_action(predictions={role: {…}}) |
25, 28 |
| Agent Policy Gate — permit/deny an agent action (English policy) | agent_policy.author(text) → authorize_action / session step |
25, 27, 28 |
| APG temporal / sequencing (precedence, rate, pairing) | agent_policy.author("… preceded by …") + create_session + step |
40 |
| APG distinct-actor quorum + separation-of-duties | agent_policy.author("… two DIFFERENT approvers, none the author …") + authorize_action(relations=…) |
28 |
| Explainable symbolic forecasting + WHY | predictions.symbolic_forecast(...) (why / prediction_record) |
23, 26 |
| Neural-evidence retrieval breadth on a query | platforms.query(..., top_k=N) |
-- |
| Panel data-sufficiency BEFORE training (usable rows, binding-constraint column, stale/discontinued series) | datasets.panel_report(dataset_id) — also persisted at ingest on schema_info["panel_sufficiency"] |
44 |
| Org-capability discovery + 403 handling | GET /api/v1/capabilities (user-scoped key) + branch on AmbertraceError.code == "capability_disabled" |
42 |
Full method surface is in Resources below; the runnable demos are in examples/.
Install
pip install ambertraceai
Authentication
The SDK authenticates with an Ambertrace API key (prefix at_...). Create one from the
dashboard at app.ambertrace.ai → Settings → API Keys, then
pass it to the client:
from ambertraceai import AmbertraceAPI
api = AmbertraceAPI(base_url="https://app.ambertrace.ai", api_key="at_...")
Keep the key out of source control — read it from the environment. The SDK does
this for you: set AMBERTRACE_API_KEY (and optionally AMBERTRACE_BASE_URL,
which defaults to https://app.ambertrace.ai) and call from_env():
api = AmbertraceAPI.from_env() # reads AMBERTRACE_API_KEY / AMBERTRACE_BASE_URL
api = AmbertraceAPI.from_env(dotenv_path=".env") # also load a .env file (real env wins)
base_url / api_key are optional on the constructor too — when omitted they
fall back to those env vars (an explicit argument always wins):
api = AmbertraceAPI() # base_url + api_key from the environment
See Agent Keys for the user- vs. platform-scoped key model.
Quick Start
from ambertraceai import AmbertraceAPI
api = AmbertraceAPI(
base_url="https://app.ambertrace.ai",
api_key="at_...",
)
# Create a domain
domain = api.domains.create(
name="Legal Contracts",
description="Contract analysis for risk and compliance",
)
# Upload data. The returned dataset exposes its fields by attribute too
# (dataset.row_count, dataset.column_count, dataset.decision_column).
dataset = api.datasets.upload(
domain_id=domain["id"],
file_path="contracts.csv",
)
# Build the ontology from the domain + uploaded data (async — returns a job).
# This MUST run before building a platform: without an ontology the build fails
# server-side ("Domain has no entities. Define entities before building.").
onto = api.domains.build_ontology(domain_id=domain["id"])
api.wait_for_job(onto.job_id, timeout=600) # raises if the ontology build fails
# Build a platform (async — returns the platform and a build job). The result
# carries a normalised, stable `id` (the platform) and `job_id` (the build job),
# so you don't unwrap `build_job.job.id` / `platform.id` by hand.
result = api.platforms.create(
domain_id=domain["id"],
dataset_id=dataset["id"],
)
platform_id = result.id # == result["platform"]["id"]
build_job_id = result.job_id # == result["build_job"]["id"]
# Wait for the build to finish
job = api.wait_for_job(build_job_id, timeout=600)
# Query the platform
answer = api.platforms.query(
platform_id=platform_id,
query="What are the highest-risk clauses?",
)
print(answer["answer"])
print(answer["explanation"])
Convenience methods return an
AttrDict— adictthat also exposes its keys as attributes (result.id,dataset.row_count). Everyresult["..."]subscript,.get(),intest andjson.dumps()keeps working exactly as before; the attribute access is additive (a key colliding with adictmethod likeitemsstays reachable via subscript).
Resources
| Resource | Methods |
|---|---|
api.domains |
list, create, get, update, delete, build_ontology, eval_config, set_eval_config, delete_eval_config, suggest_eval_config, list_templates, create_template, update_template, delete_template, feedback_stats |
api.datasets |
list, get, upload (incl. decision_column), fetch, fetch_multi, quality, panel_report, clean, preview, delete |
api.platforms |
list, create, get, delete, status, query, verify_property, suggest_rules, list_suggestions, approve_suggestion, reject_suggestion, graph |
api.predictions |
predict, list_configs, create_config, delete_config, train, list_predictions, discover_prediction_rules, discovered_prediction_rules, neurosymbolic_comparison, symbolic_forecast, residual_diagnosis (preview) |
api.connectors |
list, test |
api.usage |
get |
api.jobs |
get |
api.api_keys |
list, create (optional expires_at), revoke, rotate (grace-window rotation) |
api.agent_policy (preview) |
author, status, examples, authorize_action, create_session, step, get_session |
Certified search — prove a property for EVERY case
platforms.query certifies one input. platforms.verify_property certifies a
mechanism universally — "for every profile of preferences and every
deviation, no agent profits from misreporting" — over a finite space you declare,
and returns exactly one of:
result |
certified |
Meaning |
|---|---|---|
HOLDS |
"exhaustive" |
The property holds for every member of the space, decided by a machine-checked enumeration. search.space_size is |S| (the product of your declared domain sizes) and search.complete is True. |
VIOLATED |
"witness" |
A certified counterexample: witness is a concrete profile + agent + misreport that profits, independently re-certified by the trusted kernel. |
ABSTAIN |
None |
No verdict, with a reason (over_bound, over_ceiling, timeout, unsupported_*). Your bound is smaller than the space, or the space is above the platform ceiling. ABSTAIN is never a pass. |
res = api.platforms.verify_property(
pid, property="strategy_proof",
space={"mechanism": "plurality", "agents": 3,
"domain": ["A", "B", "C"], "bound": 2000})
print(res.answer) # VIOLATED (certified: witness): counterexample {...}
print(res.witness) # {'pref_1': 'A>B>C', 'pref_2': 'B>A>C', 'pref_3': 'C>B>A',
# 'agent': 3, 'misreport': 'B'} -- agent 3 profits by voting B
property—"strategy_proof"or"exactly_one_verdict"(see below).space.mechanism—"plurality"(3–5 alternatives, ties break indomainorder),"majority"(2 alternatives, tie → first),"vickrey"(2 bidders, second-price) or"first_price"(2 bidders; a manipulable control).space.agents,space.domain(the alternatives, or the discrete bid grid of distinct non-negative ints, at most 10),space.bound(required: the largest |S| you accept).- |S| = every agent's type × which agent deviates × its misreport — e.g. 3-candidate plurality with 3 voters is (3!)³ × 3 × 3 = 1944; majority-of-2 with 4 voters is 2⁴ × 4 × 2 = 128.
result, certified, proof_checked, proof_summary and search are all computed
from ONE source and always name the same verdict; proof_checked is the authoritative
flag (false on every ABSTAIN). answer is a one-line rendering the SDK builds from
those same fields. The method needs the query capability (403 capability_disabled
otherwise); the platform you pass scopes access and audit, the mechanism is declared
in space. Reachable through this dedicated method only (it is not a query mode).
See example 69_verify_property_strategy_proof.py.
Regime-grid totality + exclusivity — property="exactly_one_verdict"
For a platform whose decision rules classify a regime grid (e.g. priced move ×
incentive sign × reaction function), exactly_one_verdict certifies that every cell
of the grid derives EXACTLY ONE verdict from the platform's own active decision
rules — no uncovered cell, no double-covered cell:
res = api.platforms.verify_property(
pid, property="exactly_one_verdict",
space={"variables": [{"field": "priced", "domain": ["neg", "zero", "pos"]},
{"field": "incentive", "domain": ["neg", "zero", "pos"]},
{"field": "reaction", "domain": ["hawk", "dove"]}],
"bound": 18})
print(res.answer) # HOLDS (certified: exhaustive): ... all 18 members ...
if res.result == "VIOLATED":
print(res.witness) # {'priced': 'neg', 'incentive': 'zero', 'reaction': 'hawk'}
print(res.witness_detail) # {'cell': {...}, 'verdicts': [], 'coverage': 'uncovered'}
space.variables— the grid: each axis is a field named in your rule conditions with its explicit finitedomain; |S| is the product of the domain sizes (3 × 3 × 2 = 18 above).space.boundis required, as for every property. Nomechanism/agents/domainhere.VIOLATEDreturns the first violating cell inwitness(witness_index= its position, first axis slowest) and, inwitness_detail, theverdictsthat derive there ([]= uncovered, two or more = double-covered) withcoverage="uncovered"|"double_covered".- A rule whose condition is not a scalar test (
eq/ne/gt/gte/lt/lte/in) on a declared grid field — an aggregate, temporal, graph or relational leaf, or a field you did not put in the grid — makes the callABSTAIN(condition_out_of_fragment/field_outside_grid), never a guess. Rules concluding the same verdict count as one verdict; inactive rules and non-verdict (derive/flag/require) rules are ignored. - Reachable only through
verify_property(it is not aquerymode). Example70_verify_property_regime_grid.py.
The search runs as an async server-side job (it drives external checker processes):
the server answers POST .../verify-property with 202 {"job_id", "poll", ...} and the
SDK polls GET /api/v1/jobs/{job_id} every poll_interval seconds (default 5) until the job
completes, so verify_property still reads as one blocking call that returns the verdict
(timeout=300 s; wait=False returns the raw 202 envelope for you to poll with
api.wait_for_job). A failed job raises AmbertraceError (never a verdict). Polling needs a
user-scoped key or a session — a platform-scoped key can start the job but not poll it.
Verified relational queries — cross-domain cueing (preview)
api.platforms.query takes an optional facts (the focal {field: scalar} row)
and an optional relations ({relation_name: [ {column: scalar}, ... ]}) of
attached related facts. On a verified platform the kernel folds those related rows
inside the proof — an aggregate (count/sum) or existential (existsRelated)
derive rule joins them on a declared join key and its derived flag feeds the
decision. Every related row is certified per-cell at the platform's confidence
threshold; if any row is rejected the query fails closed. When an existential
cue fires, the matched rows are surfaced under
explanation["relation_provenance"][<derived_field>].
report = api.platforms.query(
platform_id,
query="Triage this track.",
facts={"identification": "unidentified", "grid_square": "G3"},
relations={"maritime_track": [
{"grid_square": "G3", "zone_status": "exclusion_breach", "ais_corroborated": True},
]},
)
report["decision"] # e.g. "escalate"
report["explanation"]["relation_provenance"] # {"<cue_field>": {relation, matched, count, ...}}
The platform's rule (e.g. "maritime-cued when there exists a related maritime_track
in the same grid_square whose zone_status is exclusion_breach and ais_corroborated
is true") derives the cue from the attached rows — no pre-joined boolean in facts.
The join is thus verified, not caller-asserted.
Agent Policy Gate (preview)
Write the rules an AI agent must obey in plain English; Ambertrace compiles them to a verified policy and proves every proposed tool-call permit/deny — fail-closed, with a machine-checked proof. The LLM only proposes; the kernel proves. You author English and read back the admitted rules (also in English) plus a permit/deny verdict with its proof; the compiled form stays internal.
The gate is feature-flagged server-side (AMBERTRACE_AGENT_POLICY_GATE) and
reachable at api.agent_policy.*:
| Method | What it does |
|---|---|
author(policy_text) |
Compile an English policy into a verified gate; returns {platform, admitted, rejected, policy_text} |
status() |
The live gate: active policy, admitted controls (English), the declared input_fields an action must supply, and advisory findings. Findings are computed when the policy is compiled, not on read: findings_compiled_at / findings_compiler_build / running_build say when and by which build, and findings_stale is True after a platform deploy — re-run author(policy_text) to regenerate them (a fix to the probe check, e.g. false V6_probe_mismatch on multi-action gates, only reaches an existing policy that way) |
examples() |
The built-in example-policy library ([{id, domain_label, title, policy_text, try_hint}, ...]) — ready-to-author policies |
authorize_action(platform_id, *, tool, args, context, relations, predictions) |
Gate ONE proposed tool-call — permit/deny with proof. relations supplies a per-request set (e.g. quorum sign-offs); predictions={role: {model_id, as_of}} fans a verified forecast in (fail-closed) |
create_session(*, platform_id, goal) |
Open a mediated session (the gate is the sole executor) for a cumulative obligation |
step(session_id, *, tool, args, context) |
Mediate one action in a session: gate → execute-on-permit / block-on-deny |
get_session(session_id) |
Fetch a session and its full mediated step trace |
# 1. Author the policy in English
result = api.agent_policy.author(
"An autonomous procurement agent may place purchase orders. Each order is "
"recorded as a row in a purchase_orders ledger with a quantity column and a "
"unit_price column. The cumulative spend — the sum of quantity times "
"unit_price across every row — must stay at or below 100000. Permit a "
"purchase order only when the resulting cumulative spend stays within budget."
)
platform_id = result["platform"]["id"]
result["admitted"] # the admitted rules, described in plain English — review these
result["rejected"] # anything outside the verified fragment, with a reason (never silently dropped)
# 2. See exactly which facts an action must supply
api.agent_policy.status()["input_fields"] # e.g. quantity (int), unit_price (float)
# 3. Gate one action — permit/deny WITH PROOF
v = api.agent_policy.authorize_action(platform_id, tool="place_order",
args={"quantity": 100, "unit_price": 400})
v["decision"] # "permit" | "deny" | a policy's own verb (e.g. "escalate"/"clear")
v["permitted"] # True iff the verdict is WITHIN policy (non-restrictive) — the
# binary execute/block reading when the decision is a domain verb
v["proof_checked"] # True — the kernel certified the firing set
# For a CUMULATIVE control, mediate a session so the obligation is proven over the
# accumulated executed-action ledger (the harness is the sole executor):
s = api.agent_policy.create_session(platform_id=platform_id, goal="place orders")
step = api.agent_policy.step(s["id"], tool="place_order",
args={"quantity": 100, "unit_price": 400})
step["step"]["verdict"]["decision"], step["step"]["executed"]
Runnable end-to-end demos (on GitHub — the runnable examples are not bundled in
the wheel, so install-from-PyPI users browse them on the repo, or read the full
flow offline via help(api.agent_policy)):
Agent Policy Gate quickstart
(the author → status → authorize / session flow + what a 404 means),
examples/27_agent_policy_gate.py
gates a single action (permit one, deny another, print the proof certificate), and
examples/25_agent_spend_budget.py
mediates a session for a cumulative spend budget.
What the proof is — and is not. The verdict's proof certificate (decision,
permitted, proof_checked, deciding_rule, certified_facts, rejected_facts)
is an output the verified engine produces: it demonstrates the result —
which facts were certified, which rule decided, and that the firing set was
machine-checked. It does not ship or reveal the kernel / Lean formalisation
that produces it; you read the certificate, the engine stays internal.
Obligation classes — the English-in authoring contract
Every policy is a set of requirements an action must satisfy. Each requirement is
one of the classes below; author it in English and the compiler admits it as a
verified obligation (anything outside these classes is rejected-and-surfaced
in result["rejected"], never silently approximated). Always confirm a
requirement landed as you intended by reading result["admitted"] and by testing
a within-limit action (expect permit) and a breaching action (expect deny).
| Class | What it expresses | Example English that compiles to it |
|---|---|---|
| Per-action condition | A check on the proposed action's own fields | "Only allow actions of type triage, schedule, or refer." / "Block any actuator command with pressure outside 2 to 8 bar." / "Require mfa_passed for privileged requests." |
| Cumulative count / sum limit | A cap on a running count or sum over a declared ledger of prior actions (only count/sum — never average/min/max) |
"Each order writes a row to an order_log with a quantity column. The total quantity summed across all rows must stay at or below 1000." / "No more than 3 actions of this kind may be executed." |
| Cumulative exposure | A cap on the running value Σ qty × price over a declared ledger; the limit is a numeric constant |
"Each order writes a row to an open_positions ledger with a quantity column and a price column. The cumulative exposure — the sum of quantity times price across every row — must stay at or below 100000." |
| Interval / band binding | An exposure cap proven for every value of one as-yet-unknown factor confined to a closed interval [lo, hi] (e.g. a fill price known only to lie in a band) |
"For a proposed order whose fill price is not yet known but is guaranteed to be between 100 and 500, the cumulative exposure must stay at or below 100000 for every possible fill price in that range." |
| Temporal / sequencing | An ORDER-carrying obligation over the session's ordered ledger: precedence (happens-before), bounded-window rate, or request/response pairing | "Permit a deploy for a service only when it is preceded by an approval for the same service in the same session." / "No more than 3 deploys to a service within any 5 of that service's actions." (example 40) |
| Distinct-actor quorum | At least N DIFFERENT actors across a per-request set of sign-offs — the kernel computes the distinct count over certified rows (no self-attestation) | "Permit a production deploy only when at least two DIFFERENT approvers have signed off." — supply the sign-offs via authorize_action(relations={"approvals": [{"approver_id": …}, …]}) |
| Separation of duties | Two named fields must differ (cross-field inequality); enforced inline, no discharge fact | "The approver must not be the author." (example 28) |
The cumulative / exposure / band / temporal classes operate over a ledger (a
named relation of prior actions): name the ledger and its column(s) in your policy,
then mediate a session (create_session + step) so the gate proves the obligation
over the resulting ORDERED history. The distinct-actor quorum reads a PER-REQUEST set
supplied via authorize_action(relations=…). Browse api.agent_policy.examples() for
more ready-to-author policies across domains (healthcare triage, grid dispatch,
automation safety, access control, supply-chain).
Availability. The Agent Policy Gate is a preview capability; its endpoints
raise AmbertraceError (404) when not enabled on your deployment. The cumulative
/ exposure / band classes additionally require the platform's numeric obligation
checker to be enabled. Its endpoints (/agent-policy-gate*, /agent-sessions*,
/example-policies, authorize-action) are part of the public v1 OpenAPI spec
(openapi/ambertrace-v1.json) — the contract is documented there and in the
generated client, not just this SDK.
author() 404 — feature-off vs not-authorised. A 404 from author() has
two possible meanings: (a) the feature isn't enabled (above), or (b) an org
agent-policy gate already exists and your credentials are not its owner
or an org-admin. The org has one gate; the first author creates it (you
become owner) and only the owner/an org-admin may replace it thereafter. The
refusal is a 404 by design (not a 403) so the gate's existence isn't revealed to
an unauthorised caller — it is not a sign the feature is unavailable. To
replace an existing org gate, author with the owner's credentials or an org-admin
key. (status() / authorize_action() / step() against an existing gate are
read/eval paths and do not require ownership.)
Neurosymbolic forecasting
Train a forecasting model, then discover explainable correction rules from its residuals and check — honestly — whether they earn their place against the neural model alone.
Preview.
symbolic_forecast/residual_diagnosisare preview capabilities behind theAMBERTRACE_SYMBOLIC_FORECASTserver flag — they raiseAmbertraceError(404) when the feature is not enabled on your deployment. Both are part of the public v1 OpenAPI spec (/platforms/{id}/symbolic-forecast,/platforms/{id}/residual-diagnosisinopenapi/ambertrace-v1.json) — the contract is documented there and in the generated client, not just this SDK.discover_prediction_rules/neurosymbolic_comparisonare generally available.
# Train a Time-Series config (target = GS10, the 10y Treasury yield). `mode`
# decides the whole model — set it explicitly (see create_config's docstring).
config = api.predictions.create_config(platform_id, mode="timeseries",
target_field="GS10",
time_index_field="date", horizon=1,
frequency="monthly", model_type="gbt")
api.predictions.train(platform_id, config["id"]) # blocks by default (1.0.0)
# 1) Discover correction rules — async; the SDK polls the job and returns the summary
summary = api.predictions.discover_prediction_rules(
platform_id, prediction_config_id=config["id"])
summary["total_accepted"], summary["total_rejected"], summary["converged"]
# 2) Read the accepted rules WITH their fire-rate and backtest delta (why each earns its place)
rules = api.predictions.discovered_prediction_rules(
platform_id, prediction_config_id=config["id"])["accepted_rules"]
for r in rules:
print(r["name"], r["rule_type"], r["fire_rate"], r["delta"])
# Discovered rules are stored PENDING expert approval (is_active=False) — review,
# then activate with api.platforms.update_rule(...).
# 3) Symbolic forecast — a transparent number with its WHY (the driver-rules behind it)
fc = api.predictions.symbolic_forecast(platform_id, prediction_config_id=config["id"],
include_fitted_series=True)
fc["forecast"], fc["why"] # each why-entry: driver, direction, contribution, base_features
# `prediction_record` is the canonical LEVEL-space, ready-to-persist output. Its
# `probability` is a CERTIFIED, calibrated probability — but only non-null with
# verified=True AND an in-regime calibration; otherwise it fails closed to None
# (`probability_certified=False`). Never treat a None probability as "confident".
# 4) Neural vs neurosymbolic — does the symbolic layer earn its place? (async; polled)
cmp = api.predictions.neurosymbolic_comparison(platform_id, prediction_config_id=config["id"])
cmp["neural"]["r2"], cmp["neurosymbolic"]["r2"], cmp["delta"] # delta = neurosymbolic − neural
discover_prediction_rules and neurosymbolic_comparison are async (HTTP 202):
by default the SDK polls the background job to completion and returns its result
— pass wait=False to get the raw {job_id, poll, ...} envelope and poll it
yourself via api.wait_for_job(job_id). Discovery is a write operation, so it
needs a user-scoped (at_...) key. A runnable end-to-end demo is in
examples/26_neurosymbolic_bond_yield.py.
"Don't just give me last month's number" — a forecast with NO last-value anchor.
Two knobs on create_config, and you need both:
config = api.predictions.create_config(
platform_id, mode="timeseries", target_field="IG_SPREAD",
time_index_field="date", horizon=1, frequency="monthly", model_type="gbt",
autoregressive="none", # no target lag/roc/rolling features
feature_config={"target_transform": "none"}, # model the LEVEL directly — no last_value + Δ
)
autoregressive="none" alone is not enough: target_transform defaults to
"auto", which differences a trending target, so the returned level is still
baseline + modelled change. target_transform="none" is the knob that removes
the anchor — predict(...)["prediction"]["baseline"] then comes back null.
skill_vs_persistence is still reported either way (it is computed from the
ground-truth series in the backtest window, so persistence stays the benchmark
even when it is no longer a component of the model). Honest cost: a
level-direct tree cannot extrapolate past its training range, so on a strongly
trending target expect a negative level R² — which is exactly why "auto"
differences such a target. Runnable contrast (both configs, side by side):
examples/48_no_ar_level_direct_forecast.py.
Two equivalent spellings of target_transform. It may be passed as a
top-level create_config kwarg or nested inside feature_config — both are
accepted and mean exactly the same thing:
api.predictions.create_config(..., target_transform="difference")
api.predictions.create_config(..., feature_config={"target_transform": "difference"})
Valid values are "auto" (the default when you omit it) | "none" |
"difference"; anything else is rejected with 422 naming the valid set,
never silently ignored — in either position. If you pass both spellings with
different values the nested one wins and the SDK warns, but the losing value
is still validated, so a wrong spelling can never disappear behind "the nested
value won" (a bogus loser is a 422, not a warning). Timeseries mode only: like
feature_config, the shorthand is ignored in cross_sectional mode. The
created config echoes
resolved_target_transform / output_space immediately, so
target_transform="difference" comes back as
resolved_target_transform="difference", output_space="change" before you
train. Omitting it echoes the "auto" default
("auto (resolved at train time)") — the transform is chosen from the data at
train time.
Fixed in 1.0.16. The top-level kwarg used to be silently dropped by the server:
target_transform="difference"(and even a bogus value) resolved to"none"with no error, which on a trending target is the difference between a level R² of −5.33 and +0.33. Use either spelling now.
Predictions → Decision bridge — a verified forecast feeds a verified decision
A symbolic_forecast(verified=True, …) call PERSISTS its prediction_record
server-side (org+owner-scoped), addressable by model_id + as_of. A verified
decision then references it by handle — the platform fetches the trusted record
and folds its certified fields into the proof, so the caller never re-supplies the
forecast value (the forecast's certificate certifies its input row, not that a
caller-typed number followed):
# 1. Produce + persist a verified forecast, named + as_of-stamped so it's addressable
api.predictions.symbolic_forecast(
forecast_pid, prediction_config_id=cfg_id, verified=True,
prediction_name="ig_spread", as_of="2026-06-30")
# 2a. Fan it into a verified QUERY/DECISION by reference (a rule reads `ig_spread.value`)
api.platforms.query(
loan_pid, query="What is the lending decision?",
facts={"credit_score": 700},
predictions={"ig_spread": {"model_id": "ig_spread", "as_of": "2026-06-30"}})
# 2b. …or fan it into the Agent Policy Gate (a policy rule reads `ig_spread.value`)
api.agent_policy.authorize_action(
gate_pid, tool="place_order", args={"qty": 100},
predictions={"ig_spread": {"model_id": "ig_spread", "as_of": "2026-06-30"}})
The admitted facts are keyed <role>.<field>: <role>.value, <role>.probability
(only if the record's probability certified), and <role>.fired.<signal> per fired
signal. FAIL-CLOSED: a reference that is missing / not proof_checked /
as_of-mismatched / (for probability) uncertified admits no fact — a rule
reading it cannot fire a certified permit, so the decision abstains (a policy
with an escalate/refer fallback routes there rather than approving on an
uncertified or low-confidence forecast).
Graceful escalate — predictions={role: {…, "mode": "non_fatal"}}. By default a
failed reference fails the WHOLE query closed (503). Add "mode": "non_fatal" to a
reference to instead let the query PROCEED on that reference's absence, so a
lower-precedence escalate/deny rule can fire and return a certified 200 ("if
the referenced determination isn't certified, escalate to a human" instead of 503).
This does not relax safety: a permit can never rest on the absence of an
uncertified reference — the verified kernel's permit-guard drops any permit whose
firing depends on a negation-as-failure over an uncertified key (a missing basis is
blocking for a permit, available-as-absence for an escalate/deny). The uncertified
reference + any guarded-out permit appear in explanation["rejected_facts"] and
explanation["graceful_escalate"].
To read a <role>.value fact, the decision
domain must DECLARE it (e.g. an ontology property / column ig_spread.value). Demos:
36_credit_forecast_to_loan_decision.py
(single) and 37_multi_forecast_policy_decision.py
(three forecasts, one decision).
Connectors
Connectors pull data from external providers. List what's available, optionally test a config, then ingest it as a dataset linked to a domain:
api.connectors.list() # discover connectors + their required config fields
# Check a config WITHOUT fetching any data. Required for ASYNC connectors
# (boe_yield_curves, swap_curves, sentiment, ...): a plain test() on one of
# those returns 422 because it cannot be tested inline.
api.connectors.test(connector_type="boe_yield_curves",
config={"curve_types": ["nominal"]},
validate_only=True) # -> {"valid": True, "errors": []}
# Stocks/ETFs and crypto are keyless:
api.datasets.fetch(domain_id=1, connector_type="yahoo",
config={"symbols": ["AAPL", "SPY"], "range": "2y"})
api.datasets.fetch(domain_id=1, connector_type="coinbase",
config={"product_ids": ["BTC-USD", "ETH-USD"]})
# FRED needs your own free key (https://fred.stlouisfed.org):
api.datasets.fetch(domain_id=1, connector_type="fred",
config={"api_key": "<your FRED key>",
"series_ids": ["GS10", "FEDFUNDS"], "frequency": "monthly"})
# FRED ALFRED vintage -- point-in-time snapshot for honest backtests:
api.datasets.fetch(domain_id=1, connector_type="fred",
config={"api_key": "<your FRED key>",
"series_ids": ["GS10"], "as_of_date": "2024-06-30"})
# Eurostat SDMX -- EU macro/prices (no API key):
api.datasets.fetch(domain_id=1, connector_type="eurostat",
config={"dataset": "prc_hicp_midx",
"key": "M.I15.CP00.EU27_2020", "label": "HICP"})
# US Treasury FiscalData -- debt, rates, FX (no API key):
api.datasets.fetch(domain_id=1, connector_type="fiscaldata",
config={"endpoint": "v2/accounting/od/avg_interest_rates",
"fields": ["record_date", "security_desc",
"avg_interest_rate_amt"],
"pivot_column": "security_desc",
"value_column": "avg_interest_rate_amt"})
# SEC EDGAR XBRL -- US company fundamentals (no API key):
api.datasets.fetch(domain_id=1, connector_type="edgar",
config={"tickers": ["AAPL"], "concepts": ["Assets"],
"period": "annual"})
# IMF iData SDMX -- global macro (BYO subscription key):
api.datasets.fetch(domain_id=1, connector_type="imf",
config={"dataflow": "IMF.STA,CPI",
"key": "USA.CPI._T.IX.M",
"api_key": "<your IMF_API_KEY>"})
# World Bank -- global development indicators (no API key):
api.datasets.fetch(domain_id=1, connector_type="worldbank",
config={"indicators": ["NY.GDP.MKTP.CD"],
"countries": ["GBR", "USA"]})
# Generic REST/CSV — bring your own auth via headers:
api.datasets.fetch(domain_id=1, connector_type="rest",
config={"url": "https://api.example.com/series",
"headers": {"Authorization": "Bearer ..."}})
| Connector | Config | Key? |
|---|---|---|
yahoo |
symbols, interval, range |
none |
coinbase |
product_ids, granularity |
none |
fred / fred_sentiment |
series_ids, frequency, api_key; ALFRED: as_of_date or vintage |
bring your own (free) |
eurostat |
dataset, key, label |
none |
fiscaldata |
endpoint, fields, pivot_column, value_column, label |
none |
edgar |
tickers, concepts, period (annual/quarterly), taxonomy |
none |
imf |
dataflow, key, api_key (set IMF_API_KEY env var) |
bring your own |
worldbank |
indicators, countries |
none |
boe |
series_codes |
none |
ecb |
series_keys |
none |
oecd |
dataflow |
none |
rest |
url, format, records_path, headers, params |
bring your own (via headers) |
gdelt |
topics or use_defaults, lookback_days, frequency, source_country |
none |
sentiment |
feeds, dimensions, lookback_days, frequency |
none |
Every connector in this table publishes a machine-readable config_schema
(field name, type, required, default, enum, example) via
api.connectors.list() -- prefer reading it over hardcoding key names.
Bring your own provider keys. Connectors that hit a credentialed provider require
your own key, passed in config -- Ambertrace never uses a shared key on your behalf.
For the IMF connector, set IMF_API_KEY in your environment (the Ocp-Apim-Subscription-Key
header value from idata.imf.org).
Panel sufficiency — check before you train
Broadening a forecast panel can silently and severely REDUCE the usable training
sample. A row is usable only where every column is non-null, so one
discontinued series truncates decades of history to a few years — and nothing
errors. datasets.panel_report() makes that visible BEFORE you build or train.
report = api.datasets.panel_report(dataset["id"], index_column="date")
inter = report["intersection"]
print(f"{report['column_count']} cols x {report['row_count']} rows "
f"-> {inter['usable_rows']} usable rows "
f"({inter['first_index']} .. {inter['last_index']})")
b = report["binding_constraint"]
print(f"Drop '{b['column']}' alone: +{b['rows_recovered_if_dropped']} rows "
f"({b['usable_rows_if_dropped']} usable, window to {b['last_index_if_dropped']})")
# Columns that go missing TOGETHER. When two series die in the same window,
# dropping either one ALONE recovers nothing -- only the group does.
# HEURISTIC (g["heuristic"] == "observed_co_missing_sets"): candidates are the
# co-missing sets actually observed as some row's exact missing set, not all
# subsets, so the best set to drop may be a superset that never appears alone.
for g in report["recovery_groups"]:
print(g["columns"], "->", g["usable_rows_if_all_dropped"], "usable")
print("Stale/discontinued:", report["stale_columns"])
| Field | Meaning |
|---|---|
intersection |
usable_rows, first_index, last_index, coverage_pct for the all-columns-non-null intersection |
binding_constraint |
the single costliest column: column, rows_recovered_if_dropped, usable_rows_if_dropped, last_index_if_dropped |
recovery_groups |
small SETS of co-missing columns: columns, rows_recovered_if_all_dropped, usable_rows_if_all_dropped, heuristic |
columns[] |
per column: first_non_null, last_non_null, non_null_count, null_count, recency_lag_periods, stale, rows_recovered_if_dropped |
stale_columns |
names flagged stale — last value lags the panel by more than stale_periods cadence periods (cadence = median index spacing) |
cadence_days |
inferred index spacing in days |
caveats |
read these — usable_rows is the RAW intersection; training removes a further warmup of horizon + max(lag, rolling window) rows |
skipped_reason |
non-null when the report could not be computed (no index column, unsupported format). Always present, so an uncomputable report can never read as a clean panel |
index_column is optional: omit it and the first of
date/time/timestamp/datetime/period present is used — the same
detection the ingest-time block uses, so the two agree on a panel whose index is
not literally called date. Naming a column that is absent gives
skipped_reason="index_column_not_found", never a silent fallback.
The same summary (minus the per-column array) is persisted at ingest on
dataset["schema_info"]["panel_sufficiency"], and the per-column freshness
fields are merged into schema_info["columns"] — so datasets.get(id) already
shows first/last non-null and the staleness flag with no extra call. The block is
recomputed by datasets.clean() as well, so it never describes the pre-clean
bytes. Worked example: 44_public_data_connectors.py.
Agent Keys
AI agents authenticate with user-scoped API keys that give full lifecycle access (domains, datasets, platforms, rules, predictions). A human creates the key from the dashboard; the agent can then create narrower platform-scoped keys for its integrations.
# Agent creates a platform-scoped key for a specific integration, optionally
# with an expiry (ISO-8601; naive = UTC; must be in the future)
platform_key = api.api_keys.create(
scope="platform",
platform_id=42,
name="Slack Integration",
expires_at="2026-12-31T00:00:00Z",
)
# List keys visible to this agent (includes expires_at / grace_until / rotated_from_id)
keys = api.api_keys.list()
# Revoke a platform key the agent created
api.api_keys.revoke(platform_key["id"])
User-scoped keys cannot create other user-scoped keys (no self-replication). Chat, conversations, and billing remain human-only.
Rotation — zero-downtime key replacement
Ahead of expiry (or on a routine rotation schedule), rotate() mints a
replacement key and puts the old key into a bounded grace window so
in-flight callers keep working while you cut over — no window with two
un-graced keys:
rotated = api.api_keys.rotate(platform_key["id"], grace_seconds=300)
new_key = rotated["key"] # returned exactly once — store it now
rotated["rotated_from_id"] # -> platform_key["id"]
rotated["old_key"]["grace_until"] # old key keeps validating until this instant
The new key inherits the old key's org, owner, platform binding, scope, name,
rate limit, token budget, IP allowlist, and expiry (pass expires_at= to set
the new key's own expiry instead). grace_seconds is 0–86400 (default from
API_KEY_ROTATION_GRACE_SECONDS, else 300s); 0 cuts over immediately. Rotating
an already-revoked, already-expired, or already-rotated key raises
AmbertraceError (409) — create a new key instead. A runnable demo is in
examples/04_api_keys.py.
Async operations
Long-running calls follow one of three conventions. This table says, per method, whether it blocks, returns a job to poll, or is synchronous:
| Method | Behaviour | How you wait |
|---|---|---|
platforms.create, domains.build_ontology |
returns a 202 envelope with a normalised job_id |
api.wait_for_job(result.job_id) |
predictions.train |
blocks by default (wait=True), returns the trained config |
automatic; wait=False for the raw job envelope |
predictions.discover_prediction_rules, neurosymbolic_comparison |
blocks by default (wait=True), returns the result |
automatic; wait=False for the raw job envelope |
agent_policy.author |
blocks always (polling hidden), returns the built gate | automatic (bounded by timeout=) |
datasets.fetch, fetch_multi, clean |
returns the dataset record with status="processing" |
poll datasets.get(id) until status="ready" |
platforms.query, authorize_action, step, predict |
synchronous | — |
The rule of thumb since 1.0.0: the prediction async methods (train,
discover_prediction_rules, neurosymbolic_comparison) all block and return
the result by default, with wait=False as the raw-job escape hatch;
platforms.create / domains.build_ontology return a job_id you pass to
wait_for_job; connector fetches poll the dataset's own status.
Job Polling
Operations that return a job_id (platform builds, ontology builds) are polled with wait_for_job:
job = api.wait_for_job(job_id, timeout=300, poll_interval=5)
if job["status"] == "error":
print(f"Failed: {job.get('error_message')}")
Long ontology builds. Ontology builds on hard domains (ReAct-authored rules) can run
15-20 minutes; the default timeout=600 is not enough. Use
api.wait_for_job(job_id, timeout=1800, poll_interval=10, stall_timeout=300) -- the job's
progress rises throughout the build, so stall_timeout separates "slow but progressing"
from "stuck".
Progress + stall detection. wait_for_job takes two optional, back-compatible
hooks so you can surface progress and catch a build that hangs without
hand-rolling a retry wrapper:
# Live progress on every poll:
api.wait_for_job(job_id, on_progress=lambda j: print(j.get("status"), j.get("progress")))
# Bail out if the build makes no forward progress (a change in status or
# `progress`) for 120s — even if the overall timeout hasn't elapsed:
try:
api.wait_for_job(job_id, timeout=600, stall_timeout=120)
except TimeoutError as e:
print("build stalled:", e) # e.g. stuck at building_ontology progress 0
Two job types — poll the right one
GET /api/v1/jobs/{id} (and wait_for_job) returns two different job types:
- the ontology build job (
type: "ontology", created bydomains.build_ontology) — itsresultis the ontology. - the platform build job (
type: "build", thebuild_jobfromplatforms.create) — itsresult.build_qualitycarries the customer-facing build-quality summary and itsresult.generation_diagnosticsthe decision-coverage detail below.
A consumer polling the ontology job will not see generation_diagnostics — poll the platform build job id instead.
Build diagnostics
After a platform build, job["result"]["generation_diagnostics"] reports what rule generation produced and how the rule set behaves — the quickest way to explain why a platform reaches (or never reaches) an adverse decision:
job = api.wait_for_job(build_job_id, timeout=600)
diag = job["result"].get("generation_diagnostics", {})
# verdict_conclusion_count == 0 (== `can_decide_adversely is False`) means the
# rule set classifies inputs but has no deny/block conclusion — it permits
# everything and can never refuse.
if not diag.get("can_decide_adversely", True):
print("Platform reaches no adverse decision:")
for w in diag.get("decision_coverage_warnings", []):
print(" -", w)
Fields: rule_count, classifier_count, verdict_conclusion_count, connected_restrictive_count (ints); can_decide_adversely (bool); decision_coverage_warnings, non_discriminating_rules, orphan_derived (list[str]), unbound_references (list).
Stated-constraint diagnostics
After an ontology build, domains.get(domain_id)["ontology"]["stated_constraint_diagnostics"] reports constraints the domain description states but no built rule encodes -- for example, "debt must not exceed 40% of income" when no cross-field coefficient rule exists. This is an advisory diagnostic only (it never gates the build) and the key is absent when all stated constraints are encoded.
domain = api.domains.get(domain_id)
diag = (domain.get("ontology") or {}).get("stated_constraint_diagnostics")
if diag:
for finding in diag:
print(f"Unencoded: {finding['comparison_field']} "
f"{finding['direction']} {finding['coefficient']}*"
f"{finding['coefficient_field']} "
f"(from: \"{finding['sentence']}\")")
Each finding is a dict with: type (always "stated_constraint_not_encoded"), comparison_field, coefficient_field (the two data columns), coefficient (numeric multiplier), direction (e.g. "lte"), sentence (the source text from the domain description).
Org-Capability Gating
Organisations can have individual capabilities (chat, query, predictions)
enabled or disabled by an administrator. All three are enabled by default --
the gate can never silently lock out an org that has not been explicitly
configured. When a capability is disabled, every endpoint tagged with that
capability returns HTTP 403 with a structured error:
# Response body when a capability is disabled:
# {
# "error": {
# "code": "capability_disabled",
# "message": "This capability is not enabled for your organisation. ..."
# },
# "capability": "query" # the gated capability name
# }
from ambertraceai import AmbertraceError
try:
api.platforms.query(platform_id, query="...", facts={...})
except AmbertraceError as e:
if e.code == "capability_disabled":
print(f"Capability '{e.capability}' is disabled for this org.")
Discovery. GET /api/v1/capabilities returns the caller's org effective set
(user-scoped and session callers only). Platform-scoped API keys receive 403
forbidden on the discovery endpoint (scope-context precedent -- they are bound
to a single platform with no org-wide visibility). A platform-key caller should
either use a user-scoped key for discovery, or have an org administrator
communicate the enabled set out of band.
caps = api._request("GET", "/api/v1/capabilities")
if caps["capabilities"].get("predictions") is False:
print("Predictions disabled -- skip the call.")
Capabilities and their gated endpoints:
| Capability | Gated endpoints |
|---|---|
chat |
POST /api/v1/chat |
query |
POST /platforms/{id}/query, /export-report, /authorize-action, agent sessions |
predictions |
All /predictions*, /prediction-configs*, /predict, /symbolic-forecast, /residual-diagnosis endpoints |
A runnable demo is in examples/42_capability_gating.py.
Error Handling
from ambertraceai import AmbertraceAPI, AmbertraceError
try:
api.domains.get(999)
except AmbertraceError as e:
print(e.status_code) # 404
print(e.code) # "not_found"
print(str(e)) # "Domain not found."
When a verified platforms.query fails closed (the engine could not certify a
decision), the error carries machine-readable diagnostics so you don't have to
string-parse the prose message:
try:
api.platforms.query(platform_id, query="...")
except AmbertraceError as e:
e.missing_atoms # atoms a decision rule needed but were neither supplied nor derived
e.deciding_rule # the rule that stalled, if named
e.rejected_facts # list[RejectedFact] = {field, value, reasons} — the facts the engine rejected
e.stalled_stage # where the chain stopped (e.g. "decision")
e.rejected_facts is a list of the typed RejectedFact — {field, value, reasons}
(the same shape as explanation["rejected_facts"] on a 200), so you can attribute a
rejection to the specific field, its offending value, and the machine-readable reasons
(e.g. out-of-domain, below-threshold). Against a pre-1.0.6 deployment that supplied only
the prose details, rejected_facts falls back to the bare field-name strings.
Each defaults to [] / None when the deployment doesn't supply it (back-compatible).
This brings the query failure path to parity with
agent_policy.authorize_action(), which already returns structured rejected_facts
/ deciding_rule.
build_ontology precondition errors
domains.build_ontology validates the dataset state before queuing the build.
Three outcomes:
| HTTP | e.code |
Meaning | Action |
|---|---|---|---|
| 400 | data_required |
No dataset is attached to the domain | Upload or fetch a dataset first |
| 409 | dataset_not_ready |
A dataset exists but is still processing (e.g. an async fetch_multi in progress) |
Retryable -- poll datasets.get(id) until status="ready", then retry |
| 409 | dataset_ingestion_failed |
A dataset exists but its ingestion terminated in a failure state (e.g. status="error") |
Not retryable -- delete the dataset and re-ingest (re-upload or re-fetch) |
try:
onto = api.domains.build_ontology(domain_id=domain["id"])
except AmbertraceError as e:
if e.code == "dataset_not_ready":
# wait and retry -- ingestion is still in progress
...
elif e.code == "dataset_ingestion_failed":
# re-ingest: delete + re-upload/re-fetch
...
elif e.code == "data_required":
# no dataset attached at all
...
API Documentation
Machine-readable OpenAPI spec: app.ambertrace.ai/api/v1/openapi.json
Interactive API reference (ReDoc): app.ambertrace.ai/docs
Agent/LLM orientation: AGENTS.md
Changelog
2.9.0
platforms.status() now returns decision_vocabulary; domains.create(decision_vocabulary=...) is rejected.
GET /api/v1/platforms/{id}/status carries decision_vocabulary ({"verbs": [{verb, rank, restrictive, default, label}, ...]}, or null when the policy
declares no custom verbs) -- previously only name/platform_id/status/version,
though the docs pointed there. Existing keys are unchanged. domains.create
never supported pinning a vocabulary and silently dropped the kwarg; it now
returns HTTP 400. State the verbs in the description and read the minted
vocabulary from platforms.status(id) or
domains.get(id)["ontology"]["decision_vocabulary"]. Example
68_decision_verb_pinning.py reads it from platforms.status.
datasets.panel_report() -- pre-training panel sufficiency + per-column
freshness. Broadening a forecast panel could silently and
severely reduce the usable training sample: a row survives only where every
column is non-null, so one discontinued series truncated decades of history with
no signal anywhere (a 312-column panel that intersected to 76 rows where the
curated 8-column version had 281). New
GET /api/v1/datasets/{id}/panel-report, wrapped as
api.datasets.panel_report(dataset_id, index_column="date", stale_periods=3):
usable rows over the all-columns-non-null intersection and its window, the
binding-constraint column with the rows dropping it alone recovers,
recovery groups of columns that go missing TOGETHER (two series dying in the
same window recover nothing individually), and per-column first_non_null /
last_non_null / recency_lag_periods / stale. The summary is also persisted
at ingest (upload, fetch, fetch_multi) on
schema_info["panel_sufficiency"] (and refreshed by clean()), with the
freshness fields merged into schema_info["columns"]. skipped_reason is always
present, so an uncomputable report can never be mistaken for a clean panel; and
each recovery group carries heuristic="observed_co_missing_sets", because the
candidate sets are the ones observed in the data rather than all subsets. See the
new
Panel sufficiency section and
44_public_data_connectors.py.
connectors.test(..., validate_only=True) -- config check without a fetch.
The backend has supported a validate_only dry-run on
POST /api/v1/connectors/test, but the SDK wrapper hardcoded the
request body and never forwarded it, so the capability was unreachable from the
published package. ConnectorResource.test() now takes validate_only: bool = False. With it set, no data is fetched and the response is
{"valid": bool, "errors": [...]} -- this is the ONLY way to check the config
of an asynchronous connector (boe_yield_curves, swap_curves,
sentiment, ...), which otherwise returns HTTP 422 because it cannot be tested
inline. Default False preserves the existing sample-fetch behaviour.
Demonstrated in 03_connectors.py.
Org-capability gating -- the capability_disabled 403 contract.
Organisations can now have individual capabilities (chat, query,
predictions) enabled or disabled by an administrator. All three default to
ENABLED (the gate never silently locks out an existing org). When a capability
is disabled, every gated endpoint returns HTTP 403 with error code
capability_disabled and a top-level capability field naming the denied
capability, so SDK callers can branch programmatically. A new
GET /api/v1/capabilities endpoint (user-scoped / session callers only; part
of the public OpenAPI spec) returns the caller's org effective capability set.
Platform-scoped API keys get 403 forbidden on the discovery endpoint
(scope-context precedent). New example 42_capability_gating.py demonstrates
discovery, 403 handling, and pre-flight checks. platforms.query(),
predictions.predict(), and predictions.symbolic_forecast() docstrings now
name the capability gate and the 403 code. See the new
Org-Capability Gating section.
Public-spec promotion (docs-only -- no SDK behaviour change). The Agent
Policy Gate and the symbolic_forecast / residual_diagnosis "why" layer were
already reachable from this SDK but omitted from the public v1 OpenAPI spec, so a
fresh agent reading ReDoc/the spec as the contract could not discover them. Both
are now in openapi/ambertrace-v1.json (58 -> 69 public paths); no method
signature changed.
Data redistribution gate -- forbidden 403 on non-redistributable sources.
platforms.query() and export-report now return HTTP 403 with error
code forbidden if the platform's datasets include data sourced from a connector
whose licence does not permit redistribution. This prevents silently re-serving
licensed data downstream via the API. Check the connector's redistributable
field via connectors.list() to determine which sources are permitted before
building a platform. The gate is fail-closed: unknown connector types are treated
as non-redistributable.
explanation.confidence.decision_margin now appears on verified (proof-checked)
platforms and measures distance-to-flip. Previously it was absent on every
proof_checked result. It is now the minimum normalised distance over every numeric
threshold leaf whose flip would change the decision: fired leaves, plus -- when no
restrictive verdict fired -- the unfired leaves of restrictive (deny-family) rules (a
default-allow policy with deny credit_score < 500 queried at 760 gives 0.342).
decision_margin_basis entries gain an additive fired boolean. overall is unchanged
and remains a reasoning-quality score, not an input margin. The values are read from the
proof's certified facts but the margin itself is not proof-carrying.
2.8.0
Certified regime-grid totality + exclusivity --
platforms.verify_property(property="exactly_one_verdict"). For a platform whose
decision rules classify a regime grid you declare in space.variables, certify that every
cell derives EXACTLY ONE verdict: HOLDS (exhaustive) or VIOLATED with the uncovered /
double-covered cell (witness, plus the new witness_detail = {cell, verdicts, coverage} on VerifyPropertyResult). A rule leaf outside the discrete scalar fragment
is an explicit ABSTAIN. Same dedicated method as 2.7.0 (async job, polled for you);
space now takes variables instead of mechanism/agents/domain for this property.
Example 70_verify_property_regime_grid.py; see Certified
search.
2.7.0
Certified search -- platforms.verify_property. Prove a mechanism's
UNIVERSAL property over a finite space instead of checking one input: the first
property is strategy_proof over plurality, majority, vickrey and
first_price. The result is HOLDS (exhaustive, machine-checked, search.complete),
VIOLATED (a certified, independently re-certified witness) or an explicit
ABSTAIN (space above your bound or the platform ceiling -- never a silent partial
pass). result, certified, proof_checked, proof_summary and the SDK-rendered
answer always name the same verdict. New dedicated method (POST /api/v1/platforms/{id}/verify-property, an async 202 + GET /jobs/{id} poll that the SDK
runs for you -- wait=True default, poll_interval=5; not a query mode), new VerifyPropertyResult
/ VerifyPropertySearch types, and example 69_verify_property_strategy_proof.py.
See the new Certified search
section.
1.0.10
Stated-constraint diagnostics. The domain detail response
(domains.get()) now documents the ontology.stated_constraint_diagnostics
key -- an advisory list of constraints the domain description states but no
built rule encodes. New StatedConstraintFinding TypedDict in responses.py.
See "Stated-constraint diagnostics" under "Build diagnostics" above.
1.0.9
Public-data connector catalog. Six new connectors for public macro, fiscal, and company-fundamentals data, plus ALFRED vintage support on the existing FRED connector:
eurostat-- Eurostat SDMX (EU HICP, unemployment, GDP; no API key; reuse under Decision 2011/833/EU). Config:dataset,key, optionallabel.fiscaldata-- US Treasury FiscalData (debt-to-penny, avg interest rates, exchange rates; no API key; US public domain). Config:endpoint, plus optionalfields,pivot_column,value_column,label. Multi-entity presets auto-pivot to wide format.edgar-- SEC EDGAR XBRL (revenue, net income, assets by ticker; no API key; US public domain; SEC fair-access rate limit enforced). Config:tickers,concepts, optionalperiod(annual/quarterly),taxonomy.imf-- IMF iData SDMX (CPI, exchange rates, BOP; BYO API key -- setIMF_API_KEYenv var, theOcp-Apim-Subscription-Key). Config:dataflow(agency-qualified, e.g.IMF.STA,CPI),key,api_key, optionallabel. Bounded retry on transient 5xx.worldbank-- World Bank Open Data (GDP, CPI, development indicators by country; no API key; CC BY 4.0). Config:indicators,countries(ISO-3166 alpha-3, or["all"]).fredALFRED vintage -- point-in-time snapshots for honest backtests (as_of_date: fetch each series as known on that date;vintage: "all_releases": full revision history for one series). Mutually exclusive with each other. Existingfredusage is unchanged.
All new connectors are async (is_async = True) -- datasets.fetch returns
HTTP 202 with status="processing"; poll datasets.get(id) until
status="ready". New example 44_public_data_connectors.py covers all six
plus a fetch_multi merge.
1.0.7 — 2026-07-12
API-key rotation + customer-settable expiry. api.api_keys.create
gains an optional expires_at (ISO-8601; naive = UTC; must be in the future, else
422). New api.api_keys.rotate(key_id, *, grace_seconds=None, expires_at=None)
atomically mints a replacement key (inheriting org/owner/platform/scope/name/rate
limit/token budget/IP allowlist/expiry) and puts the old key into a bounded grace
window (0–86400s, default 300s) so callers rotate with zero downtime; returns 201
with the new key secret exactly once. Rotating a revoked, expired, or
already-rotated key raises AmbertraceError (409). Key listings now also carry
expires_at, grace_until, rotated_from_id. See examples/04_api_keys.py.
1.0.6 (shipped within 1.0.7 — never released standalone)
Structured rejected_facts on the fail-closed error body. A verified
platforms.query that fails closed (503) now carries a top-level, machine-readable
rejected_facts list — the typed RejectedFact = {field, value, reasons} — read via
AmbertraceError.rejected_facts. Previously the 503 surfaced only the prose details
block, so AmbertraceError.rejected_facts fell back to bare field-name strings, losing
the offending value and the per-fact reasons. The shape now matches the 200
explanation.rejected_facts contract, so a rejection can be attributed to the specific
field, its value, and why (out-of-domain, below-threshold, …). Back-compatible: against a
pre-1.0.6 deployment the property still falls back to the details field names.
1.0.5
Graceful escalate on an uncertified prediction reference (additive, backward-
compatible). platforms.query(predictions=…) references gain an optional
per-reference "mode":
"mode": "fatal"(default) — unchanged: a failed reference fails the whole query closed (503). Every existing caller keeps the strict fail-closed composition."mode": "non_fatal"— a failed reference admits no fact but the query PROCEEDS and decides over what remains, so a lower-precedenceescalate/denyrule can fire on the ABSENCE of the basis and return a certified200(the "escalate to a human if the referenced determination isn't certified" pattern) instead of 503-ing.
non_fatal does NOT relax the safety guarantee: a permit can never rest
on the absence of an uncertified reference — the verified kernel's permit-guard drops
any permit whose firing depends on a negation-as-failure over an uncertified key. A
missing basis is BLOCKING for a permit but AVAILABLE-AS-ABSENCE for an escalate/deny
fallback. The uncertified reference and any guarded-out permit are surfaced in
explanation["rejected_facts"] and explanation["graceful_escalate"]
({uncertified_roles, permits_dropped}). Documented on platforms.query() + the
README predictions section; the producing/consuming method is unchanged
(platforms.query). Verified platforms only.
Open-textured SCORED determinations (new capability, additive, flag-gated default OFF).
Where a decision turns on an OPEN-TEXTURED predicate that bright-line rules cannot decide
— compatibility, reasonableness, materiality, good-faith — you can now have the
platform's OWN runtime LLM score it. platforms.create(scored_determinations={…}) (or
platforms.update) declares a determination: a head field, the question, the legal /
domain doctrine (the tuning surface), and the situation_fields (request text fields).
At query time the platform scores the predicate with self-consistency, calibrates it, and
admits the result as a confidence-carrying fact subject to τ (verified_min_confidence):
p ≥ τ→ the head fact is admitted and can support a permit (with an honest certificate: LLM model + prompt version, K, self-consistency dispersion, calibrator);p < τOR an abstain / out-of-distribution / high self-consistency dispersion determination admits no fact → the request routes to escalate, never a permit.
DEDUCTIVE-FIRST + fail-closed. The LLM-τ score fills ONLY the open-textured joint; everywhere else, ordinary deduction governs. The score is SERVER-computed from the text — a caller cannot hand-set it. Its guarantee is EMPIRICAL (calibration-in-regime
- coherent-input + fail-closed-OOD) — honestly weaker than the deductive kernel proofs;
use it exactly where deduction is silent. New runnable demo
41_records_gate_scored_determination.py(Privacy Act routine-use compatibility gate): the same request shape PERMITS or ESCALATES purely on whether the compatibility score clears τ. Composes with the graceful-escalate posture: an uncertified determination can never back a permit.
1.0.4
Discoverability wave (additive — no behaviour change). Several shipped backend capabilities were reachable but not signposted in the SDK surface a customer reads first; this release names them and adds a gate so it can't recur.
platforms.query(predictions=…)— the native Prediction → Decision fan-in.query()now documents (and takes explicit)predictions={role: {"model_id", "as_of"}}: reference a verified forecast this org already produced + persisted and the platform folds its certified<role>.valueinto the proof — BY REFERENCE, never by value. Fail-closed: a missing / uncertified / mis-aligned reference admits no fact, so the decision abstains rather than approving on an unproven (or low-confidence) forecast.top_kis now an explicitquery()param too.agent_policy.authorize_action(predictions=…). The same fail-closed bridge, now callable — the method previously had a fixed signature with nopredictionsparam, so the backend fan-in was unreachable from the SDK.- Agent Policy Gate temporal / sequencing obligations are documented + demoed.
The
author()obligation-class list now includes precedence (precededBy), bounded-window rate, and request/response pairing (a NATIVE happens-before obligation over the session's ordered ledger), plus distinct-actor quorum and separation-of-duties. New runnable demo40_agent_policy_gate_temporal.py(review-before-deploy). Example 28's stale "temporal is not yet a gate primitive" note is corrected. - N-class / multi-class classifier + custom decision vocabulary are now named in
build_ontology/platforms.createdocstrings (the grep-a-method path), not only example 38. Verified-profile build kwargs (verified_profile,verified_min_confidence,invariant_manifest,override_verification_gate) are documented onplatforms.create. Eval-config / rule-template methods gained docstrings naming their fields.symbolic_forecast'sprediction_recordnow cross-links its consuming methods. - New capability index (README top) mapping each capability → the method that produces it, and a machine-enforced discoverability gate in the pre-PR run: a shipped request field with no SDK docstring/example/doc signpost now fails the gate.
1.0.0
First stable release. Three breaking default flips land together (the SDK is
onboarding its first customers — the correct defaults are set now, while few
consumers depend on the old behaviour). See MIGRATION.md for the exact
before/after and the escape hatch for each.
- BREAKING (C1) — compact certification by default.
symbolic_forecastnow returns compact certification by default (as announced in 0.18.0). The top-levelwhy_certificationcarries acertification_summary(proof_checked- counts + min confidence) instead of the full per-feature
certified_factslist. Passcompact_certification=Falsefor the fullcertified_facts.
- De-dup: the embedded
prediction_record.why_certificationnow ALWAYS carries the compact handle (proof-carrying, re-checked by the decision layer), never a second copy of the fact list — regardless ofcompact_certification. The full list, when opted into, appears only once, at the top level.
- counts + min confidence) instead of the full per-feature
- BREAKING (C2) —
train()blocks and returns the trained config by default.platforms.train(...)now defaults towait=True: it polls the training job to completion and returns the SETTLED trainedPredictionConfig(matching itsdiscover_prediction_rules/neurosymbolic_comparisonsiblings), instead of the raw 202 job envelope. Passwait=Falseto restore the historic raw-job return and poll yourself. - BREAKING (C3) —
predict().valueis the LEVEL by default. For a differenced target (target_transform="difference")prediction.valueis now the reconstructed level (baseline + change), NOT the raw month-over-month change. The change is exposed alongside asvalue_change, andvalue_spaceis"level"on the reconstructable path. When there is no base history to reconstruct from,valueremains the raw change andvalue_spaceis"transformed_unreconstructed"(treat as unreliable). Publicpredictcontract change — regenerated OpenAPI + client.
Additive in 1.0.0 (no access change):
- Typed convenience returns (
TypedDict). Every convenience method now declares aTypedDictreturn type (query(...) -> QueryResult,authorize_action(...) -> AuthorizeActionResult,symbolic_forecast(...) -> SymbolicForecastResult,get(...) -> PlatformOut, …) instead of a baredict. Since aTypedDictis adictat runtime, nothing changes at runtime —result["answer"]/result.answer/.get(...)keep working byte-for-byte — but your IDE now autocompletes the fields and a type-checker catches a typo (result["desicion"]). The shapes live inambertraceai.responsesand are exported from the top-level package. Genuinely open sub-blocks (explanation, the rawprediction, awhy_certificationpayload) are typed as an opendict[str, Any](aliasedJsonDict) rather than forced into a rigid shape. - DX doc/example fixes.
create_confignow documentsmode(cross_sectionalvstimeseries) as its primary switch and what it means forfeature_overrides;datasets.fetch/cleandocument their async processing→ready poll; a danglingPlatformResource.prediction_modeldocstring reference was removed;wait_for_job/JobResource.getacceptint | strids.examples/06_predictions.pywas corrected to the realpredict(...)/train(...)signatures, andexamples/02_platform_lifecycle.pynow agrees with the README/docstring on polling build-ontology by its returnedjob_id.
0.18.0
- Verified prediction developer-experience (additive — no breaking changes).
- Certified
prediction_record.symbolic_forecastnow surfaces a top-levelprediction_record— the canonical, ready-to-persist Stage-A output (proof-carrying, addressable by role) — alongside a certified probability. This is the bridge-shaped record the query / decision layer ingests. - Addressing / naming kwargs on
symbolic_forecast. Seven optional kwargs (prediction_name,prediction_model_id,as_of,sector,period,entity,top_drivers_n) name and address the emittedprediction_recordso a downstream verified decision can fan several forecasts in by role. compact_certificationonsymbolic_forecast(opt-in, defaultFalse): slims the certification payload (both the top-level block and the embeddedprediction_record). Deprecation note:compact_certificationbecomes the default (True) in 0.19.0 — passcompact_certification=Falseexplicitly if you depend on the full payload.predict()transform surface.value_space,target_transformandbaselineare now documented on the predict workflow: a forecast is emitted in a known space ("level"vs"change"), with the effective (post-auto-resolution) transform and the reconstruction baseline exposed.PredictionConfigOutechoes the resolved transform (resolved_target_transform/output_space/target_transform_reason).train(wait=...).platforms.train(...)gains an opt-inwaitflag —wait=Truepolls the training job to completion (returning the resolved transform / output space);wait=False(the default) preserves the historic raw-202-envelope return type.
- Certified
0.17.0
- Developer-experience ergonomics (no breaking changes).
AmbertraceAPI.from_env()(and env defaults on the constructor): readsAMBERTRACE_API_KEY/AMBERTRACE_BASE_URL(base URL defaults tohttps://app.ambertrace.ai), with optional.envloading viafrom_env(dotenv_path=...)— no per-project auth boilerplate. An explicit argument always wins over the environment.- Consistent envelopes.
platforms.createanddomains.build_ontologyreturn anAttrDictstamped with a normalised, stableid/job_idregardless of the underlying shape (platform.id,build_job.job.id, ...), so callers no longer hand-roll multi-shape unwrapping. The original keys are preserved. - Typed dataset returns.
datasets.upload/get/listreturn anAttrDictexposing the documentedDatasetOutfields (row_count,column_count,decision_column, ...) by attribute as well as subscript — discoverable without grepping SDK source.AttrDictis adictsubclass, so every existing subscript /.get()/in/json.dumps()is unchanged. - Build-stall detection in
wait_for_job. New optionalon_progresscallback (invoked with the job dict each poll) andstall_timeout(raiseTimeoutErroron no forward progress — a change instatusorprogress— for N seconds), so a hung build is caught without a hand-rolled retry wrapper. The existing two-arg signature is unchanged. - Structured fail-closed query errors. A verified
platforms.querythat can't certify now surfacesmissing_atoms,deciding_rule,rejected_factsandstalled_stageonAmbertraceError(read off the error body; default[]/Nonewhen absent) — parity withagent_policy.authorize_action. decision_columndocstring.datasets.upload(..., decision_column=...)now documents that naming a column flips the build from features-only to label-supervised (verdict generation grounded against the labelled outcomes).
0.16.0
- Agent Policy Gate — documented + exampled. The
api.agent_policyresource (author an English governance policy, then prove every proposed agent action permit/deny against it — fail-closed, with a machine-checked proof) is now fully surfaced in the README (the Agent Policy Gate section, the method table, and the obligation-class authoring contract) and in a new single-action worked example,examples/27_agent_policy_gate.py— author a per-action policy, gate a PERMIT case and a DENY case, and print the verdict's proof certificate (decision,permitted,proof_checked,deciding_rule,certified_facts,rejected_facts,denied_reason). The proof certificate is an output demonstrating the result; it does not reveal the kernel/Lean engine that produces it. The gate is a preview capability (feature-flagged server-side; its endpoints returnAmbertraceError(404) when not enabled). No client API changed —AgentPolicyResourcealready shipped.
0.15.0
- Multi-source connector fetch + decision-column upload.
api.datasets.fetch_multi(domain_id=..., sources=[...], join_on="date", ...)fetches from two or more connectors and merges them into ONE date-aligned panel (each value column namespaced by connector type), with optionalfrequency/aggregationresampling so mixed-cadence sources land on a common grid.api.datasets.upload(...)now acceptsdecision_column=to declare the dataset's decision/label column at upload time.
0.11.2
- Per-period neurosymbolic-comparison series (for charting).
neurosymbolic_comparisonnow acceptsinclude_series=True— the completed job result then carries aserieslist of the per-period neural-vs-neurosymbolic head-to-head over the SAME held-out backtest points the aggregate metrics are computed from, so the comparison can be charted OVER TIME. Each entry is{index, time?, actual, neural, neurosymbolic, rule_fired}(rule_firedmarks the periods where applying the rules changed the prediction). The series reconciles with the aggregate metrics and honoursinclude_pending. Omitted by default (additive / back-compatible); timeseries configs only.
0.11.1
- Sound neurosymbolic loop +
include_pendingpreview.neurosymbolic_comparisonnow acceptsinclude_pending=True— a read-only "what-if" that applies the accepted-but-pending discovered rules before the human approval gate (modeswitches topreview_pending, withn_pending_rules); default scores active rules only. Server-side, rule discovery is corrected to score candidates in the same space they're applied (greedy forward selection through the live evaluator), so an accepted rule set never degrades the backtest, and discovery now runs oningested-status datasets (previously it silently returned no rules). Cleaner generated module names throughout (explicitoperationIds on every route).
0.11.0
- Neurosymbolic rule discovery + neural-vs-neurosymbolic comparison. New
api.predictionsmethods:discover_prediction_rules(async — analyse a trained model's residuals, propose corrective adjustment/constraint rules, and A/B-test each against the expanding-window backtest; accepted rules are stored pending expert approval),discovered_prediction_rules(read the accepted rules with each rule'sfire_rateand backtestdelta), andneurosymbolic_comparison(async — head-to-head neural vs neurosymbolic R²/RMSE so you can see whether the symbolic layer earns its place). The two async methods poll the background job by default; passwait=Falsefor the raw 202 envelope. New headline exampleexamples/26_neurosymbolic_bond_yield.pywalks the full 10y-Treasury-yield flow end to end.
0.10.2
symbolic_forecastwhycontract — enriched (non-breaking superset).whynow surfaces the full set of materially-contributing accepted drivers the model induced and accepted on the holdout — not only the drivers firing on the most-recent row. Sowhyis informative even when nothing fires on the latest row (the case where it used to come back[]). Each entry carriesfired_on_latest_row(is this driver active now?),base_features(the human-named source feature(s) behind an engineered antecedent), andstandalone_holdout_skill(per-driver data-fit evidence); a new top-levelmax_standalone_holdout_skillreports the strongest single driver's skill.accepted_driversis now an alias ofwhy(same content, one source of truth). This is a non-breaking superset: theforecastvalue/interval,baseline, andskill_vs_persistenceare unchanged — consumers readingwhysimply get the full driver set instead of the fired-only subset. Read the enrichedwhyto explain a forecast even off the latest row.
0.10.1
- Trim-forward release: IP-redacted docstrings for the public SDK.
Metadata
Release files for ambertraceai 2.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ambertraceai-2.9.0.tar.gz | 1.3 MB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ambertraceai-2.9.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.0 MB
Release files / ambertraceai-2.9.0.tar.gz
| Download URL | ambertraceai-2.9.0.tar.gz |
|---|---|
| Size | 1.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
39150b1c46768496f81a01203f1c79db762fdf67d319ccb3b706678174b1e200
|
|
BLAKE2b-256 checksum How to use checksums |
c01da36ecd251e58249a0a05dcc9c2d1948017de5ca608013c1a1bb6eae57b38
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / ambertraceai-2.9.0-py3-none-any.whl
| Download URL | ambertraceai-2.9.0-py3-none-any.whl |
|---|---|
| Size | 760.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
937992c9a1d588a7d26cdc23c6c58bd3d6c89abee49541451895e15bdb504923
|
|
BLAKE2b-256 checksum How to use checksums |
4e75fe392d36931779275a69907cae2232ba3eb8e3c5532e5947dc8f44971c59
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logSecurity for ambertraceai 2.9.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ambertraceai-2.9.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.