dev-double 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.
Files changed (84) hide show
  1. dev_double/__init__.py +3 -0
  2. dev_double/apps/__init__.py +1 -0
  3. dev_double/apps/cli/__init__.py +5 -0
  4. dev_double/apps/cli/main.py +122 -0
  5. dev_double/apps/client/__init__.py +5 -0
  6. dev_double/apps/client/http_client.py +102 -0
  7. dev_double/apps/composition.py +71 -0
  8. dev_double/apps/config.py +36 -0
  9. dev_double/apps/server/__init__.py +5 -0
  10. dev_double/apps/server/app.py +122 -0
  11. dev_double/apps/server/recorder.py +62 -0
  12. dev_double/apps/server/transport/__init__.py +39 -0
  13. dev_double/apps/server/transport/choice_use_cases.py +121 -0
  14. dev_double/apps/server/transport/common.py +37 -0
  15. dev_double/apps/server/transport/decide.py +131 -0
  16. dev_double/apps/server/transport/extract.py +93 -0
  17. dev_double/apps/server/transport/guard_judge.py +97 -0
  18. dev_double/apps/server/transport/rerank.py +55 -0
  19. dev_double/client.py +5 -0
  20. dev_double/core/__init__.py +1 -0
  21. dev_double/core/decision/__init__.py +62 -0
  22. dev_double/core/decision/answer_shaping.py +48 -0
  23. dev_double/core/decision/confidence.py +19 -0
  24. dev_double/core/decision/decider_basic_impl.py +65 -0
  25. dev_double/core/decision/decision_service_basic_impl.py +141 -0
  26. dev_double/core/decision/defaults.py +40 -0
  27. dev_double/core/decision/errors.py +7 -0
  28. dev_double/core/decision/extract_questions.py +40 -0
  29. dev_double/core/decision/field_extraction.py +53 -0
  30. dev_double/core/decision/i_clock.py +11 -0
  31. dev_double/core/decision/i_decider.py +36 -0
  32. dev_double/core/decision/i_decision_service.py +40 -0
  33. dev_double/core/decision/i_engine.py +27 -0
  34. dev_double/core/decision/i_extractor.py +19 -0
  35. dev_double/core/decision/i_generator.py +19 -0
  36. dev_double/core/decision/i_id_provider.py +9 -0
  37. dev_double/core/decision/i_record_reader.py +25 -0
  38. dev_double/core/decision/i_reranker.py +18 -0
  39. dev_double/core/decision/label_scoring.py +89 -0
  40. dev_double/core/decision/prompts.py +130 -0
  41. dev_double/core/decision/record_parsing.py +153 -0
  42. dev_double/core/decision/t_answer.py +32 -0
  43. dev_double/core/decision/t_classify.py +42 -0
  44. dev_double/core/decision/t_decide.py +26 -0
  45. dev_double/core/decision/t_extract.py +89 -0
  46. dev_double/core/decision/t_gate.py +36 -0
  47. dev_double/core/decision/t_generate.py +37 -0
  48. dev_double/core/decision/t_guard.py +38 -0
  49. dev_double/core/decision/t_input.py +8 -0
  50. dev_double/core/decision/t_judge.py +35 -0
  51. dev_double/core/decision/t_label_query.py +36 -0
  52. dev_double/core/decision/t_meta.py +17 -0
  53. dev_double/core/decision/t_question.py +70 -0
  54. dev_double/core/decision/t_rerank.py +36 -0
  55. dev_double/core/decision/t_route.py +28 -0
  56. dev_double/core/decision/t_usage.py +15 -0
  57. dev_double/core/decision/tracker.py +27 -0
  58. dev_double/core/decision/use_case_questions.py +80 -0
  59. dev_double/core/decision/value_parsing.py +109 -0
  60. dev_double/providers/__init__.py +1 -0
  61. dev_double/providers/mock/__init__.py +1 -0
  62. dev_double/providers/mock/decision/__init__.py +5 -0
  63. dev_double/providers/mock/decision/clock_mock_impl.py +19 -0
  64. dev_double/providers/mock/decision/engine_mock_impl.py +78 -0
  65. dev_double/providers/mock/decision/id_provider_mock_impl.py +15 -0
  66. dev_double/providers/needle/__init__.py +1 -0
  67. dev_double/providers/needle/decision/__init__.py +4 -0
  68. dev_double/providers/needle/decision/decider_needle_impl.py +117 -0
  69. dev_double/providers/needle/decision/record_tool.py +75 -0
  70. dev_double/providers/openai/__init__.py +1 -0
  71. dev_double/providers/openai/decision/__init__.py +3 -0
  72. dev_double/providers/openai/decision/engine_openai_impl.py +154 -0
  73. dev_double/providers/std/__init__.py +1 -0
  74. dev_double/providers/std/decision/__init__.py +4 -0
  75. dev_double/providers/std/decision/clock_std_impl.py +12 -0
  76. dev_double/providers/std/decision/id_provider_std_impl.py +12 -0
  77. dev_double/providers/systemone/__init__.py +1 -0
  78. dev_double/providers/systemone/decision/__init__.py +3 -0
  79. dev_double/providers/systemone/decision/decider_system_one_impl.py +100 -0
  80. dev_double-0.1.0.dist-info/METADATA +354 -0
  81. dev_double-0.1.0.dist-info/RECORD +84 -0
  82. dev_double-0.1.0.dist-info/WHEEL +4 -0
  83. dev_double-0.1.0.dist-info/entry_points.txt +2 -0
  84. dev_double-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,130 @@
