decision-circuits
Deterministic decision gates over calibrated model answers.
pip install decision-circuits # no dependencies, Python 3.10+
The idea in one paragraph
Ask a model small, typed questions about a piece of state and get back
probabilities, not prose. Then decide with code: thresholds, AND/OR/NOT,
argmax with a confidence floor, majority votes, verification, ordinal
buckets. That code is a decision circuit. The model never sees it,
so the circuit is versionable, testable offline, and auditable per item:
every result carries its probability, its outcome, and a trace. And when
the probability sits too close to a threshold to call, the gate says so
(abstain or escalate) instead of guessing.
Background: Attaining LLM Certainty with AI Decision Circuits.
Sixty seconds
from decision_circuits import Circuit, Q, argmax
c = Circuit()
c.noul("pii", "Does this text contain personal information about a private individual?")
c.noul("angry", "Is the writer angry?")
c.choice("dept", "Which team should handle this?", {"billing": "Money, refunds", "technical": "Bugs, outages", "other": None})
c.gate("redact", Q("pii") >= 0.7, on_uncertain="escalate")
c.gate("route", argmax("dept", min_confidence=0.3))
c.gate("human", (Q("angry") | Q("pii")) >= 0.6)
Now something has to answer the questions. Anything with one method will do:
from decision_circuits.backends import SystemOne
jev = SystemOne(api_key=TYPESAFE_API_KEY, model="jev-latest") # TypeSafe's System One model
out = c.run(jev, "Card charged twice, refund NOW. My card ends in 4412.")
out["gates"]["redact"]
# {'value': True, 'p': 0.97, 'outcome': 'decided', 'trace': ['pii p=0.97'], ...}
out["gates"]["route"]
# {'value': 'billing', 'p': 1.0, 'confidence': 1.0, 'outcome': 'decided', ...}
No model handy? c.evaluate(answers) runs the gates on answers you
already have, and examples/01_first_circuit.py
does exactly that with hand-written numbers.
Where it plugs in
The point of a circuit is to sit inside an agent and make the small decisions the agent shouldn't be trusted with. Adapters exist for the three agent SDKs; each is a few lines over the same object.
| framework | what you get | install |
|---|---|---|
| LangChain agents | CircuitToolGuard middleware: judge each tool call, then allow, block, or pause for approval with the standard human-in-the-loop interrupt. CircuitRouter picks a model per run. |
[langchain] |
| OpenAI Agents SDK | circuit_input_guardrail, circuit_output_guardrail, circuit_tool_guardrail |
[openai-agents] |
| Claude Agent SDK | circuit_pre_tool_use hook returning allow / deny / ask; circuit_can_use_tool |
[claude] |
| LangGraph | as_node and route_on: gates as conditional edges |
none |
from decision_circuits.integrations.langchain import CircuitToolGuard
guard = CircuitToolGuard(c, jev, gate="block", tools=[delete_file, send_email])
agent = create_agent(model, tools=[read_file, delete_file, send_email], middleware=[guard])
Compared with a single yes/no classifier at a fixed 0.5 (what
langchain-typesafe's AutoModeMiddleware does), a circuit lets you
combine several questions, set the threshold and its uncertainty band
explicitly, and send the uncertain cases to a human rather than
silently allowing or blocking them. See docs/integrations.md.
Backends
Circuits are built for System One models: models that answer typed
questions with calibrated probabilities, in one pass, fast. That is what
makes a threshold and a band mean something. The SystemOne backend
talks to any server speaking the System One contract:
| backend | install | server |
|---|---|---|
SystemOne("https://api.typesafe.ai/v1/systemone", api_key, model="jev-latest") |
none | TypeSafe's Jev |
SystemOne("https://api.decisioncircuits.com/v1/systemone", api_key) |
none | the hosted circuit family: circuit-1.7b, circuit-8b, circuit-vl-4b (images), circuit-audio-7b (sound); free keys at decisioncircuits.com |
SystemOne("http://localhost:8901/v1/systemone", api_key) |
none | circuit, the same open-weights models run yourself |
A backend is any object with answer(state, questions, *, model=None),
so wrapping one (a cache, code-owned facts, a fallback) is a dozen
lines: examples/03_your_own_backend.py.
No S1 model yet? Chat models can stand in, with caveats.
OpenAILogprobs ([openai]) letters the options and reads next-token
logprobs; works with OpenAI, Azure OpenAI, Fireworks, vLLM, up to 20
options. Anthropic ([anthropic]) asks Claude for probabilities
through tool use, either stated in one call or sampled over k calls;
works with AnthropicBedrock and AnthropicVertex clients too. Both
are slower, cost a request per question or per sample, and a chat
model's stated confidence is not calibrated the way an S1 model's
output is. Check on a labeled sample before trusting a threshold.
The expression language
| form | meaning |
|---|---|
Q("noul_id") |
P(yes) for a yes/no question |
Q("choice_id")["option"], Q("score_id")[level] |
probability of one option or level |
G("gate_id"), G("gate_id")["value"] |
an earlier gate's probability, or one value of a categorical gate |
~e |
NOT: 1 - p |
a & b & c |
AND: product (independence assumed and recorded in the trace) |
a | b |
OR: 1 - prod(1 - p) |
e >= tau, e.at(tau) |
threshold with an uncertainty band; >= builds a node, it does not compare |
argmax("choice", min_confidence=...) |
top option; abstain below the floor |
majority("c1", "c2", "c3") |
vote across paraphrased questions |
verify("choice", check=Q("supported"), tau=...) |
a negative checker: escalate when the check does not support the pick |
order("score", [c1, c2, ...]) |
bucket the expected score by cutpoints |
at_least(k, "multi"), at_least(k, a, b, c) |
at least k of a multi's options, or of the references, hold (independence assumed) |
count("multi"), G("n")[2] |
the most likely number that hold; index it for P(exactly that many) |
consistent(a, b, relation="complement") |
two answers checked against each other: same, complement, or implies; escalate when they disagree |
Q("multi")["option"], Q("locate")["none"] |
one multi option's probability; the chance a locate question found nothing |
Every gate takes on_uncertain="abstain" | "escalate" | "default"
(with default=value) and band=width. c.to_mermaid() draws it.
What changes the decision
Change the input, run the circuit again, and compare. Each intervention is Pearl's do(): the effect is measured, not explained after the fact, and it works on any backend.
from decision_circuits import drop, set_to
r = refund_circuit.intervene(
api,
ticket,
{
"no receipt": drop("receipt"), # remove a field
"vip": set_to("customer", "tier", "vip"), # do(tier = "vip")
},
)
r["effects"]["no receipt"]["flipped"] # ['pay']: the gates whose decision changed
r["effects"]["vip"]["answers"]["desk"] # {'option': 'general', 'p_before': 0.9, 'p_after': 0.1, 'dp': -0.8}
refund_circuit.ablate(api, ticket) # remove each field (dict) or sentence (text) in turn
One backend call per intervention, plus one for the baseline (pass
baseline= to reuse a run); workers sets how many run at once.
Several options, and pointing at the text
Circuit v2 models (circuit-1.7b v2.0 and later) answer two more kinds of question about a text state:
c.multi("issues", "Which problems does the customer report?", {"late": None, "damaged": None, "wrong_item": None})
c.locate("ask", "Which sentence says what they want?", none="the message never says")
c.gate("escalate", at_least(2, "issues"), on_uncertain="escalate")
c.gate("says_what", ~Q("ask")["none"] >= 0.5)
multi gives every option its own probability (none may apply, or all). locate points at a
field, a list element or a sentence of the state, or at "none". See
examples/13_what_v2_reads.py.
Tracing
With pip install "decision-circuits[otel]" and any OpenTelemetry SDK and exporter
configured, every run is a span:
decision_circuits.run gen_ai.request.model, gen_ai.response.id, gen_ai.usage.input_tokens,
│ decision_circuits.escalated = ["pay"], decision_circuits.abstained = []
│ event decision_circuits.answer question, type, pick, p (one per answer; per option for a multi)
│ event decision_circuits.gate gate, value, outcome, p, trace (one per gate)
└── decision_circuits.backend the model call
intervene and ablate add a decision_circuits.intervene span over one run per edited
state, threads included. SystemOne sends traceparent, so a traced server joins the same
trace. Circuits called inside LangChain, LangGraph or an agent SDK nest under that
framework's spans. The state is never recorded; question ids, option names, probabilities
and gate traces are. Without OpenTelemetry installed, nothing is imported and nothing is
recorded.
Images and audio
The state can be a picture or a recording; the circuit does not change.
from decision_circuits import Audio, Circuit, Image, Q, argmax
out = receipt_circuit.run(api, Image("receipt.png"), model="circuit-vl-4b")
out = call_circuit.run(api, Audio("call.wav", text="Inbound, Tuesday"), model="circuit-audio-7b")
Image and Audio take a path, a URL, or bytes, plus an optional
caption, and are sent as {"image": <data URI>, "text": ...}. See
examples/12_images_and_audio.py.
Proved
The gate arithmetic is proved in Lean 4 (proofs/, checked in CI): the count distribution
behind at_least, count, and and or (sums to 1, and is the product, or is one minus
the product of complements, order does not matter, at_least falls as k rises), and pick
dominance: a decided gate never reads an option it did not pick as likelier than the one it
did. tests/test_properties.py checks the Python against the same statements. The models are
not proved; they are measured.
Examples
Numbered, each self-contained, in examples/:
- a circuit with hand-written answers (offline)
- the same circuit answered by Jev
- writing a backend (offline)
- LangChain tool guard
- OpenAI Agents guardrails
- Claude Agent SDK hook
- integrating a framework that has no adapter (offline)
- a refund desk: seven questions, six gates, code-owned facts mixed with model judgments, five tickets routed to four queues (offline or Jev)
- the refund desk answered by Claude, stated versus sampled probabilities
- the refund desk answered through OpenAI-compatible logprobs (OpenAI, Fireworks, vLLM)
- the article's water-utility call center: the two-parser, negative-checker circuit from Attaining LLM Certainty with AI Decision Circuits, on its original 100 calls, with the article's confidence tiers and cost model
Docs
- docs/concepts.md: questions, answers, gates, uncertainty, the trace
- docs/integrations.md: each SDK adapter and what "ask" means there
- docs/extending.md: backends, gate ops, adapters
Status
0.4.x, alpha. The wire format is TypeSafe's POST /v1/systemone request plus a
gates block, and for the vision and audio models a media state. MIT.
See CHANGELOG.md.
Changed in 0.3.0: Circuit.run takes a backend, not an HTTP client.
Where 0.2 wrote c.run(httpx.Client(), state, url=..., headers=...),
write c.run(SystemOne(url, api_key=...), state). The full server
response (usage, model) is on SystemOne.last_response. The core no
longer depends on pydantic.
Metadata
Release files for decision-circuits 0.5.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| decision_circuits-0.5.5.tar.gz | 59.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| decision_circuits-0.5.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 113.5 kB
Release files / decision_circuits-0.5.5.tar.gz
| Download URL | decision_circuits-0.5.5.tar.gz |
|---|---|
| Size | 59.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6d2daa1db3feea57cacd5c5c782f7b78aa787b8d8dcbfe61b04eb17d5676d250
|
|
BLAKE2b-256 checksum How to use checksums |
d41263dc8cb153ed9e818bfc31e213c06ac8f07753d1504f0b1cd1e9e767f34c
|
| 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 Sep 24, 2026.
Transparency logRelease files / decision_circuits-0.5.5-py3-none-any.whl
| Download URL | decision_circuits-0.5.5-py3-none-any.whl |
|---|---|
| Size | 53.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
520cd6df54981004807ac90f87e151e23d828ef99449373c803dab9e0d6d6970
|
|
BLAKE2b-256 checksum How to use checksums |
ce27026ce156ed6c4f05bed3b81aec176543ed397be19922a6705456ba3ce0c3
|
| 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 Sep 24, 2026.
Transparency log