Exploring uncertainty-aware programming for AI agents: standardizing what
an agent does once it has a confidence number — escalate, verify, ask a
human — as explicit, enforced control flow (Prediction[T],
on_confidence) instead of ad hoc if statements scattered through the
codebase.
What this project is not: a way to make confidence numbers themselves
trustworthy. Whether confidence=0.6 means anything is a statistics/ML
problem — it's exactly as hard in plain Python as it is here, and no
library removes it. aip only standardizes what happens after you've
decided to trust a number; getting that number right is still on you,
same as it would be without this package.
from aip import Agent, Evidence, Policy, Prediction, confidence, on_confidence
class CustomerSupport(Agent):
model = "gpt"
policy = Policy(max_steps=5, require_evidence=True)
def goal(self, question: str) -> Prediction[str]:
samples = sample_model(question, n=5) # your provider call, x5
return Prediction(
value=majority_vote(samples),
confidence=confidence.from_self_consistency(samples),
evidence=[Evidence(source="policy.md", excerpt="30-day return window")],
)
@on_confidence(below=0.70)
def verify(self, prediction: Prediction[str]) -> Prediction[str]:
... # re-check and return an updated, higher-confidence Prediction
@on_confidence(below=0.40)
def ask_human(self, prediction: Prediction[str]) -> None:
... # terminal escalation — no re-check, run() stops hereEarly prototype: an embedded Python DSL, not a new language/syntax.
aip/ is a small, working package — Agent, Prediction, Policy,
on_confidence, tool, confidence — no parser, no compiler, just plain
Python classes and decorators, so it gets IDE/type-checker support for free
and costs nothing to iterate on.
Where this landed, in one paragraph: the control-flow layer
(Agent/Policy/on_confidence/resolve()) is validated — it caught a
real rule-ordering bug class in a head-to-head comparison against
hand-written Python, and survived a structural stress test (a multi-step
agent) that forced a real refactor, not a cosmetic one. Grounded,
evidence-based confidence checking (RAG + from_entailment) is the one
confidence-computation approach with a positive result, validated twice —
once with mocked data, once fully live (real retrieval, real generation,
real LLM-as-judge — examples/rag_live.py). Self-contained confidence
estimation with no evidence to check against — self-consistency in three
forms, self-report in two — was tested against a real model across four
questions designed to need different confidence levels, and all five
failed to discriminate reliably; see
docs/confidence-findings.md for why each
one fails and what hasn't been tried yet. That's the project's actual,
evidence-backed scope right now: a validated control-flow layer, one
validated way to compute trustworthy confidence (when you have something
to check against), and a documented open problem for when you don't.
Run it:
pip install -e ".[dev]"
python examples/customer_support.py
pytest
What's validated so far: the control-flow mechanics — when multiple
on_confidence rules match, the lowest threshold (most severe escalation)
wins; handlers that return a new Prediction get re-evaluated against the
rules; policy.require_evidence / policy.max_steps are enforced as
exceptions (EvidenceRequired, StepLimitExceeded).
aip/confidence.py exists to make that "on you" part less error-prone
to implement, not to solve it — it's a set of tested, reusable
implementations of known techniques, so you're not re-deriving
self-consistency voting or entailment scoring from scratch in every
project. Self-reported "how confident are you" scores from an LLM are
known to be poorly calibrated, so that's included only as one explicit,
visible option among several interchangeable strategies — pick one per
Prediction, or combine them:
| Strategy | Signal | Needs |
|---|---|---|
from_self_consistency |
agreement rate across N sampled outputs | any provider, N calls |
from_logprobs |
geometric mean of token log-probabilities | provider exposes logprobs |
from_entailment |
a verifier/NLI model's P(evidence entails value) | a second, separate model |
from_self_report |
the model's own stated confidence | nothing extra — and the least trustworthy |
combine(*scores, weights=...) |
weighted mean of any of the above | — |
naive_lexical_overlap / best_matching_evidence |
word-overlap stand-in for from_entailment's score, until you wire up a real verifier |
— |
examples/customer_support.py wires up from_self_consistency end to end
(samples are still mocked — no provider is called yet — but the confidence
number is genuinely computed from sample agreement, not hand-picked).
Two RAG examples exercise from_entailment end to end — retrieve →
generate → check the answer against retrieved evidence:
examples/rag_support.pychecks one whole answer against its evidence, and catches a hallucination: a mocked answer fabricates a claim the retrieved doc never made, and low entailment routes it toask_human()instead of returning it silently.examples/rag_policy_summary.pyfollows a more staged, industry-typical pipeline (retrieve → rerank → generate → decompose into claims → per-claim grounding check → aggregate) and catches something the first example's whole-answer check would have missed: one fabricated claim buried in an otherwise well-grounded, multi-sentence answer, invisible in the aggregate confidence (0.74 — looks fine) but caught the moment each claim is checked individually.
naive_lexical_overlap/best_matching_evidence started as near-identical
code duplicated across both RAG examples; once a second example showed the
same function was genuinely needed twice, it moved into aip/confidence.py
— the orchestration around it (single-answer vs. per-claim, "regenerate"
vs. "widen the search pool") differs enough between the two examples that
a shared RAGAgent-style base class isn't justified yet.
examples/researcher.py stress-tests the design against a multi-step
agent (see Agent.resolve() in aip/agent.py).
None of these strategies are calibrated out of the box — a 0.7 here is a
relative signal, not "70% empirically correct," until you check it
against labeled examples (e.g. temperature/Platt scaling). That's true
whether you call confidence.from_self_consistency or hand-roll the same
voting logic yourself; the function just saves you from re-writing and
re-debugging it.
That calibration check has been run, against a real provider, and the
full results — including a positive one — are in
docs/confidence-findings.md.
examples/customer_support_live.py and examples/confidence_comparison.py
wire from_self_consistency (plus embedding-based clustering and
self-report) up to real OpenAI calls across four test questions chosen to
need different confidence levels — and none of those five methods
reliably separates "the model knows this" from "the model is guessing or
appropriately declining." examples/rag_live.py is the positive
follow-up: real embedding-based retrieval, real generation, and a real
LLM-as-judge entailment check (no mocked data, no lexical-overlap proxy)
on top of the one path that did work — grounded/evidence-based
checking, the same mechanism as the mocked RAG examples below, now run
against a live model end to end.
aip/ the package: Agent, Prediction, Policy, on_confidence, tool, confidence
examples/ runnable example agents
tests/ pytest suite (also doubles as executable spec for the rules)
docs/ confidence-findings.md — the real-API investigation results