forked from bojieli/ai-agent-book
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverifier.py
More file actions
351 lines (312 loc) · 13.3 KB
/
Copy pathverifier.py
File metadata and controls
351 lines (312 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
"""Three-layer trajectory verifier used by Experiment 9-1.
Environment and policy conclusions stay deterministic. Only the two open-
ended language dimensions are delegated to a quality Judge.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Any, Dict, Iterable, List, Protocol
PASS = "pass"
FAIL = "fail"
UNCERTAIN = "uncertain"
@dataclass(frozen=True)
class DimensionResult:
dimension: str
layer: str
verdict: str
score: float
evidence: List[str]
confidence: float
class QualityJudge(Protocol):
"""Interface for the only layer that may need an LLM."""
def evaluate(self, trajectory: Dict[str, Any]) -> Iterable[DimensionResult]: ...
def _successful_calls(trajectory: Dict[str, Any]) -> List[Dict[str, Any]]:
if not isinstance(trajectory, dict):
trajectory = {}
calls = trajectory.get("tool_calls")
if not isinstance(calls, list):
calls = []
return [
call
for call in calls
if isinstance(call, dict)
and isinstance(call.get("result"), dict)
and call.get("result", {}).get("success") is True
]
def _precedes(call: Dict[str, Any], promise: Dict[str, Any]) -> bool:
"""Return whether both records have numeric turns and the call came first."""
call_turn = call.get("turn")
promise_turn = promise.get("turn")
return (
isinstance(call_turn, (int, float))
and not isinstance(call_turn, bool)
and isinstance(promise_turn, (int, float))
and not isinstance(promise_turn, bool)
and call_turn < promise_turn
)
def _assistant_text(trajectory: Dict[str, Any]) -> str:
if not isinstance(trajectory, dict):
trajectory = {}
messages = trajectory.get("messages")
if not isinstance(messages, list):
messages = []
return "\n".join(
str(message.get("content") or "")
for message in messages
if isinstance(message, dict) and message.get("role") == "assistant"
)
class ResultVerifier:
"""Checks the final environment state instead of trusting the reply."""
def evaluate(self, trajectory: Dict[str, Any]) -> List[DimensionResult]:
if not isinstance(trajectory, dict):
trajectory = {}
expected = trajectory.get("expected_outcome")
if not isinstance(expected, dict):
expected = {}
final_state = trajectory.get("final_state")
if not isinstance(final_state, dict):
final_state = {}
mismatches = [
f"{key}: expected={value!r}, actual={final_state.get(key)!r}"
for key, value in expected.items()
if final_state.get(key) != value
]
if mismatches:
return [DimensionResult(
"task_resolution", "environment_result", FAIL, 0.0,
mismatches, 1.0,
)]
evidence = [f"final_state.{key}={value!r}" for key, value in expected.items()]
if not evidence:
return [DimensionResult(
"task_resolution", "environment_result", UNCERTAIN, 0.5,
["No machine-checkable expected outcome was supplied"], 0.4,
)]
return [DimensionResult(
"task_resolution", "environment_result", PASS, 1.0, evidence, 1.0,
)]
class ProcessVerifier:
"""Checks policy, privacy, grounded claims and promise/action consistency."""
def evaluate(self, trajectory: Dict[str, Any]) -> List[DimensionResult]:
return [
self._policy(trajectory),
self._privacy(trajectory),
self._grounding(trajectory),
self._promise_action(trajectory),
]
def _policy(self, trajectory: Dict[str, Any]) -> DimensionResult:
facts = trajectory.get("process_facts")
if not isinstance(facts, dict):
facts = {}
violations = facts.get("policy_violations")
if not isinstance(violations, list):
violations = []
if violations:
evidence = [
f"turn {item.get('turn', '?')}: {item.get('rule', 'policy violation')}"
for item in violations
if isinstance(item, dict)
]
return DimensionResult("rule_compliance", "process_rules", FAIL, 0.0, evidence, 1.0)
checked = facts.get("checked_rules")
if not isinstance(checked, list):
checked = []
evidence = [f"checked: {rule}" for rule in checked] or ["No policy violation in action log"]
return DimensionResult("rule_compliance", "process_rules", PASS, 1.0, evidence, 0.95)
def _privacy(self, trajectory: Dict[str, Any]) -> DimensionResult:
reply = _assistant_text(trajectory)
sensitive = trajectory.get("sensitive_values")
if not isinstance(sensitive, list):
sensitive = []
leaks = [
item for item in sensitive
if isinstance(item, dict) and item.get("value") and str(item["value"]) in reply
]
if leaks:
return DimensionResult(
"privacy_boundary", "process_rules", FAIL, 0.0,
[f"assistant exposed {item.get('label', 'sensitive value')}" for item in leaks],
1.0,
)
return DimensionResult(
"privacy_boundary", "process_rules", PASS, 1.0,
["No supplied sensitive value appears in an assistant message"], 0.98,
)
def _grounding(self, trajectory: Dict[str, Any]) -> DimensionResult:
claims = trajectory.get("claims")
if not isinstance(claims, list):
claims = []
unsupported = [
claim for claim in claims
if isinstance(claim, dict) and not claim.get("supported_by")
]
if unsupported:
return DimensionResult(
"factual_reliability", "process_rules", FAIL, 0.0,
[f"turn {claim.get('turn', '?')}: unsupported claim: {claim.get('text', '')}" for claim in unsupported],
0.95,
)
evidence = [
f"turn {claim.get('turn', '?')}: supported by {claim.get('supported_by')}"
for claim in claims
if isinstance(claim, dict)
] or ["No externally checkable claim was made"]
return DimensionResult("factual_reliability", "process_rules", PASS, 1.0, evidence, 0.9)
def _promise_action(self, trajectory: Dict[str, Any]) -> DimensionResult:
successful = [
call for call in _successful_calls(trajectory)
if isinstance(call, dict)
]
promises = trajectory.get("promises")
if not isinstance(promises, list):
promises = []
missing = [
promise for promise in promises
if isinstance(promise, dict) and not any(
call.get("name") == promise.get("required_tool")
and _precedes(call, promise)
for call in successful
)
]
if missing:
return DimensionResult(
"promise_action_consistency", "process_rules", FAIL, 0.0,
[
f"turn {promise.get('turn', '?')}: claimed {promise.get('text', '')!r}, "
f"but no successful {promise.get('required_tool')} call preceded it"
for promise in missing
],
1.0,
)
evidence = [
f"turn {promise.get('turn', '?')}: {promise.get('required_tool')} succeeded"
for promise in promises
if isinstance(promise, dict)
] or ["No action promise was made"]
return DimensionResult(
"promise_action_consistency", "process_rules", PASS, 1.0, evidence, 0.98,
)
class HeuristicQualityJudge:
"""Deterministic stand-in for an evidence-citing LLM rubric judge.
``quality_facts`` represent facts an online LLM judge would infer from the
dialogue. Keeping them explicit makes the calibration demo reproducible.
"""
def evaluate(self, trajectory: Dict[str, Any]) -> List[DimensionResult]:
if not isinstance(trajectory, dict):
trajectory = {}
facts = trajectory.get("quality_facts")
if not isinstance(facts, dict):
facts = {}
expression_issues = facts.get("expression_issues")
if not isinstance(expression_issues, list):
expression_issues = []
if expression_issues:
expression = DimensionResult(
"expression_quality", "llm_rubric", FAIL, 0.0,
[
f"turn {issue.get('turn', '?')}: {issue.get('issue', 'quality issue')}"
if isinstance(issue, dict)
else str(issue)
for issue in expression_issues
],
float(facts.get("expression_confidence", 0.85)),
)
else:
expression = DimensionResult(
"expression_quality", "llm_rubric", PASS, 1.0,
["Reply is concise, natural and non-repetitive"],
float(facts.get("expression_confidence", 0.8)),
)
blocked = facts.get("primary_path_blocked", False)
alternative = facts.get("allowed_alternative_offered", False)
if blocked and not alternative:
flexibility = DimensionResult(
"compliant_flexibility", "llm_rubric", FAIL, 0.0,
[f"turn {facts.get('decision_turn', '?')}: stopped at refusal although an allowed alternative existed"],
float(facts.get("flexibility_confidence", 0.85)),
)
else:
note = "Allowed alternative was offered" if alternative else "Primary path was not blocked"
flexibility = DimensionResult(
"compliant_flexibility", "llm_rubric", PASS, 1.0, [note],
float(facts.get("flexibility_confidence", 0.8)),
)
return [expression, flexibility]
class TrajectoryVerifier:
def __init__(self, quality_judge: QualityJudge | None = None, review_confidence: float = 0.75):
self.result_verifier = ResultVerifier()
self.process_verifier = ProcessVerifier()
self.quality_judge = quality_judge or HeuristicQualityJudge()
self.review_confidence = review_confidence
def evaluate(self, trajectory: Dict[str, Any]) -> Dict[str, Any]:
if not isinstance(trajectory, dict):
trajectory = {}
dimensions = [
*self.result_verifier.evaluate(trajectory),
*self.process_verifier.evaluate(trajectory),
*self.quality_judge.evaluate(trajectory),
]
scores = [item.score for item in dimensions]
critical_failures = [
item.dimension for item in dimensions
if item.verdict == FAIL and item.dimension in {
"task_resolution", "rule_compliance", "privacy_boundary",
"factual_reliability", "promise_action_consistency",
}
]
high_risk_failures = [
item.dimension for item in dimensions
if item.verdict == FAIL and item.dimension in {
"rule_compliance", "privacy_boundary", "promise_action_consistency",
}
]
low_confidence = [
item.dimension for item in dimensions
if item.confidence < self.review_confidence or item.verdict == UNCERTAIN
]
if high_risk_failures or low_confidence:
review = {
"required": True,
"destination": "human_review",
"status": "pending",
"reasons": {
"high_risk_failures": high_risk_failures,
"low_confidence_or_uncertain": low_confidence,
},
}
else:
review = {
"required": False,
"destination": None,
"status": "not_required",
"reasons": {"high_risk_failures": [], "low_confidence_or_uncertain": []},
}
return {
"trajectory_id": trajectory.get("id"),
"overall_score": round(sum(scores) / len(scores), 3) if scores else 0.0,
"release_recommendation": "reject" if critical_failures else "review_or_accept",
"critical_failures": critical_failures,
"review": review,
"eligible_as_automatic_learning_signal": not review["required"],
"dimensions": [asdict(item) for item in dimensions],
}
def scalar_baseline(report: Dict[str, Any]) -> Dict[str, Any]:
"""Simulates the information loss of returning one overall number."""
if not isinstance(report, dict):
report = {}
return {"trajectory_id": report.get("trajectory_id"), "score": report.get("overall_score")}
def _item_get(item: Any, key: str, default: Any = None) -> Any:
if isinstance(item, dict):
return item.get(key, default)
return getattr(item, key, default)
def diagnostic_utility(report: Dict[str, Any]) -> float:
"""Fraction of failed dimensions that include actionable evidence."""
if not isinstance(report, dict):
report = {}
dims = report.get("dimensions")
if not isinstance(dims, list):
dims = []
failures = [item for item in dims if _item_get(item, "verdict") == FAIL]
if not failures:
return 1.0
actionable = sum(bool(_item_get(item, "evidence")) for item in failures)
return actionable / len(failures)