stated-confidence 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stated_confidence/__init__.py +41 -0
- stated_confidence/basis.py +260 -0
- stated_confidence/checks.py +84 -0
- stated_confidence/guidance.py +52 -0
- stated_confidence/levels.py +152 -0
- stated_confidence/model.py +107 -0
- stated_confidence/presentation.py +70 -0
- stated_confidence/py.typed +0 -0
- stated_confidence/pydantic_ai.py +47 -0
- stated_confidence/taxonomy.py +57 -0
- stated_confidence/terms.py +19 -0
- stated_confidence-0.1.0.dist-info/METADATA +129 -0
- stated_confidence-0.1.0.dist-info/RECORD +15 -0
- stated_confidence-0.1.0.dist-info/WHEEL +4 -0
- stated_confidence-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""stated-confidence: make agents say how sure they are, and on what basis.
|
|
2
|
+
|
|
3
|
+
An LLM asked to attach a confidence *level* and an evidential *basis* to
|
|
4
|
+
each claim it makes tends to fabricate less: naming the kind of evidence
|
|
5
|
+
forces a provenance check the model would otherwise skip. This package is
|
|
6
|
+
that schema, the vocabulary behind it, prompt guidance for filling it, and
|
|
7
|
+
the consistency checks that send hedging-without-meaning-it back to the
|
|
8
|
+
model.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .basis import (
|
|
12
|
+
BasisEnum,
|
|
13
|
+
DataAnalysisBasis,
|
|
14
|
+
DecisionBasis,
|
|
15
|
+
GeneralKnowledgeBasis,
|
|
16
|
+
)
|
|
17
|
+
from .checks import check_all, check_consistency, iter_confidences
|
|
18
|
+
from .guidance import confidence_instructions
|
|
19
|
+
from .levels import ConfidenceLevel, ConfidenceType
|
|
20
|
+
from .model import Confidence
|
|
21
|
+
from .presentation import SCALE_EXPLANATION, Tone, describe, tone
|
|
22
|
+
from .terms import Term
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"SCALE_EXPLANATION",
|
|
26
|
+
"BasisEnum",
|
|
27
|
+
"Confidence",
|
|
28
|
+
"ConfidenceLevel",
|
|
29
|
+
"ConfidenceType",
|
|
30
|
+
"DataAnalysisBasis",
|
|
31
|
+
"DecisionBasis",
|
|
32
|
+
"GeneralKnowledgeBasis",
|
|
33
|
+
"Term",
|
|
34
|
+
"Tone",
|
|
35
|
+
"check_all",
|
|
36
|
+
"check_consistency",
|
|
37
|
+
"confidence_instructions",
|
|
38
|
+
"describe",
|
|
39
|
+
"iter_confidences",
|
|
40
|
+
"tone",
|
|
41
|
+
]
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Basis vocabularies: *what kind of evidence* stands behind a claim.
|
|
2
|
+
|
|
3
|
+
The level scale is fixed; the basis set is the extension point. A domain
|
|
4
|
+
declares its own by subclassing ``BasisEnum`` and filling ``__terms__``
|
|
5
|
+
and ``__fallback__``. Three sets ship: general knowledge, data analysis,
|
|
6
|
+
and decisions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from typing import ClassVar
|
|
12
|
+
|
|
13
|
+
from .levels import normalize_token
|
|
14
|
+
from .terms import Term
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("stated_confidence")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BasisEnum(StrEnum):
|
|
20
|
+
"""Base class for basis vocabularies.
|
|
21
|
+
|
|
22
|
+
``__terms__`` maps each value to its ``Term``; ``__fallback__`` is the
|
|
23
|
+
value substituted for anything unrecognized. Models occasionally invent
|
|
24
|
+
plausible values (one emitted ``INDUSTRY_STANDARD`` before it existed),
|
|
25
|
+
and one bad string must not fail a whole response. The substitution is
|
|
26
|
+
logged at WARNING on the ``stated_confidence`` logger so a basis set
|
|
27
|
+
can be grown deliberately from what models actually say.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
__terms__: ClassVar[dict[str, Term]] = {}
|
|
31
|
+
__fallback__: ClassVar[str] = ""
|
|
32
|
+
__registry__: ClassVar[list[type["BasisEnum"]]] = []
|
|
33
|
+
|
|
34
|
+
def __init_subclass__(cls, **kwargs: object) -> None:
|
|
35
|
+
super().__init_subclass__(**kwargs)
|
|
36
|
+
BasisEnum.__registry__.append(cls)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def registered(cls) -> "list[type[BasisEnum]]":
|
|
40
|
+
"""Every basis set defined so far, shipped and user-defined."""
|
|
41
|
+
return list(cls.__registry__)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def _missing_(cls, value: object) -> "BasisEnum | None":
|
|
45
|
+
if not isinstance(value, str):
|
|
46
|
+
return None
|
|
47
|
+
token = normalize_token(value)
|
|
48
|
+
for member in cls:
|
|
49
|
+
if member.value == token:
|
|
50
|
+
return member
|
|
51
|
+
if not cls.__fallback__:
|
|
52
|
+
return None
|
|
53
|
+
logger.warning(
|
|
54
|
+
"Unrecognized %s %r; using %s",
|
|
55
|
+
cls.__name__,
|
|
56
|
+
value,
|
|
57
|
+
cls.__fallback__,
|
|
58
|
+
)
|
|
59
|
+
return cls(cls.__fallback__)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def fallback(cls) -> "BasisEnum":
|
|
63
|
+
return cls(cls.__fallback__)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def term(self) -> Term:
|
|
67
|
+
return type(self).__terms__[self.value]
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def strength(self) -> str | None:
|
|
71
|
+
return self.term.strength
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def weakest(cls) -> list["BasisEnum"]:
|
|
75
|
+
return [member for member in cls if member.strength == "weak"]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class GeneralKnowledgeBasis(BasisEnum):
|
|
79
|
+
"""For claims about the world: facts, history, science, fiction."""
|
|
80
|
+
|
|
81
|
+
DOCUMENTED_FACT = "DOCUMENTED_FACT"
|
|
82
|
+
SCIENTIFIC_CONSENSUS = "SCIENTIFIC_CONSENSUS"
|
|
83
|
+
HISTORICAL_RECORD = "HISTORICAL_RECORD"
|
|
84
|
+
INDUSTRY_STANDARD = "INDUSTRY_STANDARD"
|
|
85
|
+
EXPERT_OPINION = "EXPERT_OPINION"
|
|
86
|
+
THEORETICAL_FRAMEWORK = "THEORETICAL_FRAMEWORK"
|
|
87
|
+
FICTIONAL_UNIVERSE = "FICTIONAL_UNIVERSE"
|
|
88
|
+
SPECULATION = "SPECULATION"
|
|
89
|
+
EDUCATED_GUESS = "EDUCATED_GUESS"
|
|
90
|
+
|
|
91
|
+
__fallback__ = "EXPERT_OPINION"
|
|
92
|
+
__terms__ = {
|
|
93
|
+
"DOCUMENTED_FACT": Term(
|
|
94
|
+
label="Documented fact",
|
|
95
|
+
description="Stated directly in recorded, citable sources.",
|
|
96
|
+
guidance="recorded evidence you could cite",
|
|
97
|
+
strength="strong",
|
|
98
|
+
),
|
|
99
|
+
"SCIENTIFIC_CONSENSUS": Term(
|
|
100
|
+
label="Scientific consensus",
|
|
101
|
+
description="Supported by peer-reviewed scientific agreement.",
|
|
102
|
+
guidance="peer-reviewed agreement",
|
|
103
|
+
strength="strong",
|
|
104
|
+
),
|
|
105
|
+
"HISTORICAL_RECORD": Term(
|
|
106
|
+
label="Historical record",
|
|
107
|
+
description="Part of the documented historical record.",
|
|
108
|
+
guidance="documented history",
|
|
109
|
+
strength="strong",
|
|
110
|
+
),
|
|
111
|
+
"INDUSTRY_STANDARD": Term(
|
|
112
|
+
label="Industry standard",
|
|
113
|
+
description=(
|
|
114
|
+
"Established standards or widespread industry practice."
|
|
115
|
+
),
|
|
116
|
+
guidance=(
|
|
117
|
+
"established standards and widespread engineering practice"
|
|
118
|
+
),
|
|
119
|
+
strength="strong",
|
|
120
|
+
),
|
|
121
|
+
"EXPERT_OPINION": Term(
|
|
122
|
+
label="Expert opinion",
|
|
123
|
+
description="Based on the judgment of subject-matter experts.",
|
|
124
|
+
guidance="the judgment of subject-matter experts",
|
|
125
|
+
strength="moderate",
|
|
126
|
+
),
|
|
127
|
+
"THEORETICAL_FRAMEWORK": Term(
|
|
128
|
+
label="Theoretical framework",
|
|
129
|
+
description="Follows from an established theory or model.",
|
|
130
|
+
guidance="an established theory or model",
|
|
131
|
+
strength="moderate",
|
|
132
|
+
),
|
|
133
|
+
"FICTIONAL_UNIVERSE": Term(
|
|
134
|
+
label="Fictional universe",
|
|
135
|
+
description="True within a fictional or creative work.",
|
|
136
|
+
guidance="creative content that is true within its fiction",
|
|
137
|
+
strength="moderate",
|
|
138
|
+
),
|
|
139
|
+
"SPECULATION": Term(
|
|
140
|
+
label="Speculation",
|
|
141
|
+
description="An unverified theory or possibility.",
|
|
142
|
+
guidance="unverified theories",
|
|
143
|
+
strength="weak",
|
|
144
|
+
),
|
|
145
|
+
"EDUCATED_GUESS": Term(
|
|
146
|
+
label="Educated guess",
|
|
147
|
+
description="A plausible inference made without direct evidence.",
|
|
148
|
+
guidance="plausible inference with no direct evidence",
|
|
149
|
+
strength="weak",
|
|
150
|
+
),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class DataAnalysisBasis(BasisEnum):
|
|
155
|
+
"""For claims an agent makes about data it retrieved or computed."""
|
|
156
|
+
|
|
157
|
+
QUERY_RESULT = "QUERY_RESULT"
|
|
158
|
+
COMPUTED = "COMPUTED"
|
|
159
|
+
SCHEMA_INFERENCE = "SCHEMA_INFERENCE"
|
|
160
|
+
GENERAL_KNOWLEDGE = "GENERAL_KNOWLEDGE"
|
|
161
|
+
ASSUMPTION = "ASSUMPTION"
|
|
162
|
+
|
|
163
|
+
__fallback__ = "ASSUMPTION"
|
|
164
|
+
__terms__ = {
|
|
165
|
+
"QUERY_RESULT": Term(
|
|
166
|
+
label="Query result",
|
|
167
|
+
description="Read directly from the rows the query returned.",
|
|
168
|
+
guidance="a value read directly from returned rows",
|
|
169
|
+
strength="strong",
|
|
170
|
+
),
|
|
171
|
+
"COMPUTED": Term(
|
|
172
|
+
label="Computed",
|
|
173
|
+
description=(
|
|
174
|
+
"Arithmetic over the returned rows, such as a ratio, "
|
|
175
|
+
"difference, or trend."
|
|
176
|
+
),
|
|
177
|
+
guidance=(
|
|
178
|
+
"arithmetic you did over returned rows "
|
|
179
|
+
"(a ratio, difference, or trend)"
|
|
180
|
+
),
|
|
181
|
+
strength="strong",
|
|
182
|
+
),
|
|
183
|
+
"SCHEMA_INFERENCE": Term(
|
|
184
|
+
label="Schema inference",
|
|
185
|
+
description=(
|
|
186
|
+
"Inferred from table or column names and the schema, "
|
|
187
|
+
"not verified against data."
|
|
188
|
+
),
|
|
189
|
+
guidance=(
|
|
190
|
+
"something inferred from table or column names "
|
|
191
|
+
"rather than checked against data"
|
|
192
|
+
),
|
|
193
|
+
strength="moderate",
|
|
194
|
+
),
|
|
195
|
+
"GENERAL_KNOWLEDGE": Term(
|
|
196
|
+
label="General knowledge",
|
|
197
|
+
description=(
|
|
198
|
+
"Background knowledge about the domain, not from this data."
|
|
199
|
+
),
|
|
200
|
+
guidance="background knowledge about the domain, not this data",
|
|
201
|
+
strength="moderate",
|
|
202
|
+
),
|
|
203
|
+
"ASSUMPTION": Term(
|
|
204
|
+
label="Assumption",
|
|
205
|
+
description="A premise that was not checked against the data.",
|
|
206
|
+
guidance="a premise you did not check against the data",
|
|
207
|
+
strength="weak",
|
|
208
|
+
),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class DecisionBasis(BasisEnum):
|
|
213
|
+
"""For recommendations and decisions."""
|
|
214
|
+
|
|
215
|
+
POLICY = "POLICY"
|
|
216
|
+
MEASURED_OUTCOME = "MEASURED_OUTCOME"
|
|
217
|
+
PRECEDENT = "PRECEDENT"
|
|
218
|
+
TRADEOFF_ANALYSIS = "TRADEOFF_ANALYSIS"
|
|
219
|
+
EXPERT_JUDGMENT = "EXPERT_JUDGMENT"
|
|
220
|
+
ASSUMPTION = "ASSUMPTION"
|
|
221
|
+
|
|
222
|
+
__fallback__ = "ASSUMPTION"
|
|
223
|
+
__terms__ = {
|
|
224
|
+
"POLICY": Term(
|
|
225
|
+
label="Policy",
|
|
226
|
+
description="Required by a stated policy, rule, or constraint.",
|
|
227
|
+
guidance="a stated policy, rule, or hard constraint",
|
|
228
|
+
strength="strong",
|
|
229
|
+
),
|
|
230
|
+
"MEASURED_OUTCOME": Term(
|
|
231
|
+
label="Measured outcome",
|
|
232
|
+
description="Observed results from measurement or experiment.",
|
|
233
|
+
guidance="results actually measured or observed",
|
|
234
|
+
strength="strong",
|
|
235
|
+
),
|
|
236
|
+
"PRECEDENT": Term(
|
|
237
|
+
label="Precedent",
|
|
238
|
+
description="What was done in comparable past cases.",
|
|
239
|
+
guidance="what comparable past cases did",
|
|
240
|
+
strength="moderate",
|
|
241
|
+
),
|
|
242
|
+
"TRADEOFF_ANALYSIS": Term(
|
|
243
|
+
label="Trade-off analysis",
|
|
244
|
+
description="Weighing the options against stated criteria.",
|
|
245
|
+
guidance="weighing the options against stated criteria",
|
|
246
|
+
strength="moderate",
|
|
247
|
+
),
|
|
248
|
+
"EXPERT_JUDGMENT": Term(
|
|
249
|
+
label="Expert judgment",
|
|
250
|
+
description="Based on the judgment of subject-matter experts.",
|
|
251
|
+
guidance="the judgment of subject-matter experts",
|
|
252
|
+
strength="moderate",
|
|
253
|
+
),
|
|
254
|
+
"ASSUMPTION": Term(
|
|
255
|
+
label="Assumption",
|
|
256
|
+
description="A premise that was taken as given, not verified.",
|
|
257
|
+
guidance="a premise taken as given without verification",
|
|
258
|
+
strength="weak",
|
|
259
|
+
),
|
|
260
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Consistency rules between what a ``Confidence`` says about itself.
|
|
2
|
+
|
|
3
|
+
Each rule catches a way of hedging without meaning it: a HIGH level on a
|
|
4
|
+
basis that admits there is no evidence, a SPECULATIVE level on a basis
|
|
5
|
+
that says the claim is documented, or a non-HIGH level with no reason
|
|
6
|
+
given. Violations are plain sentences a
|
|
7
|
+
model can act on when they come back as a retry.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from .levels import ConfidenceLevel, ConfidenceType
|
|
16
|
+
from .model import Confidence
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def check_consistency(confidence: Confidence) -> list[str]:
|
|
20
|
+
"""Return every violated rule, or an empty list when consistent."""
|
|
21
|
+
violations: list[str] = []
|
|
22
|
+
level = confidence.level
|
|
23
|
+
basis = confidence.basis
|
|
24
|
+
if level is ConfidenceLevel.SPECULATIVE and basis.strength == "strong":
|
|
25
|
+
violations.append(
|
|
26
|
+
f"level SPECULATIVE contradicts basis {basis.value} "
|
|
27
|
+
f"({basis.term.description.rstrip('.').lower()}); a claim that "
|
|
28
|
+
"rests on that is asserted, not speculated. Raise the level or "
|
|
29
|
+
"choose the basis the idea actually rests on."
|
|
30
|
+
)
|
|
31
|
+
if level is ConfidenceLevel.HIGH and basis.strength == "weak":
|
|
32
|
+
violations.append(
|
|
33
|
+
f"level HIGH contradicts basis {basis.value} "
|
|
34
|
+
f"({basis.term.description.rstrip('.').lower()}); lower the "
|
|
35
|
+
"level or choose the basis that actually supports the claim."
|
|
36
|
+
)
|
|
37
|
+
if (
|
|
38
|
+
level is ConfidenceLevel.HIGH
|
|
39
|
+
and confidence.type is ConfidenceType.SPECULATIVE
|
|
40
|
+
):
|
|
41
|
+
violations.append(
|
|
42
|
+
"level HIGH contradicts type SPECULATIVE; lower the level or "
|
|
43
|
+
"change the type."
|
|
44
|
+
)
|
|
45
|
+
if level is not ConfidenceLevel.HIGH and not confidence.reasoning.strip():
|
|
46
|
+
violations.append(
|
|
47
|
+
f"reasoning is empty; say in a sentence why the confidence is "
|
|
48
|
+
f"{level.value}."
|
|
49
|
+
)
|
|
50
|
+
return violations
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def iter_confidences(
|
|
54
|
+
value: Any, path: str = ""
|
|
55
|
+
) -> Iterator[tuple[str, Confidence]]:
|
|
56
|
+
"""Yield every ``Confidence`` reachable from ``value`` with its path.
|
|
57
|
+
|
|
58
|
+
Walks Pydantic models, lists, tuples, and dict values, so an output
|
|
59
|
+
model holding ``findings: list[Finding]`` needs no adapter.
|
|
60
|
+
"""
|
|
61
|
+
if isinstance(value, Confidence):
|
|
62
|
+
yield path, value
|
|
63
|
+
return
|
|
64
|
+
if isinstance(value, BaseModel):
|
|
65
|
+
for name in type(value).model_fields:
|
|
66
|
+
child = getattr(value, name)
|
|
67
|
+
yield from iter_confidences(
|
|
68
|
+
child, f"{path}.{name}" if path else name
|
|
69
|
+
)
|
|
70
|
+
elif isinstance(value, list | tuple):
|
|
71
|
+
for index, item in enumerate(value):
|
|
72
|
+
yield from iter_confidences(item, f"{path}[{index}]")
|
|
73
|
+
elif isinstance(value, dict):
|
|
74
|
+
for key, item in value.items():
|
|
75
|
+
yield from iter_confidences(item, f"{path}[{key!r}]")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def check_all(value: Any) -> list[str]:
|
|
79
|
+
"""Violations for every ``Confidence`` in ``value``, prefixed by path."""
|
|
80
|
+
return [
|
|
81
|
+
f"{path}: {violation}" if path else violation
|
|
82
|
+
for path, confidence in iter_confidences(value)
|
|
83
|
+
for violation in check_consistency(confidence)
|
|
84
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Prompt text that teaches a model to fill ``Confidence`` honestly.
|
|
2
|
+
|
|
3
|
+
The field descriptions in the JSON schema carry the same vocabulary; this
|
|
4
|
+
block belongs in the system prompt or instructions, where models weight
|
|
5
|
+
it more than schema descriptions and where the rule about numbers lands
|
|
6
|
+
before the model starts writing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .basis import BasisEnum
|
|
10
|
+
from .levels import LEVEL_TERMS, ConfidenceLevel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def confidence_instructions(
|
|
14
|
+
basis_set: type[BasisEnum],
|
|
15
|
+
*,
|
|
16
|
+
field_name: str = "confidence",
|
|
17
|
+
localized: bool = False,
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Instructions for filling every ``field_name`` in the output.
|
|
20
|
+
|
|
21
|
+
``localized`` adds the rule that free text follows the user's language
|
|
22
|
+
while enum values stay uppercase English.
|
|
23
|
+
"""
|
|
24
|
+
levels = "; ".join(
|
|
25
|
+
f"{level.value} for {LEVEL_TERMS[level].guidance}"
|
|
26
|
+
for level in ConfidenceLevel
|
|
27
|
+
)
|
|
28
|
+
bases = "; ".join(
|
|
29
|
+
f"{member.value} for {member.term.guidance}" for member in basis_set
|
|
30
|
+
)
|
|
31
|
+
weakest = " or ".join(member.value for member in basis_set.weakest())
|
|
32
|
+
lines = [
|
|
33
|
+
f"For every `{field_name}`, state how sure you are and what that "
|
|
34
|
+
"certainty rests on. Before writing a claim, ask what kind of "
|
|
35
|
+
"evidence you actually have for it; if the honest answer is "
|
|
36
|
+
f"{weakest}, say so rather than presenting the claim as established.",
|
|
37
|
+
f"`{field_name}.level` must be exactly one of: {levels}. Never use "
|
|
38
|
+
"numbers or percentages for the level. SPECULATIVE is the level for "
|
|
39
|
+
"an idea you are offering, not a claim you are making; use it "
|
|
40
|
+
"freely, and say in `reasoning` what the idea extends.",
|
|
41
|
+
f"`{field_name}.basis` must be exactly one of: {bases}.",
|
|
42
|
+
f"`{field_name}.reasoning` is one or two sentences on why that "
|
|
43
|
+
"level and basis apply. `evidence` lists what concretely backs the "
|
|
44
|
+
"claim; leave it empty when nothing concrete does.",
|
|
45
|
+
]
|
|
46
|
+
if localized:
|
|
47
|
+
lines.append(
|
|
48
|
+
"Write `reasoning` and `label` in the language of the user's "
|
|
49
|
+
"input; `level`, `basis`, and `type` values stay in uppercase "
|
|
50
|
+
"English."
|
|
51
|
+
)
|
|
52
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""The confidence level scale and the kind-of-information type."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
|
|
6
|
+
from .terms import Term
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("stated_confidence")
|
|
9
|
+
|
|
10
|
+
NUMERIC_HIGH = 0.8
|
|
11
|
+
NUMERIC_MEDIUM = 0.5
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize_token(value: str) -> str:
|
|
15
|
+
return value.strip().upper().replace(" ", "_").replace("-", "_")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfidenceLevel(StrEnum):
|
|
19
|
+
"""How sure the model is that a claim is true.
|
|
20
|
+
|
|
21
|
+
``SPECULATIVE`` is not a low rung on the scale: it is the level for
|
|
22
|
+
ideas the model offers rather than asserts (an extension a source
|
|
23
|
+
suggests, a hypothesis, a what-if). Giving the model that room is part
|
|
24
|
+
of what keeps it from dressing ideas up as facts.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
HIGH = "HIGH"
|
|
28
|
+
MEDIUM = "MEDIUM"
|
|
29
|
+
LOW = "LOW"
|
|
30
|
+
SPECULATIVE = "SPECULATIVE"
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def _missing_(cls, value: object) -> "ConfidenceLevel | None":
|
|
34
|
+
# Models ignore "no numbers" often enough that mapping a score is
|
|
35
|
+
# cheaper than failing the whole response; case drift ("High")
|
|
36
|
+
# is the historical storage format.
|
|
37
|
+
if isinstance(value, bool):
|
|
38
|
+
return None
|
|
39
|
+
if isinstance(value, int | float):
|
|
40
|
+
return cls.from_score(float(value))
|
|
41
|
+
if not isinstance(value, str):
|
|
42
|
+
return None
|
|
43
|
+
token = normalize_token(value)
|
|
44
|
+
for member in cls:
|
|
45
|
+
if member.value == token:
|
|
46
|
+
return member
|
|
47
|
+
try:
|
|
48
|
+
return cls.from_score(float(token))
|
|
49
|
+
except ValueError:
|
|
50
|
+
logger.warning(
|
|
51
|
+
"Unrecognized confidence level %r; using MEDIUM", value
|
|
52
|
+
)
|
|
53
|
+
return cls.MEDIUM
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_score(cls, score: float) -> "ConfidenceLevel":
|
|
57
|
+
"""Map a 0-1 (or 0-100) probability-style score onto the scale."""
|
|
58
|
+
if score > 1:
|
|
59
|
+
score /= 100
|
|
60
|
+
if score >= NUMERIC_HIGH:
|
|
61
|
+
return cls.HIGH
|
|
62
|
+
if score >= NUMERIC_MEDIUM:
|
|
63
|
+
return cls.MEDIUM
|
|
64
|
+
return cls.LOW
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
LEVEL_TERMS: dict[ConfidenceLevel, Term] = {
|
|
68
|
+
ConfidenceLevel.HIGH: Term(
|
|
69
|
+
label="High confidence",
|
|
70
|
+
description="Well-documented information that is firmly established.",
|
|
71
|
+
guidance="well-documented claims you could verify directly",
|
|
72
|
+
strength="strong",
|
|
73
|
+
),
|
|
74
|
+
ConfidenceLevel.MEDIUM: Term(
|
|
75
|
+
label="Medium confidence",
|
|
76
|
+
description=(
|
|
77
|
+
"Generally accepted information that involves some interpretation."
|
|
78
|
+
),
|
|
79
|
+
guidance="generally accepted claims that involve some interpretation",
|
|
80
|
+
strength="moderate",
|
|
81
|
+
),
|
|
82
|
+
ConfidenceLevel.LOW: Term(
|
|
83
|
+
label="Low confidence",
|
|
84
|
+
description=(
|
|
85
|
+
"Uncertain or disputed content with limited supporting evidence."
|
|
86
|
+
),
|
|
87
|
+
guidance="uncertain, disputed, or speculative claims",
|
|
88
|
+
strength="weak",
|
|
89
|
+
),
|
|
90
|
+
ConfidenceLevel.SPECULATIVE: Term(
|
|
91
|
+
label="Speculative",
|
|
92
|
+
description=(
|
|
93
|
+
"Offered as a possibility rather than asserted: hypothetical, "
|
|
94
|
+
"fictional, or an idea that goes beyond the sources."
|
|
95
|
+
),
|
|
96
|
+
guidance=(
|
|
97
|
+
"ideas you are proposing rather than asserting: extensions, "
|
|
98
|
+
"hypotheses, what-ifs that go beyond the evidence"
|
|
99
|
+
),
|
|
100
|
+
strength="weak",
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ConfidenceType(StrEnum):
|
|
106
|
+
"""What kind of information the claim is."""
|
|
107
|
+
|
|
108
|
+
FACTUAL = "FACTUAL"
|
|
109
|
+
CONSENSUS = "CONSENSUS"
|
|
110
|
+
THEORETICAL = "THEORETICAL"
|
|
111
|
+
SPECULATIVE = "SPECULATIVE"
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def _missing_(cls, value: object) -> "ConfidenceType | None":
|
|
115
|
+
if not isinstance(value, str):
|
|
116
|
+
return None
|
|
117
|
+
token = normalize_token(value)
|
|
118
|
+
for member in cls:
|
|
119
|
+
if member.value == token:
|
|
120
|
+
return member
|
|
121
|
+
logger.warning(
|
|
122
|
+
"Unrecognized confidence type %r; using CONSENSUS", value
|
|
123
|
+
)
|
|
124
|
+
return cls.CONSENSUS
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
TYPE_TERMS: dict[ConfidenceType, Term] = {
|
|
128
|
+
ConfidenceType.FACTUAL: Term(
|
|
129
|
+
label="Factual",
|
|
130
|
+
description="Based on verifiable facts.",
|
|
131
|
+
guidance="real-world verifiable facts such as recorded events",
|
|
132
|
+
strength="strong",
|
|
133
|
+
),
|
|
134
|
+
ConfidenceType.CONSENSUS: Term(
|
|
135
|
+
label="Consensus",
|
|
136
|
+
description="Reflects broad agreement among sources.",
|
|
137
|
+
guidance="generally accepted information",
|
|
138
|
+
strength="moderate",
|
|
139
|
+
),
|
|
140
|
+
ConfidenceType.THEORETICAL: Term(
|
|
141
|
+
label="Theoretical",
|
|
142
|
+
description="Follows from a theory or framework.",
|
|
143
|
+
guidance="scientific theories and scholarly frameworks",
|
|
144
|
+
strength="moderate",
|
|
145
|
+
),
|
|
146
|
+
ConfidenceType.SPECULATIVE: Term(
|
|
147
|
+
label="Speculative",
|
|
148
|
+
description="A hypothesis or conjecture.",
|
|
149
|
+
guidance="fiction, hypothetical scenarios, or unverified claims",
|
|
150
|
+
strength="weak",
|
|
151
|
+
),
|
|
152
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""The ``Confidence`` model attached to each claim an agent makes."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, get_args
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator
|
|
6
|
+
|
|
7
|
+
from .basis import BasisEnum
|
|
8
|
+
from .levels import (
|
|
9
|
+
LEVEL_TERMS,
|
|
10
|
+
TYPE_TERMS,
|
|
11
|
+
ConfidenceLevel,
|
|
12
|
+
ConfidenceType,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def level_guidance() -> str:
|
|
17
|
+
options = ", ".join(
|
|
18
|
+
f"{level.value} for {LEVEL_TERMS[level].guidance}"
|
|
19
|
+
for level in ConfidenceLevel
|
|
20
|
+
)
|
|
21
|
+
return (
|
|
22
|
+
f"Confidence level, exactly one of {options}. "
|
|
23
|
+
"Never use numbers or percentages."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def type_guidance() -> str:
|
|
28
|
+
options = ", ".join(
|
|
29
|
+
f"{kind.value} for {term.guidance}"
|
|
30
|
+
for kind, term in TYPE_TERMS.items()
|
|
31
|
+
)
|
|
32
|
+
return f"Kind of information being assessed: {options}."
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Confidence[B: BasisEnum](BaseModel):
|
|
36
|
+
"""A stated confidence level and the evidential basis behind it.
|
|
37
|
+
|
|
38
|
+
Parametrize with a basis set: ``Confidence[DataAnalysisBasis]``. The
|
|
39
|
+
generic form is what an agent's output model embeds; presentation and
|
|
40
|
+
consistency checks work on any parametrization.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
level: ConfidenceLevel = Field(
|
|
44
|
+
default=ConfidenceLevel.MEDIUM, description=level_guidance()
|
|
45
|
+
)
|
|
46
|
+
basis: B = Field(
|
|
47
|
+
description=(
|
|
48
|
+
"The kind of evidence behind this claim. Choose the value that "
|
|
49
|
+
"most honestly describes what the claim rests on."
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
type: ConfidenceType | None = Field(
|
|
53
|
+
default=None, description=type_guidance()
|
|
54
|
+
)
|
|
55
|
+
label: str = Field(
|
|
56
|
+
default="",
|
|
57
|
+
description=(
|
|
58
|
+
"Human-readable confidence label in the language of the user's "
|
|
59
|
+
"input, e.g. 'Haute confiance'. Leave empty for English."
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
reasoning: str = Field(
|
|
63
|
+
default="",
|
|
64
|
+
description=(
|
|
65
|
+
"One or two sentences on why this level and basis apply, in the "
|
|
66
|
+
"language of the user's input."
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
evidence: list[str] = Field(
|
|
70
|
+
default_factory=list,
|
|
71
|
+
description=(
|
|
72
|
+
"What concretely backs the claim: source titles or URLs, "
|
|
73
|
+
"document ids, column names, tool results. Empty when nothing "
|
|
74
|
+
"concrete does."
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def basis_set(cls) -> "type[BasisEnum] | None":
|
|
80
|
+
"""The basis enum this class is parametrized with, if any."""
|
|
81
|
+
annotation = cls.model_fields["basis"].annotation
|
|
82
|
+
if isinstance(annotation, type) and issubclass(annotation, BasisEnum):
|
|
83
|
+
return annotation
|
|
84
|
+
for arg in get_args(annotation):
|
|
85
|
+
if isinstance(arg, type) and issubclass(arg, BasisEnum):
|
|
86
|
+
return arg
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
@model_validator(mode="before")
|
|
90
|
+
@classmethod
|
|
91
|
+
def _default_missing_basis(cls, data: Any) -> Any:
|
|
92
|
+
# A model that omits the basis entirely gets the set's fallback,
|
|
93
|
+
# for the same reason an invented value does: one thin claim must
|
|
94
|
+
# not fail the whole response.
|
|
95
|
+
if isinstance(data, dict) and data.get("basis") in (None, ""):
|
|
96
|
+
basis_set = cls.basis_set()
|
|
97
|
+
if basis_set is not None and basis_set.__fallback__:
|
|
98
|
+
data = {**data, "basis": basis_set.__fallback__}
|
|
99
|
+
return data
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def is_weak(self) -> bool:
|
|
103
|
+
"""True when the level or the basis says this is not to be relied on."""
|
|
104
|
+
return (
|
|
105
|
+
LEVEL_TERMS[self.level].strength == "weak"
|
|
106
|
+
or self.basis.strength == "weak"
|
|
107
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Display vocabulary: labels, descriptions, and a framework-neutral tone.
|
|
2
|
+
|
|
3
|
+
Kept free of any CSS or component library so a web UI, a terminal, and a
|
|
4
|
+
report can all read the same words.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from .basis import BasisEnum
|
|
10
|
+
from .levels import LEVEL_TERMS, TYPE_TERMS, ConfidenceLevel, ConfidenceType
|
|
11
|
+
from .terms import Term
|
|
12
|
+
|
|
13
|
+
Tone = Literal["positive", "neutral", "caution", "warning"]
|
|
14
|
+
|
|
15
|
+
_TONES: dict[ConfidenceLevel, Tone] = {
|
|
16
|
+
ConfidenceLevel.HIGH: "positive",
|
|
17
|
+
ConfidenceLevel.MEDIUM: "neutral",
|
|
18
|
+
ConfidenceLevel.LOW: "caution",
|
|
19
|
+
ConfidenceLevel.SPECULATIVE: "warning",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
SCALE_EXPLANATION = (
|
|
23
|
+
"The model's own assessment of how well-supported this content is. "
|
|
24
|
+
+ " · ".join(
|
|
25
|
+
f"{level.value.title()}: {LEVEL_TERMS[level].description.rstrip('.').lower()}"
|
|
26
|
+
for level in ConfidenceLevel
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _prettify(value: str) -> str:
|
|
32
|
+
words = value.replace("_", " ").lower().strip()
|
|
33
|
+
return words[:1].upper() + words[1:]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def describe(
|
|
37
|
+
value: ConfidenceLevel | ConfidenceType | BasisEnum | str,
|
|
38
|
+
) -> Term:
|
|
39
|
+
"""The ``Term`` for an enum member, or a prettified stand-in for a string.
|
|
40
|
+
|
|
41
|
+
Strings are matched against the levels and every registered basis
|
|
42
|
+
set; anything else (a value from a set this process has not defined,
|
|
43
|
+
an old spelling) gets a readable label and no description, never an
|
|
44
|
+
error.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(value, ConfidenceLevel):
|
|
47
|
+
return LEVEL_TERMS[value]
|
|
48
|
+
if isinstance(value, ConfidenceType):
|
|
49
|
+
return TYPE_TERMS[value]
|
|
50
|
+
if isinstance(value, BasisEnum):
|
|
51
|
+
return value.term
|
|
52
|
+
token = value.strip().upper()
|
|
53
|
+
if token in ConfidenceLevel.__members__:
|
|
54
|
+
return LEVEL_TERMS[ConfidenceLevel[token]]
|
|
55
|
+
for basis_set in BasisEnum.registered():
|
|
56
|
+
if token in basis_set.__terms__:
|
|
57
|
+
return basis_set.__terms__[token]
|
|
58
|
+
return Term(label=_prettify(value), description="")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def tone(level: ConfidenceLevel | str | None) -> Tone:
|
|
62
|
+
"""A four-way tone for badges and highlights; unknown values are neutral."""
|
|
63
|
+
if level is None:
|
|
64
|
+
return "neutral"
|
|
65
|
+
if not isinstance(level, ConfidenceLevel):
|
|
66
|
+
try:
|
|
67
|
+
level = ConfidenceLevel(level)
|
|
68
|
+
except ValueError:
|
|
69
|
+
return "neutral"
|
|
70
|
+
return _TONES[level]
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Pydantic AI integration: an output validator that sends inconsistent
|
|
2
|
+
confidence assessments back to the model as a retry.
|
|
3
|
+
|
|
4
|
+
Usage::
|
|
5
|
+
|
|
6
|
+
from stated_confidence.pydantic_ai import confidence_output_validator
|
|
7
|
+
|
|
8
|
+
agent = Agent(..., output_type=Report)
|
|
9
|
+
agent.output_validator(confidence_output_validator())
|
|
10
|
+
|
|
11
|
+
Requires the ``pydantic-ai`` extra.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from typing import Any, TypeVar
|
|
16
|
+
|
|
17
|
+
from pydantic_ai import ModelRetry, RunContext
|
|
18
|
+
|
|
19
|
+
from .checks import check_all
|
|
20
|
+
|
|
21
|
+
OutputT = TypeVar("OutputT")
|
|
22
|
+
ExtraCheck = Callable[[RunContext[Any], Any], list[str]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def confidence_output_validator(
|
|
26
|
+
*extra_checks: ExtraCheck,
|
|
27
|
+
) -> Callable[[RunContext[Any], OutputT], OutputT]:
|
|
28
|
+
"""Build a validator raising ``ModelRetry`` for every violation found.
|
|
29
|
+
|
|
30
|
+
``extra_checks`` are ``(ctx, output) -> list[str]`` callables for rules
|
|
31
|
+
that need the run's context, such as checking that evidence names a
|
|
32
|
+
column the agent's query actually returned. Their messages are joined
|
|
33
|
+
with the built-in consistency rules into one retry prompt.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def validate(ctx: RunContext[Any], output: OutputT) -> OutputT:
|
|
37
|
+
violations = check_all(output)
|
|
38
|
+
for check in extra_checks:
|
|
39
|
+
violations.extend(check(ctx, output))
|
|
40
|
+
if violations:
|
|
41
|
+
raise ModelRetry(
|
|
42
|
+
"Fix these confidence assessments and answer again:\n"
|
|
43
|
+
+ "\n".join(f"- {v}" for v in violations)
|
|
44
|
+
)
|
|
45
|
+
return output
|
|
46
|
+
|
|
47
|
+
return validate
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Export the vocabulary as JSON, the interchange format other language
|
|
2
|
+
packages (and the checked-in ``taxonomy.json``) are built from.
|
|
3
|
+
|
|
4
|
+
Run ``python -m stated_confidence.taxonomy`` to print it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import asdict
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .basis import (
|
|
13
|
+
BasisEnum,
|
|
14
|
+
DataAnalysisBasis,
|
|
15
|
+
DecisionBasis,
|
|
16
|
+
GeneralKnowledgeBasis,
|
|
17
|
+
)
|
|
18
|
+
from .levels import LEVEL_TERMS, TYPE_TERMS
|
|
19
|
+
from .presentation import _TONES
|
|
20
|
+
|
|
21
|
+
BASIS_SETS: tuple[type[BasisEnum], ...] = (
|
|
22
|
+
GeneralKnowledgeBasis,
|
|
23
|
+
DataAnalysisBasis,
|
|
24
|
+
DecisionBasis,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _terms(table: dict[Any, Any]) -> dict[str, dict[str, Any]]:
|
|
29
|
+
return {str(key.value): asdict(term) for key, term in table.items()}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def taxonomy() -> dict[str, Any]:
|
|
33
|
+
return {
|
|
34
|
+
"version": 1,
|
|
35
|
+
"levels": {
|
|
36
|
+
level.value: {**asdict(term), "tone": _TONES[level]}
|
|
37
|
+
for level, term in LEVEL_TERMS.items()
|
|
38
|
+
},
|
|
39
|
+
"types": _terms(TYPE_TERMS),
|
|
40
|
+
"basis_sets": {
|
|
41
|
+
basis_set.__name__: {
|
|
42
|
+
"fallback": basis_set.__fallback__,
|
|
43
|
+
"terms": {
|
|
44
|
+
member.value: asdict(member.term) for member in basis_set
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
for basis_set in BASIS_SETS
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def dumps() -> str:
|
|
53
|
+
return json.dumps(taxonomy(), indent=2, ensure_ascii=False) + "\n"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
sys.stdout.write(dumps())
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Vocabulary entries shared by every enum in the taxonomy.
|
|
2
|
+
|
|
3
|
+
A ``Term`` carries three texts for one enum value: what a UI shows
|
|
4
|
+
(``label``, ``description``) and what a model is told when asked to pick
|
|
5
|
+
the value (``guidance``). ``strength`` is what the consistency checks use.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
Strength = Literal["strong", "moderate", "weak"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class Term:
|
|
16
|
+
label: str
|
|
17
|
+
description: str
|
|
18
|
+
guidance: str = ""
|
|
19
|
+
strength: Strength | None = None
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: stated-confidence
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Make LLM agents state the confidence level and the evidential basis behind each claim.
|
|
5
|
+
Project-URL: Homepage, https://github.com/johnwlockwood/stated-confidence
|
|
6
|
+
Author-email: John Lockwood <johnwlockwood@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agents,confidence,grounding,hallucination,llm,pydantic,pydantic-ai
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: pydantic>=2.5
|
|
20
|
+
Provides-Extra: pydantic-ai
|
|
21
|
+
Requires-Dist: pydantic-ai-slim>=1.0; extra == 'pydantic-ai'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# stated-confidence
|
|
25
|
+
|
|
26
|
+
Make LLM agents say how sure they are, and what that certainty rests on.
|
|
27
|
+
|
|
28
|
+
Ask a model to attach a confidence **level** and an evidential **basis** to
|
|
29
|
+
each claim it produces and it fabricates less. The level alone is common
|
|
30
|
+
practice; the basis is what does the work. Choosing between "documented
|
|
31
|
+
fact", "expert opinion", and "educated guess" forces a provenance check the
|
|
32
|
+
model would otherwise skip, and a claim it cannot back gets labelled as a
|
|
33
|
+
guess or quietly dropped instead of dressed up as a figure.
|
|
34
|
+
|
|
35
|
+
This package is that schema, the vocabulary behind it, prompt guidance for
|
|
36
|
+
filling it, and the consistency checks that send hedging-without-meaning-it
|
|
37
|
+
back to the model. It was extracted from a production knowledge-graph
|
|
38
|
+
generator where it was observed to sharply reduce invented facts and
|
|
39
|
+
figures. The effect is observed, not yet measured; an evaluation harness is
|
|
40
|
+
planned.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install stated-confidence # pydantic only
|
|
46
|
+
pip install "stated-confidence[pydantic-ai]"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## The shape
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from stated_confidence import Confidence, DataAnalysisBasis
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Finding(BaseModel):
|
|
56
|
+
text: str
|
|
57
|
+
confidence: Confidence[DataAnalysisBasis]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A `Confidence` has:
|
|
61
|
+
|
|
62
|
+
| field | meaning |
|
|
63
|
+
| ----------- | ------------------------------------------------------------------------- |
|
|
64
|
+
| `level` | `HIGH`, `MEDIUM`, `LOW`, or `SPECULATIVE`. Never a number. |
|
|
65
|
+
| `basis` | The kind of evidence, from a basis set chosen for the domain. |
|
|
66
|
+
| `reasoning` | One or two sentences on why that level and basis apply. |
|
|
67
|
+
| `evidence` | What concretely backs the claim: sources, ids, column names, tool results. |
|
|
68
|
+
| `type` | Optional: `FACTUAL`, `CONSENSUS`, `THEORETICAL`, `SPECULATIVE`. |
|
|
69
|
+
| `label` | Optional localized label for display. |
|
|
70
|
+
|
|
71
|
+
`SPECULATIVE` is not the bottom of the scale. It is the level for an idea
|
|
72
|
+
the model is offering rather than a claim it is making: an extension a
|
|
73
|
+
source suggests, a hypothesis, a what-if. A generator that has somewhere
|
|
74
|
+
honest to put an idea does not dress it up as a fact; take that room away
|
|
75
|
+
and the ideas come back as fabricated "facts" or disappear altogether.
|
|
76
|
+
|
|
77
|
+
The level scale is fixed. The **basis set** is the extension point. Three
|
|
78
|
+
ship:
|
|
79
|
+
|
|
80
|
+
- `GeneralKnowledgeBasis` for claims about the world: documented fact,
|
|
81
|
+
scientific consensus, historical record, industry standard, expert
|
|
82
|
+
opinion, theoretical framework, fictional universe, speculation, educated
|
|
83
|
+
guess.
|
|
84
|
+
- `DataAnalysisBasis` for agents reasoning over data they retrieved: query
|
|
85
|
+
result, computed, schema inference, general knowledge, assumption.
|
|
86
|
+
- `DecisionBasis` for recommendations: policy, measured outcome, precedent,
|
|
87
|
+
trade-off analysis, expert judgment, assumption.
|
|
88
|
+
|
|
89
|
+
Define your own by subclassing `BasisEnum` with a `__terms__` table and a
|
|
90
|
+
`__fallback__` value.
|
|
91
|
+
|
|
92
|
+
## Prompting
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from stated_confidence import confidence_instructions
|
|
96
|
+
|
|
97
|
+
system_prompt += confidence_instructions(DataAnalysisBasis, localized=True)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Pydantic AI
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from pydantic_ai import Agent
|
|
104
|
+
from stated_confidence.pydantic_ai import confidence_output_validator
|
|
105
|
+
|
|
106
|
+
agent = Agent("openai:gpt-5", output_type=Report)
|
|
107
|
+
agent.output_validator(confidence_output_validator())
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Every `Confidence` anywhere in the output is checked. A `HIGH` level on a
|
|
111
|
+
basis of `ASSUMPTION`, a `SPECULATIVE` level on a basis of `DOCUMENTED_FACT`,
|
|
112
|
+
or a non-`HIGH` level with empty reasoning comes back to the model as a
|
|
113
|
+
retry listing what to fix. Pass extra `(ctx, output) -> list[str]` callables for rules that
|
|
114
|
+
need run context, such as requiring that evidence names a column the
|
|
115
|
+
agent's query actually returned.
|
|
116
|
+
|
|
117
|
+
## Lenient parsing, strict prompting
|
|
118
|
+
|
|
119
|
+
Models drift. `"High"` parses. `0.9` parses (as `HIGH`). An invented basis
|
|
120
|
+
value parses as the set's fallback and is logged at `WARNING` on the
|
|
121
|
+
`stated_confidence` logger so the set can grow from what models actually
|
|
122
|
+
say.
|
|
123
|
+
|
|
124
|
+
## Display
|
|
125
|
+
|
|
126
|
+
`describe(value)` returns a label and description for any level, type, or
|
|
127
|
+
basis value; `tone(level)` maps to `positive`, `neutral`, `caution`, or
|
|
128
|
+
`warning` with no CSS framework attached. `taxonomy.json` at the package
|
|
129
|
+
root is the same vocabulary as data, for other languages to consume.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
stated_confidence/__init__.py,sha256=dPe7yjNwuK_546rgGYnM7SKQ9FJBxqv3ZIk8UxoXHsY,1192
|
|
2
|
+
stated_confidence/basis.py,sha256=goO44qynjvztxzWY_c3wP0YbbYj_uWPaZAz_rsrZHTs,9033
|
|
3
|
+
stated_confidence/checks.py,sha256=nQSej_cU-u2ChjYwpeMSD--fdABeY1uGmwhA7L-8ByE,3205
|
|
4
|
+
stated_confidence/guidance.py,sha256=knNSHHhqrHdUjxz7faiY2I9a5wu2dx45jzIRaNha5Jw,2190
|
|
5
|
+
stated_confidence/levels.py,sha256=aJ4pNg_LpMnK21G0EXj9dLYEsfILuTKsT6MESslc0nA,4836
|
|
6
|
+
stated_confidence/model.py,sha256=3qakjnU-QME8jQbIpgtMzahNGBlfAmqV4FuiGqvWPEQ,3498
|
|
7
|
+
stated_confidence/presentation.py,sha256=71sYU6IN26bshWxsH6a6CAwozLLuZzPqsrI6sVlRenA,2291
|
|
8
|
+
stated_confidence/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
stated_confidence/pydantic_ai.py,sha256=qRPUCPjZ88i6jFSkJBPRv1KDb1h9AwF9IbqdpOB6Pks,1507
|
|
10
|
+
stated_confidence/taxonomy.py,sha256=ZjeI2lVq8VJo4g4ojc_9XmPq2Q6yjFh6fH5-4KrqFVA,1429
|
|
11
|
+
stated_confidence/terms.py,sha256=t66DaqsSVJbZjwacQMGyDSR2aWaxNfLa7Jkd0vxvv-I,538
|
|
12
|
+
stated_confidence-0.1.0.dist-info/METADATA,sha256=Kv-X6k1tOxtmYW3P5y6JA11Qs3ts_eO0WuPqmqkwdWQ,5360
|
|
13
|
+
stated_confidence-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
14
|
+
stated_confidence-0.1.0.dist-info/licenses/LICENSE,sha256=MxI5z_i6TBS_xUk8SVZxCPi069K8Da586gjyrCWpNus,1070
|
|
15
|
+
stated_confidence-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Lockwood
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|