1
+ """Turns a question into a single-label prompt.
2
+
3
+ Every prompt ends by asking for exactly one label (an option name, a digit, or
4
+ yes/no), so the answer sits in the first few tokens, whose probabilities we can read.
5
+
6
+ Choices are answered by option name, not by letter: small models over-pick
7
+ "B" when options are lettered, which skews the probabilities.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+
14
+ from .t_extract import TExtractField
15
+ from .t_generate import TGenerateQuery
16
+ from .t_input import TInputValue
17
+ from .t_label_query import TLabelQuery
18
+ from .t_question import TBinaryQuestion, TChoiceQuestion, TQuestion, TScaleQuestion
19
+
20
+ SYSTEM = (
21
+ "You are a precise decision function. Read the input, then answer the question "
22
+ "with exactly one label from the allowed labels. Output only the label: no "
23
+ "explanation, no punctuation, no other text."
24
+ )
25
+
26
+ # Extraction: all free-text fields of a request in one JSON object. Wording
27
+ # chosen by A/B test against a per-field prompt (which small models answered
28
+ # with NONE for values plainly present, or echoed the field description).
29
+ RECORD_SYSTEM = (
30
+ "You extract data from text into JSON. Output one JSON object with exactly the requested "
31
+ "keys. Copy values from the text, converting them to the requested format. Use null only "
32
+ "when the text gives no information for a key. Output only the JSON."
33
+ )
34
+ RECORD_MAX_TOKENS = 512
35
+
36
+ def render_input(value: TInputValue) -> str:
37
+ if isinstance(value, str):
38
+ return value
39
+ return json.dumps(value, ensure_ascii=False, indent=2)
40
+
41
+
42
+ def _frame(question: str, input_text: str, body: str) -> str:
43
+ # Asking the question before and after the input measurably helps small
44
+ # models: they read the input knowing what to look for.
45
+ return f"Question: {question}\n\n<input>\n{input_text}\n</input>\n\n{body}"
46
+
47
+
48
+ def binary(input_text: str, q: TBinaryQuestion) -> TLabelQuery:
49
+ lines = [f"Question: {q.question}"]
50
+ if q.yes:
51
+ lines.append(f'Answer "yes" if: {q.yes}')
52
+ if q.no:
53
+ lines.append(f'Answer "no" if: {q.no}')
54
+ lines.append("Reply with exactly one word: yes or no.")
55
+ return TLabelQuery(
56
+ system=SYSTEM,
57
+ user=_frame(q.question, input_text, "\n".join(lines)),
58
+ labels=["yes", "no"],
59
+ input_text=input_text,
60
+ question=q.question,
61
+ descriptions=[f"{q.question} {q.yes or ''}", q.no or ""],
62
+ )
63
+
64
+
65
+ def choice(input_text: str, q: TChoiceQuestion) -> TLabelQuery:
66
+ keys = list(q.options)
67
+ option_lines = [f"- {key}: {q.options[key]}" for key in keys]
68
+ body = (
69
+ f"Question: {q.question}\n\nOptions:\n"
70
+ + "\n".join(option_lines)
71
+ + f"\n\nReply with exactly one option name: {', '.join(keys)}."
72
+ )
73
+ return TLabelQuery(
74
+ system=SYSTEM,
75
+ user=_frame(q.question, input_text, body),
76
+ labels=keys,
77
+ input_text=input_text,
78
+ question=q.question,
79
+ descriptions=[f"{key} {q.options[key]}" for key in keys],
80
+ )
81
+
82
+
83
+ def scale(input_text: str, q: TScaleQuestion) -> TLabelQuery:
84
+ digits = [str(i) for i in range(len(q.levels))]
85
+ level_lines = [f"{d}: {text}" for d, text in zip(digits, q.levels)]
86
+ body = (
87
+ f"Question: {q.question}\n\nLevels (lowest first):\n"
88
+ + "\n".join(level_lines)
89
+ + f"\n\nReply with exactly one digit: {', '.join(digits)}."
90
+ )
91
+ return TLabelQuery(
92
+ system=SYSTEM,
93
+ user=_frame(q.question, input_text, body),
94
+ labels=digits,
95
+ input_text=input_text,
96
+ question=q.question,
97
+ descriptions=list(q.levels),
98
+ )
99
+
100
+
101
+ def query_for(input_text: str, q: TQuestion) -> TLabelQuery:
102
+ """Dispatch on the question type."""
103
+ if isinstance(q, TBinaryQuestion):
104
+ return binary(input_text, q)
105
+ if isinstance(q, TChoiceQuestion):
106
+ return choice(input_text, q)
107
+ return scale(input_text, q)
108
+
109
+
110
+ def record_max_tokens(n_fields: int) -> int:
111
+ return min(RECORD_MAX_TOKENS, 48 + 40 * n_fields)
112
+
113
+
114
+ def record_key_line(name: str, field: TExtractField) -> str:
115
+ return f'- "{name}" ({field.type}): {field.description or name.replace("_", " ")}'
116
+
117
+
118
+ def extract_record(input_text: str, fields: dict[str, TExtractField]) -> TGenerateQuery:
119
+ """Free-text fields (string, number, integer) as one JSON object, keys listed
120
+ before and after the text."""
121
+ keys = "\n".join(record_key_line(name, f) for name, f in fields.items())
122
+ user = f"Keys:\n{keys}\n\n<text>\n{input_text}\n</text>\n\nFill in the keys from the text above:\n{keys}\n\nJSON:"
123
+ return TGenerateQuery(
124
+ system=RECORD_SYSTEM,
125
+ user=user,
126
+ max_tokens=record_max_tokens(len(fields)),
127
+ json=True,
128
+ input_text=input_text,
129
+ fields=tuple(fields),
130
+ )
@@ -0,0 +1,153 @@
1
+ """Pure helpers that turn a model's JSON record into typed field values, with a
2
+ per-field confidence read from the tokens that spell each value.
3
+
4
+ The model may wrap the object in ``` fences or prose; we take the outermost
5
+ {...}. Each value's span in the output is located by a small depth-aware
6
+ scan, and its confidence is exp(mean logprob) of the tokens overlapping it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import math
13
+ from typing import Any, Optional, Sequence
14
+
15
+ from .confidence import DIGITS
16
+ from .t_extract import EMPTY, TExtractField, TFieldValue
17
+ from .tracker import Tracker
18
+ from .value_parsing import coerce_json_value
19
+
20
+
21
+ def parse_object(text: str) -> Optional[dict[str, Any]]:
22
+ """The outermost {...} in the text as a dict, or None if it isn't a JSON object."""
23
+ start, end = text.find("{"), text.rfind("}")
24
+ if start < 0 or end < start:
25
+ return None
26
+ try:
27
+ obj = json.loads(text[start : end + 1])
28
+ except ValueError:
29
+ return None
30
+ return obj if isinstance(obj, dict) else None
31
+
32
+
33
+ def _string_end(text: str, i: int) -> int:
34
+ """Index just past the JSON string starting at text[i] == '"', or -1."""
35
+ j = i + 1
36
+ while j < len(text):
37
+ if text[j] == "\\":
38
+ j += 2
39
+ continue
40
+ if text[j] == '"':
41
+ return j + 1
42
+ j += 1
43
+ return -1
44
+
45
+
46
+ def _value_end(text: str, j: int) -> int:
47
+ """Index of the ',' or '}' that ends the value starting at j (depth 0, outside strings)."""
48
+ depth = 0
49
+ while j < len(text):
50
+ ch = text[j]
51
+ if ch == '"':
52
+ end = _string_end(text, j)
53
+ if end < 0:
54
+ return len(text)
55
+ j = end
56
+ continue
57
+ if ch in "[{":
58
+ depth += 1
59
+ elif ch in "]}":
60
+ if depth == 0:
61
+ return j
62
+ depth -= 1
63
+ elif ch == "," and depth == 0:
64
+ return j
65
+ j += 1
66
+ return j
67
+
68
+
69
+ def value_spans(text: str) -> dict[str, tuple[int, int]]:
70
+ """Key -> (start, end) of its raw value (e.g. '"Acme"', '12.5', 'null') in the
71
+ first top-level object of the text. Stops quietly at malformed JSON."""
72
+ i = text.find("{")
73
+ if i < 0:
74
+ return {}
75
+ spans: dict[str, tuple[int, int]] = {}
76
+ i += 1
77
+ while i < len(text):
78
+ if text[i].isspace() or text[i] == ",":
79
+ i += 1
80
+ continue
81
+ if text[i] != '"':
82
+ break # '}' or malformed
83
+ key_end = _string_end(text, i)
84
+ if key_end < 0:
85
+ break
86
+ try:
87
+ key = json.loads(text[i:key_end])
88
+ except ValueError:
89
+ break
90
+ j = key_end
91
+ while j < len(text) and text[j].isspace():
92
+ j += 1
93
+ if j >= len(text) or text[j] != ":":
94
+ break
95
+ j += 1
96
+ while j < len(text) and text[j].isspace():
97
+ j += 1
98
+ end = _value_end(text, j)
99
+ stop = end
100
+ while stop > j and text[stop - 1].isspace():
101
+ stop -= 1
102
+ spans[key] = (j, stop)
103
+ i = end
104
+ return spans
105
+
106
+
107
+ def span_confidence(tokens: Sequence[tuple[str, float]], start: int, end: int) -> Optional[float]:
108
+ """exp(mean logprob) of the tokens overlapping text[start:end], or None if none do."""
109
+ logprobs, offset = [], 0
110
+ for token, logprob in tokens:
111
+ if offset < end and offset + len(token) > start:
112
+ logprobs.append(logprob)
113
+ offset += len(token)
114
+ if not logprobs:
115
+ return None
116
+ return min(1.0, math.exp(sum(logprobs) / len(logprobs)))
117
+
118
+
119
+ def parse_record(
120
+ text: str,
121
+ tokens: Sequence[tuple[str, float]],
122
+ fields: dict[str, TExtractField],
123
+ fallback_confidence: float,
124
+ tracker: Tracker,
125
+ ) -> dict[str, TFieldValue]:
126
+ """Field name -> value for the text fields in a generated JSON record.
127
+
128
+ Missing keys and nulls give None. A value's confidence comes from its own
129
+ tokens; ``fallback_confidence`` (the whole generation's) is used when there
130
+ are no tokens or the value's span can't be found in them.
131
+ """
132
+ obj = parse_object(text)
133
+ if obj is None:
134
+ tracker.warn(f"the model did not return a JSON object (got {text[:40]!r}); text fields left empty")
135
+ return {name: EMPTY for name in fields}
136
+ spans = value_spans("".join(token for token, _ in tokens)) if tokens else {}
137
+ out: dict[str, TFieldValue] = {}
138
+ for name, field in fields.items():
139
+ conf = None
140
+ if name in spans:
141
+ conf = span_confidence(tokens, *spans[name])
142
+ conf = round(fallback_confidence if conf is None else conf, DIGITS)
143
+ raw = obj.get(name)
144
+ if raw is None:
145
+ out[name] = TFieldValue(None, conf)
146
+ continue
147
+ value, problem = coerce_json_value(raw, field.type)
148
+ if problem is not None:
149
+ tracker.warn(problem, name)
150
+ out[name] = EMPTY
151
+ continue
152
+ out[name] = TFieldValue(value, conf)
153
+ return out
@@ -0,0 +1,32 @@
1
+ """Answers to the three question types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Union
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class TBinaryAnswer:
11
+ value: bool
12
+ probability: float # probability that the answer is yes
13
+ confidence: float
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class TChoiceAnswer:
18
+ value: str
19
+ probabilities: dict[str, float]
20
+ confidence: float
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class TScaleAnswer:
25
+ value: float # probability-weighted level, from 0 to len(levels) - 1
26
+ level: int # the single most likely level
27
+ probabilities: dict[str, float]
28
+ legend: dict[str, str]
29
+ confidence: float
30
+
31
+
32
+ TAnswer = Union[TBinaryAnswer, TChoiceAnswer, TScaleAnswer]
@@ -0,0 +1,42 @@
1
+ """Labeling one item or a batch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ from .defaults import DEFAULT_CLASSIFY_QUESTION
9
+ from .t_input import TInputValue
10
+ from .t_meta import TMeta
11
+ from .t_question import check_options
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class TClassifyRequest:
16
+ labels: dict[str, str]
17
+ input: Optional[TInputValue] = None # one item...
18
+ inputs: Optional[list[TInputValue]] = None # ...or many, labeled in parallel
19
+ question: str = DEFAULT_CLASSIFY_QUESTION
20
+
21
+ def __post_init__(self) -> None:
22
+ check_options(self.labels, "labels")
23
+ if (self.input is None) == (self.inputs is None):
24
+ raise ValueError("Provide exactly one of `input` or `inputs`.")
25
+ if self.inputs is not None and not self.inputs:
26
+ raise ValueError("`inputs` must not be empty.")
27
+
28
+ def items(self) -> list[TInputValue]:
29
+ return self.inputs if self.inputs is not None else [self.input] # type: ignore[list-item]
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class TClassification:
34
+ label: str
35
+ probabilities: dict[str, float]
36
+ confidence: float
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class TClassifyResponse:
41
+ results: list[TClassification]
42
+ meta: TMeta
@@ -0,0 +1,26 @@
1
+ """Generic decision: several typed questions about one input."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from .t_answer import TAnswer
8
+ from .t_input import TInputValue
9
+ from .t_meta import TMeta
10
+ from .t_question import TQuestion
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class TDecideRequest:
15
+ input: TInputValue
16
+ questions: dict[str, TQuestion]
17
+
18
+ def __post_init__(self) -> None:
19
+ if not self.questions:
20
+ raise ValueError("`questions` must not be empty.")
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class TDecideResponse:
25
+ answers: dict[str, TAnswer]
26
+ meta: TMeta
@@ -0,0 +1,89 @@
1
+ """Structured extraction: typed fields pulled out of one input.
2
+
3
+ Enum and boolean fields are answered as choice and binary questions (real
4
+ probabilities); string, number, and integer fields are generated as text.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Optional, Union
11
+
12
+ from .t_input import TInputValue
13
+ from .t_meta import TMeta
14
+ from .t_question import check_options
15
+
16
+ FIELD_TYPES = ("string", "number", "integer", "boolean", "enum")
17
+ TEXT_TYPES = ("string", "number", "integer") # generated as text, then parsed
18
+ MAX_FIELDS = 30
19
+
20
+ # An extracted value; None when the input doesn't contain the field.
21
+ TValue = Optional[Union[str, float, int, bool]]
22
+
23
+
24
+ def _check_probability(p: Optional[float], what: str) -> None:
25
+ if p is not None and not 0 <= p <= 1:
26
+ raise ValueError(f"`{what}` must be between 0 and 1, got {p}.")
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class TExtractField:
31
+ type: str # one of FIELD_TYPES
32
+ description: Optional[str] = None
33
+ # enum only: option -> description, or a list of option names (each its own description).
34
+ options: Optional[Union[dict[str, str], list[str]]] = None
35
+
36
+ def __post_init__(self) -> None:
37
+ if self.type not in FIELD_TYPES:
38
+ raise ValueError(f"Field `type` must be one of {', '.join(FIELD_TYPES)}; got {self.type!r}.")
39
+ if self.type != "enum":
40
+ if self.options is not None:
41
+ raise ValueError(f"Only enum fields take `options`, not {self.type} fields.")
42
+ return
43
+ if self.options is None:
44
+ raise ValueError("Enum fields need `options`.")
45
+ options = self.options
46
+ if isinstance(options, list):
47
+ if len(set(options)) != len(options):
48
+ raise ValueError("Duplicate enum options.")
49
+ options = {name: name for name in options}
50
+ check_options(options)
51
+ object.__setattr__(self, "options", dict(options))
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class TExtractRequest:
56
+ input: TInputValue
57
+ fields: dict[str, TExtractField] # field name -> spec; answers keep this order
58
+
59
+ def __post_init__(self) -> None:
60
+ if not 1 <= len(self.fields) <= MAX_FIELDS:
61
+ raise ValueError(f"`fields` must have 1 to {MAX_FIELDS} entries, got {len(self.fields)}.")
62
+ for name in self.fields:
63
+ if not name.strip():
64
+ raise ValueError("Field names must not be empty.")
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class TFieldValue:
69
+ value: TValue
70
+ confidence: float
71
+ probabilities: Optional[dict[str, float]] = None # enum fields only
72
+ probability: Optional[float] = None # boolean fields only: probability of true
73
+
74
+ def __post_init__(self) -> None:
75
+ _check_probability(self.confidence, "confidence")
76
+ _check_probability(self.probability, "probability")
77
+
78
+
79
+ EMPTY = TFieldValue(None, 0.0)
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class TExtractResponse:
84
+ fields: dict[str, TFieldValue]
85
+ meta: TMeta
86
+
87
+ @property
88
+ def values(self) -> dict[str, TValue]:
89
+ return {name: f.value for name, f in self.fields.items()}
@@ -0,0 +1,36 @@
1
+ """Tool-call gating: allow, ask a human, or deny."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Optional, Union
7
+
8
+ from .t_input import TInputValue
9
+ from .t_meta import TMeta
10
+ from .t_question import check_options
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class TToolCall:
15
+ name: str
16
+ arguments: Union[dict[str, Any], str] = field(default_factory=dict)
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class TGateRequest:
21
+ tool_call: TToolCall
22
+ context: Optional[TInputValue] = None # what the user asked for, or the recent conversation
23
+ policy: Optional[str] = None # rules for tool use, in plain language
24
+ outcomes: Optional[dict[str, str]] = None # outcome -> when; None = DEFAULT_OUTCOMES
25
+
26
+ def __post_init__(self) -> None:
27
+ if self.outcomes is not None:
28
+ check_options(self.outcomes, "outcomes")
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class TGateResponse:
33
+ decision: str
34
+ probabilities: dict[str, float]
35
+ confidence: float
36
+ meta: TMeta
@@ -0,0 +1,37 @@
1
+ """The generator contract's data: a rendered prompt in, a short text and how
2
+ sure the model was of it out."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+
8
+ from .t_usage import TUsage
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class TGenerateQuery:
13
+ system: str
14
+ user: str
15
+ max_tokens: int
16
+ json: bool = False # ask for one JSON object (e.g. response_format json_object)
17
+ # Semantic parts, for engines that don't use the rendered prompt (e.g. mock).
18
+ input_text: str = ""
19
+ fields: tuple[str, ...] = () # the field names being extracted
20
+
21
+ def __post_init__(self) -> None:
22
+ if self.max_tokens < 1:
23
+ raise ValueError("`max_tokens` must be at least 1.")
24
+
25
+
26
+ @dataclass
27
+ class TGenerateResult:
28
+ text: str
29
+ confidence: float # 0..1 for the whole text, e.g. exp(mean token logprob)
30
+ usage: TUsage = field(default_factory=TUsage)
31
+ warnings: list[str] = field(default_factory=list)
32
+ # The generated tokens as (text, logprob), in order; empty if the server returns none.
33
+ tokens: list[tuple[str, float]] = field(default_factory=list)
34
+
35
+ def __post_init__(self) -> None:
36
+ if not 0 <= self.confidence <= 1:
37
+ raise ValueError(f"`confidence` must be between 0 and 1, got {self.confidence}.")
@@ -0,0 +1,38 @@
1
+ """Input screening: injection, abuse, off-topic use."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ from .defaults import DEFAULT_GUARD_THRESHOLD
9
+ from .t_input import TInputValue
10
+ from .t_meta import TMeta
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class TGuardRequest:
15
+ input: TInputValue
16
+ policies: Optional[dict[str, str]] = None # policy -> violation; None = DEFAULT_POLICIES
17
+ scope: Optional[str] = None # what the assistant is for; adds an off_topic check
18
+ threshold: float = DEFAULT_GUARD_THRESHOLD # block when a violation probability reaches this
19
+
20
+ def __post_init__(self) -> None:
21
+ if self.policies is not None and not self.policies:
22
+ raise ValueError("`policies` must not be empty.")
23
+ if not 0 <= self.threshold <= 1:
24
+ raise ValueError("`threshold` must be between 0 and 1.")
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class TGuardCheck:
29
+ probability: float
30
+ flagged: bool
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class TGuardResponse:
35
+ allowed: bool
36
+ flagged: list[str]
37
+ checks: dict[str, TGuardCheck]
38
+ meta: TMeta
@@ -0,0 +1,8 @@
1
+ """The content a question is asked about."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Union
6
+
7
+ # Text, an object, or a list. Objects and lists are shown to the model as JSON.
8
+ TInputValue = Union[str, dict[str, Any], list[Any]]
@@ -0,0 +1,35 @@
1
+ """LLM evals: score an output against a rubric."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ from .defaults import DEFAULT_JUDGE_CRITERIA
9
+ from .t_input import TInputValue
10
+ from .t_meta import TMeta
11
+ from .t_question import check_levels
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class TJudgeRequest:
16
+ output: TInputValue # the answer being judged
17
+ input: Optional[TInputValue] = None # the prompt or task that produced it
18
+ reference: Optional[TInputValue] = None # optional reference answer
19
+ criteria: str = DEFAULT_JUDGE_CRITERIA
20
+ levels: Optional[list[str]] = None # ordered rubric, worst first; None = DEFAULT_RUBRIC
21
+
22
+ def __post_init__(self) -> None:
23
+ if self.levels is not None:
24
+ check_levels(self.levels)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class TJudgeResponse:
29
+ score: float # probability-weighted level, from 0 to len(levels) - 1
30
+ normalized: float # score / (len(levels) - 1), from 0 to 1
31
+ level: int
32
+ probabilities: dict[str, float]
33
+ legend: dict[str, str]
34
+ confidence: float
35
+ meta: TMeta
@@ -0,0 +1,36 @@
1
+ """The engine contract's data: a rendered single-label prompt in, a
2
+ distribution over those labels out."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+
8
+ from .t_usage import TUsage
9
+
10
+
11
+ @dataclass
12
+ class TLabelQuery:
13
+ """One question, rendered and ready for an engine."""
14
+
15
+ system: str
16
+ user: str
17
+ labels: list[str]
18
+ # Semantic parts, for engines that don't use the rendered prompt (e.g. mock).
19
+ input_text: str
20
+ question: str
21
+ descriptions: list[str]
22
+
23
+
24
+ @dataclass
25
+ class TLabelResult:
26
+ probabilities: dict[str, float]
27
+ usage: TUsage = field(default_factory=TUsage)
28
+ warnings: list[str] = field(default_factory=list)
29
+
30
+
31
+ @dataclass
32
+ class TPosition:
33
+ """One generated token and the top alternatives the model considered there."""
34
+
35
+ token: str
36
+ candidates: list[tuple[str, float]] # (token, logprob)
@@ -0,0 +1,17 @@
1
+ """Response metadata shared by every use case."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from .t_usage import TUsage
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class TMeta:
12
+ engine: str
13
+ model: str
14
+ latency_ms: int
15
+ usage: TUsage
16
+ # Where this answer may be less faithful than a real decision model.
17
+ warnings: list[str] = field(default_factory=list)