agent-learning 0.4.1__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 (71) hide show
  1. agent_learning/__init__.py +160 -0
  2. agent_learning/_version.py +3 -0
  3. agent_learning/capture.py +271 -0
  4. agent_learning/classifiers/__init__.py +37 -0
  5. agent_learning/classifiers/base.py +185 -0
  6. agent_learning/classifiers/router.py +236 -0
  7. agent_learning/classifiers/scorers/__init__.py +34 -0
  8. agent_learning/classifiers/scorers/_base.py +162 -0
  9. agent_learning/classifiers/scorers/adherence.py +26 -0
  10. agent_learning/classifiers/scorers/completion.py +26 -0
  11. agent_learning/classifiers/scorers/intent.py +25 -0
  12. agent_learning/cli.py +386 -0
  13. agent_learning/config.py +524 -0
  14. agent_learning/learners/__init__.py +6 -0
  15. agent_learning/learners/base.py +38 -0
  16. agent_learning/learners/reinforce.py +153 -0
  17. agent_learning/metrics/__init__.py +22 -0
  18. agent_learning/metrics/base.py +233 -0
  19. agent_learning/metrics/intent_resolution.py +50 -0
  20. agent_learning/metrics/registry.py +42 -0
  21. agent_learning/metrics/task_adherence.py +42 -0
  22. agent_learning/metrics/task_completion.py +54 -0
  23. agent_learning/policy/__init__.py +7 -0
  24. agent_learning/policy/base.py +59 -0
  25. agent_learning/policy/contextual_softmax.py +243 -0
  26. agent_learning/policy/softmax_bandit.py +157 -0
  27. agent_learning/py.typed +1 -0
  28. agent_learning/rewards/__init__.py +6 -0
  29. agent_learning/rewards/shaping.py +121 -0
  30. agent_learning/rewards/writer.py +130 -0
  31. agent_learning/scorers/__init__.py +187 -0
  32. agent_learning/scorers/base.py +49 -0
  33. agent_learning/scorers/llm/__init__.py +19 -0
  34. agent_learning/scorers/llm/_base.py +126 -0
  35. agent_learning/scorers/llm/adherence.py +21 -0
  36. agent_learning/scorers/llm/completion.py +21 -0
  37. agent_learning/scorers/llm/intent.py +21 -0
  38. agent_learning/scorers/nlp/__init__.py +16 -0
  39. agent_learning/scorers/nlp/_base.py +99 -0
  40. agent_learning/scorers/nlp/adherence.py +17 -0
  41. agent_learning/scorers/nlp/completion.py +17 -0
  42. agent_learning/scorers/nlp/intent.py +17 -0
  43. agent_learning/scorers/nlp_text/__init__.py +26 -0
  44. agent_learning/scorers/nlp_text/_base.py +234 -0
  45. agent_learning/scorers/nlp_text/adherence.py +94 -0
  46. agent_learning/scorers/nlp_text/completion.py +91 -0
  47. agent_learning/scorers/nlp_text/intent.py +59 -0
  48. agent_learning/scorers/slm/__init__.py +25 -0
  49. agent_learning/scorers/slm/_base.py +292 -0
  50. agent_learning/scorers/slm/adherence.py +98 -0
  51. agent_learning/scorers/slm/completion.py +111 -0
  52. agent_learning/scorers/slm/intent.py +80 -0
  53. agent_learning/scorers/stdlib/__init__.py +38 -0
  54. agent_learning/scorers/stdlib/_text.py +87 -0
  55. agent_learning/scorers/stdlib/adherence.py +157 -0
  56. agent_learning/scorers/stdlib/completion.py +117 -0
  57. agent_learning/scorers/stdlib/intent.py +182 -0
  58. agent_learning/storage/__init__.py +14 -0
  59. agent_learning/storage/base.py +156 -0
  60. agent_learning/storage/cosmos.py +506 -0
  61. agent_learning/storage/local.py +353 -0
  62. agent_learning/storage/memory.py +209 -0
  63. agent_learning/training/__init__.py +5 -0
  64. agent_learning/training/runner.py +172 -0
  65. agent_learning/types.py +507 -0
  66. agent_learning-0.4.1.dist-info/METADATA +82 -0
  67. agent_learning-0.4.1.dist-info/RECORD +71 -0
  68. agent_learning-0.4.1.dist-info/WHEEL +5 -0
  69. agent_learning-0.4.1.dist-info/entry_points.txt +2 -0
  70. agent_learning-0.4.1.dist-info/licenses/LICENSE +21 -0
  71. agent_learning-0.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,292 @@
