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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.