1
+ """Phi-4-mini-instruct ONNX runner + shared scorer base for Tier 3 SLM.
2
+
3
+ The runner lazily loads ``onnxruntime-genai`` and the Phi-4-mini-instruct
4
+ INT4 model from a local directory the first time :meth:`SlmRunner.generate`
5
+ is called. Subsequent calls reuse the cached model and tokenizer instances.
6
+ The runner abstraction also makes the per-scorer prompt building and JSON
7
+ parsing trivially mockable in unit tests via :meth:`set_runner`.
8
+
9
+ Prompt format
10
+ ~~~~~~~~~~~~~
11
+
12
+ The runner applies the Phi-3.5 / Phi-4 chat template:
13
+
14
+ .. code-block:: text
15
+
16
+ <|system|>
17
+ {system}<|end|>
18
+ <|user|>
19
+ {user}<|end|>
20
+ <|assistant|>
21
+
22
+ Each subclass of :class:`SlmScorerBase` supplies its own ``system`` and
23
+ ``user`` strings; the assistant reply is parsed by
24
+ :func:`parse_verdict` which expects a single JSON object of the form
25
+ ``{"verdict": "pass"|"fail", "confidence": <number in [0,1]>}``. The
26
+ parser is permissive about leading or trailing prose because instruction-
27
+ tuned small models occasionally emit a sentence in front of the JSON.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import os
34
+ import re
35
+ import threading
36
+ from dataclasses import dataclass, field
37
+ from typing import Any, Dict, Optional, Tuple
38
+
39
+ from ..base import ScoreResult
40
+
41
+
42
+ _GENAI_IMPORT_ERROR = (
43
+ "Tier 3 SLM scorers require the [slm] extra. Install with: "
44
+ "pip install agent-learning[slm]"
45
+ )
46
+
47
+
48
+ def _require_genai() -> Any:
49
+ """Lazy-import ``onnxruntime_genai``. Raises a friendly error if missing."""
50
+ try:
51
+ import onnxruntime_genai as og # noqa: F401
52
+ except ImportError as exc: # pragma: no cover - exercised via tests/mocks
53
+ raise ImportError(_GENAI_IMPORT_ERROR) from exc
54
+ return og
55
+
56
+
57
+ # ----- model runner --------------------------------------------------------
58
+
59
+
60
+ class SlmRunner:
61
+ """Thin wrapper around onnxruntime-genai's ``Model``+``Tokenizer``+``Generator``.
62
+
63
+ The wrapper hides the version-dependent generator loop behind a single
64
+ :meth:`generate` method. A test harness can replace the runner with any
65
+ object that implements ``generate(prompt: str, *, max_new_tokens: int,
66
+ temperature: float) -> str``.
67
+ """
68
+
69
+ _shared_lock = threading.Lock()
70
+ _shared_runners: Dict[str, "SlmRunner"] = {}
71
+
72
+ def __init__(self, model_dir: str) -> None:
73
+ if not model_dir:
74
+ raise ValueError("SlmRunner requires a non-empty model_dir.")
75
+ if not os.path.isdir(model_dir):
76
+ raise FileNotFoundError(
77
+ f"SLM model directory not found: {model_dir!r}. "
78
+ "Download Phi-4-mini-instruct ONNX (INT4) to this path or "
79
+ "set AGENT_LEARNING_SLM_MODEL_DIR to its location."
80
+ )
81
+ og = _require_genai()
82
+ self._og = og
83
+ self._model = og.Model(model_dir)
84
+ self._tokenizer = og.Tokenizer(self._model)
85
+ self.model_dir = model_dir
86
+
87
+ @classmethod
88
+ def get_shared(cls, model_dir: str) -> "SlmRunner":
89
+ """Return a process-wide cached runner for ``model_dir``.
90
+
91
+ Loading the 3.8 B Phi-4-mini-instruct ONNX model from cold can take
92
+ several seconds and consume ~2.5 GB of RAM. Sharing one runner across
93
+ the three Tier 3 scorers keeps inference hot and avoids the
94
+ re-loading cost.
95
+ """
96
+ with cls._shared_lock:
97
+ runner = cls._shared_runners.get(model_dir)
98
+ if runner is None:
99
+ runner = cls(model_dir)
100
+ cls._shared_runners[model_dir] = runner
101
+ return runner
102
+
103
+ def generate(
104
+ self,
105
+ prompt: str,
106
+ *,
107
+ max_new_tokens: int = 64,
108
+ temperature: float = 0.0,
109
+ ) -> str:
110
+ """Run greedy decoding on ``prompt`` and return the assistant text."""
111
+ og = self._og
112
+ tokenizer = self._tokenizer
113
+ input_tokens = tokenizer.encode(prompt)
114
+ input_length = len(input_tokens)
115
+ params = og.GeneratorParams(self._model)
116
+ params.set_search_options(
117
+ max_length=input_length + int(max_new_tokens),
118
+ do_sample=False,
119
+ temperature=float(max(temperature, 0.0)),
120
+ )
121
+ # API surface drift across onnxruntime-genai versions: 0.4 wants
122
+ # params.input_ids, 0.5+ uses generator.append_tokens. Try both.
123
+ try:
124
+ params.input_ids = input_tokens
125
+ except (AttributeError, TypeError): # pragma: no cover - version drift
126
+ pass
127
+ generator = og.Generator(self._model, params)
128
+ append = getattr(generator, "append_tokens", None)
129
+ if callable(append): # pragma: no cover - version drift
130
+ try:
131
+ append(input_tokens)
132
+ except Exception:
133
+ pass
134
+ # Greedy decoding loop.
135
+ while not generator.is_done():
136
+ compute_logits = getattr(generator, "compute_logits", None)
137
+ if callable(compute_logits): # pragma: no cover - version drift
138
+ compute_logits()
139
+ generator.generate_next_token()
140
+ get_sequence = getattr(generator, "get_sequence", None)
141
+ if callable(get_sequence):
142
+ full = get_sequence(0)
143
+ else: # pragma: no cover - very old version
144
+ full = generator.get_next_tokens()
145
+ # Strip the prompt prefix.
146
+ output_tokens = list(full)[input_length:]
147
+ text = tokenizer.decode(output_tokens)
148
+ return text
149
+
150
+
151
+ # ----- prompt rendering ----------------------------------------------------
152
+
153
+
154
+ _CHAT_TEMPLATE = (
155
+ "<|system|>\n{system}<|end|>\n"
156
+ "<|user|>\n{user}<|end|>\n"
157
+ "<|assistant|>\n"
158
+ )
159
+
160
+
161
+ def render_chat_prompt(*, system: str, user: str) -> str:
162
+ """Render a Phi-3.5 / Phi-4 chat-style prompt."""
163
+ return _CHAT_TEMPLATE.format(system=system.strip(), user=user.strip())
164
+
165
+
166
+ # ----- verdict parsing -----------------------------------------------------
167
+
168
+
169
+ _JSON_RE = re.compile(r"\{[^{}]*\}", re.DOTALL)
170
+
171
+
172
+ def parse_verdict(text: str) -> Tuple[float, Dict[str, Any]]:
173
+ """Extract ``(probability_pass, raw)`` from the model's reply.
174
+
175
+ Looks for the first JSON object in ``text``. Falls back to a token
176
+ heuristic if no JSON is recoverable. The function never raises on a
177
+ malformed reply; an unrecoverable parse degrades to a neutral 0.5
178
+ probability so the caller can still emit a score.
179
+ """
180
+ raw: Dict[str, Any] = {"text": text}
181
+ if not text:
182
+ return 0.5, raw
183
+ # First, try to parse as raw JSON.
184
+ candidate: Optional[Dict[str, Any]] = None
185
+ try:
186
+ loaded = json.loads(text)
187
+ if isinstance(loaded, dict):
188
+ candidate = loaded
189
+ except (ValueError, TypeError):
190
+ match = _JSON_RE.search(text)
191
+ if match is not None:
192
+ try:
193
+ loaded = json.loads(match.group(0))
194
+ if isinstance(loaded, dict):
195
+ candidate = loaded
196
+ except (ValueError, TypeError):
197
+ candidate = None
198
+ if candidate is not None:
199
+ verdict = str(candidate.get("verdict", "")).strip().lower()
200
+ try:
201
+ confidence = float(candidate.get("confidence", 0.5))
202
+ except (TypeError, ValueError):
203
+ confidence = 0.5
204
+ confidence = max(0.0, min(1.0, confidence))
205
+ raw["verdict"] = verdict
206
+ raw["confidence"] = confidence
207
+ if verdict.startswith("pass"):
208
+ return confidence, raw
209
+ if verdict.startswith("fail"):
210
+ return 1.0 - confidence, raw
211
+ # Token-level fallback. Look for an isolated "pass" or "fail" word.
212
+ lowered = text.lower()
213
+ pass_hit = re.search(r"\bpass\b", lowered)
214
+ fail_hit = re.search(r"\bfail\b", lowered)
215
+ if pass_hit and (not fail_hit or pass_hit.start() < fail_hit.start()):
216
+ raw["heuristic"] = "pass"
217
+ return 0.75, raw
218
+ if fail_hit:
219
+ raw["heuristic"] = "fail"
220
+ return 0.25, raw
221
+ raw["heuristic"] = "neutral"
222
+ return 0.5, raw
223
+
224
+
225
+ # ----- shared base ---------------------------------------------------------
226
+
227
+
228
+ @dataclass
229
+ class SlmScorerBase:
230
+ """Shared scoring scaffolding for Tier 3 small-language-model scorers."""
231
+
232
+ name: str = "slm"
233
+ pass_threshold: float = 0.5
234
+ max_new_tokens: int = 64
235
+ temperature: float = 0.0
236
+ model_dir: str = ""
237
+ runner: Optional[SlmRunner] = None
238
+ system_prompt: str = field(default="", repr=False)
239
+
240
+ def _get_runner(self) -> SlmRunner:
241
+ if self.runner is not None:
242
+ return self.runner
243
+ if not self.model_dir:
244
+ raise ValueError(
245
+ f"{self.name}: model_dir must be set to load the SLM"
246
+ )
247
+ self.runner = SlmRunner.get_shared(self.model_dir)
248
+ return self.runner
249
+
250
+ def _build_user_prompt(self, **inputs: Any) -> str: # pragma: no cover - override
251
+ raise NotImplementedError
252
+
253
+ def _validate(self, **inputs: Any) -> None: # pragma: no cover - override
254
+ return
255
+
256
+ def _score_from_inputs(self, **inputs: Any) -> ScoreResult:
257
+ self._validate(**inputs)
258
+ prompt = render_chat_prompt(
259
+ system=self.system_prompt,
260
+ user=self._build_user_prompt(**inputs),
261
+ )
262
+ runner = self._get_runner()
263
+ text = runner.generate(
264
+ prompt,
265
+ max_new_tokens=self.max_new_tokens,
266
+ temperature=self.temperature,
267
+ )
268
+ probability, raw = parse_verdict(text)
269
+ if probability >= self.pass_threshold:
270
+ label = "pass"
271
+ confidence = probability
272
+ else:
273
+ label = "fail"
274
+ confidence = 1.0 - probability
275
+ return ScoreResult(
276
+ label=label,
277
+ confidence=confidence,
278
+ normalized=probability,
279
+ features={
280
+ "probability": probability,
281
+ "model_dir": self.model_dir,
282
+ "raw": raw,
283
+ },
284
+ )
285
+
286
+
287
+ __all__ = [
288
+ "SlmScorerBase",
289
+ "SlmRunner",
290
+ "parse_verdict",
291
+ "render_chat_prompt",
292
+ ]
@@ -0,0 +1,98 @@
1
+ """Tier 3 SLM adherence scorer.
2
+
3
+ Asks Phi-4-mini-instruct whether the response adheres to the supplied
4
+ contract (required substrings, forbidden substrings, length bounds, JSON
5
+ shape). The scorer is read-only: it never modifies the contract or the
6
+ response, only scores them against each other.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Dict, Optional
14
+
15
+ from ..base import ScoreResult
16
+ from ._base import SlmRunner, SlmScorerBase
17
+
18
+
19
+ _SYSTEM_PROMPT = (
20
+ "You are a strict evaluator that checks whether an agent's response "
21
+ "complies with a structural contract. Reply only with a single-line "
22
+ "JSON object: {\"verdict\": \"pass\" | \"fail\", \"confidence\": "
23
+ "<number in [0, 1]>}. Output no other text."
24
+ )
25
+
26
+
27
+ def _format_contract(contract: Optional[Dict[str, Any]]) -> str:
28
+ if not contract:
29
+ return (
30
+ "(no explicit contract; the response is acceptable as long "
31
+ "as it is on-topic and free of obvious violations)"
32
+ )
33
+ try:
34
+ return json.dumps(contract, ensure_ascii=False, sort_keys=True)
35
+ except (TypeError, ValueError):
36
+ return str(contract)
37
+
38
+
39
+ _USER_PROMPT_TEMPLATE = (
40
+ "CONTRACT:\n{contract}\n\nRESPONSE:\n{response}\n\n"
41
+ "Does the response satisfy the contract? Reply with the JSON verdict "
42
+ "only."
43
+ )
44
+
45
+
46
+ @dataclass
47
+ class SlmAdherenceScorer(SlmScorerBase):
48
+ """Phi-4-mini-instruct adherence scorer."""
49
+
50
+ name: str = "adherence"
51
+ system_prompt: str = field(default=_SYSTEM_PROMPT, repr=False)
52
+
53
+ @classmethod
54
+ def load_or_default(
55
+ cls,
56
+ model_dir: Optional[str] = None,
57
+ *,
58
+ pass_threshold: float = 0.5,
59
+ max_new_tokens: int = 64,
60
+ temperature: float = 0.0,
61
+ runner: Optional[SlmRunner] = None,
62
+ ) -> "SlmAdherenceScorer":
63
+ return cls(
64
+ model_dir=model_dir or "",
65
+ pass_threshold=pass_threshold,
66
+ max_new_tokens=max_new_tokens,
67
+ temperature=temperature,
68
+ runner=runner,
69
+ )
70
+
71
+ def _validate(self, **inputs: object) -> None:
72
+ if inputs.get("response") is None:
73
+ raise ValueError(
74
+ "adherence SLM scorer requires a response"
75
+ )
76
+
77
+ def _build_user_prompt(self, **inputs: object) -> str:
78
+ return _USER_PROMPT_TEMPLATE.format(
79
+ contract=_format_contract(inputs.get("contract")),
80
+ response=str(inputs["response"]).strip(),
81
+ )
82
+
83
+ def score(
84
+ self,
85
+ *,
86
+ response: Optional[str] = None,
87
+ contract: Optional[Dict[str, Any]] = None,
88
+ query: Optional[str] = None,
89
+ **_: object,
90
+ ) -> ScoreResult:
91
+ return self._score_from_inputs(
92
+ response=response,
93
+ contract=contract,
94
+ query=query,
95
+ )
96
+
97
+
98
+ __all__ = ["SlmAdherenceScorer"]
@@ -0,0 +1,111 @@
1
+ """Tier 3 SLM completion scorer.
2
+
3
+ Asks Phi-4-mini-instruct whether every required token / sub-task is
4
+ present in the response. The expected tokens can be supplied either
5
+ through ``expected_tokens`` directly or through ``contract.completion_tokens``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, List, Optional, Sequence
12
+
13
+ from ..base import ScoreResult
14
+ from ._base import SlmRunner, SlmScorerBase
15
+
16
+
17
+ _SYSTEM_PROMPT = (
18
+ "You are a strict evaluator that checks whether an agent's response "
19
+ "covers every required item. Reply only with a single-line JSON "
20
+ "object: {\"verdict\": \"pass\" | \"fail\", \"confidence\": <number "
21
+ "in [0, 1]>}. Output no other text."
22
+ )
23
+
24
+
25
+ _USER_PROMPT_TEMPLATE = (
26
+ "REQUIRED ITEMS (every one must be covered):\n{items}\n\n"
27
+ "RESPONSE:\n{response}\n\n"
28
+ "Does the response cover every required item? Reply with the JSON "
29
+ "verdict only."
30
+ )
31
+
32
+
33
+ def _resolve_targets(
34
+ expected_tokens: Optional[Sequence[str]],
35
+ contract: Optional[Dict[str, Any]],
36
+ ) -> List[str]:
37
+ if expected_tokens:
38
+ return [str(t).strip() for t in expected_tokens if str(t).strip()]
39
+ if contract:
40
+ tokens = contract.get("completion_tokens")
41
+ if isinstance(tokens, Sequence) and not isinstance(tokens, (str, bytes)):
42
+ return [str(t).strip() for t in tokens if str(t).strip()]
43
+ return []
44
+
45
+
46
+ @dataclass
47
+ class SlmCompletionScorer(SlmScorerBase):
48
+ """Phi-4-mini-instruct completion scorer."""
49
+
50
+ name: str = "completion"
51
+ system_prompt: str = field(default=_SYSTEM_PROMPT, repr=False)
52
+
53
+ @classmethod
54
+ def load_or_default(
55
+ cls,
56
+ model_dir: Optional[str] = None,
57
+ *,
58
+ pass_threshold: float = 0.5,
59
+ max_new_tokens: int = 64,
60
+ temperature: float = 0.0,
61
+ runner: Optional[SlmRunner] = None,
62
+ ) -> "SlmCompletionScorer":
63
+ return cls(
64
+ model_dir=model_dir or "",
65
+ pass_threshold=pass_threshold,
66
+ max_new_tokens=max_new_tokens,
67
+ temperature=temperature,
68
+ runner=runner,
69
+ )
70
+
71
+ def _validate(self, **inputs: object) -> None:
72
+ if inputs.get("response") is None:
73
+ raise ValueError(
74
+ "completion SLM scorer requires a response"
75
+ )
76
+
77
+ def _build_user_prompt(self, **inputs: object) -> str:
78
+ targets = _resolve_targets(
79
+ inputs.get("expected_tokens"), # type: ignore[arg-type]
80
+ inputs.get("contract"), # type: ignore[arg-type]
81
+ )
82
+ if targets:
83
+ items_str = "\n".join(f"- {t}" for t in targets)
84
+ else:
85
+ items_str = (
86
+ "(no explicit checklist; treat the response as complete "
87
+ "if it answers the apparent task)"
88
+ )
89
+ return _USER_PROMPT_TEMPLATE.format(
90
+ items=items_str,
91
+ response=str(inputs["response"]).strip(),
92
+ )
93
+
94
+ def score(
95
+ self,
96
+ *,
97
+ response: Optional[str] = None,
98
+ expected_tokens: Optional[Sequence[str]] = None,
99
+ contract: Optional[Dict[str, Any]] = None,
100
+ query: Optional[str] = None,
101
+ **_: object,
102
+ ) -> ScoreResult:
103
+ return self._score_from_inputs(
104
+ response=response,
105
+ expected_tokens=expected_tokens,
106
+ contract=contract,
107
+ query=query,
108
+ )
109
+
110
+
111
+ __all__ = ["SlmCompletionScorer"]
@@ -0,0 +1,80 @@
1
+ """Tier 3 SLM intent scorer.
2
+
3
+ Asks Phi-4-mini-instruct whether the model's response actually addresses
4
+ the user's question. The scorer requires both ``query`` and ``response``;
5
+ this matches the intent-evaluator surface area used by Tiers 1, 2, and 4.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Optional
12
+
13
+ from ..base import ScoreResult
14
+ from ._base import SlmRunner, SlmScorerBase
15
+
16
+
17
+ _SYSTEM_PROMPT = (
18
+ "You are a strict evaluator that checks whether an agent's response "
19
+ "addresses the user's question. Reply only with a single-line JSON "
20
+ "object: {\"verdict\": \"pass\" | \"fail\", \"confidence\": <number "
21
+ "in [0, 1]>}. Output no other text."
22
+ )
23
+
24
+ _USER_PROMPT_TEMPLATE = (
25
+ "QUERY:\n{query}\n\nRESPONSE:\n{response}\n\n"
26
+ "Does the response actually address the query? Reply with the JSON "
27
+ "verdict only."
28
+ )
29
+
30
+
31
+ @dataclass
32
+ class SlmIntentScorer(SlmScorerBase):
33
+ """Phi-4-mini-instruct intent scorer."""
34
+
35
+ name: str = "intent"
36
+ system_prompt: str = field(default=_SYSTEM_PROMPT, repr=False)
37
+
38
+ @classmethod
39
+ def load_or_default(
40
+ cls,
41
+ model_dir: Optional[str] = None,
42
+ *,
43
+ pass_threshold: float = 0.5,
44
+ max_new_tokens: int = 64,
45
+ temperature: float = 0.0,
46
+ runner: Optional[SlmRunner] = None,
47
+ ) -> "SlmIntentScorer":
48
+ return cls(
49
+ model_dir=model_dir or "",
50
+ pass_threshold=pass_threshold,
51
+ max_new_tokens=max_new_tokens,
52
+ temperature=temperature,
53
+ runner=runner,
54
+ )
55
+
56
+ def _validate(self, **inputs: object) -> None:
57
+ query = inputs.get("query")
58
+ response = inputs.get("response")
59
+ if not query or not response:
60
+ raise ValueError(
61
+ "intent SLM scorer requires both query and response"
62
+ )
63
+
64
+ def _build_user_prompt(self, **inputs: object) -> str:
65
+ return _USER_PROMPT_TEMPLATE.format(
66
+ query=str(inputs["query"]).strip(),
67
+ response=str(inputs["response"]).strip(),
68
+ )
69
+
70
+ def score(
71
+ self,
72
+ *,
73
+ query: Optional[str] = None,
74
+ response: Optional[str] = None,
75
+ **_: object,
76
+ ) -> ScoreResult:
77
+ return self._score_from_inputs(query=query, response=response)
78
+
79
+
80
+ __all__ = ["SlmIntentScorer"]
@@ -0,0 +1,38 @@
1
+ """Tier 1 stdlib scorers (zero external dependencies).
2
+
3
+ These scorers score the rendered response text directly, using only
4
+ the Python standard library:
5
+
6
+ - :class:`StdlibIntentScorer` is a bag-of-words binary classifier with
7
+ hashing-trick features and the SDK's pure-Python logistic regression
8
+ primitive. It returns an unfitted ``pass``-prior policy until fitted.
9
+ - :class:`StdlibAdherenceScorer` is a deterministic rule engine over
10
+ required substrings, forbidden substrings, length bounds, and an
11
+ optional JSON-shape requirement. No training is required.
12
+ - :class:`StdlibCompletionScorer` is a token-coverage scorer that
13
+ measures how many of the expected tokens the response contains.
14
+ No training is required.
15
+
16
+ The wire-up contract is the same as every other scorer backend in the
17
+ SDK: each scorer exposes ``score(**kwargs) -> ScoreResult`` and ignores
18
+ extra keyword arguments. The three scorers expect:
19
+
20
+ - intent: ``query: str``, ``response: str``
21
+ - adherence: ``response: str``, ``contract: dict``
22
+ - completion: ``response: str``, ``expected_tokens: list[str]``
23
+
24
+ The factory in :mod:`agent_learning.scorers` routes ``tier="stdlib"``
25
+ to this package.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from .adherence import StdlibAdherenceScorer
31
+ from .completion import StdlibCompletionScorer
32
+ from .intent import StdlibIntentScorer
33
+
34
+ __all__ = [
35
+ "StdlibAdherenceScorer",
36
+ "StdlibCompletionScorer",
37
+ "StdlibIntentScorer",
38
+ ]