understudy 0.4.0__tar.gz → 0.5.0__tar.gz
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.
- {understudy-0.4.0 → understudy-0.5.0}/PKG-INFO +21 -4
- {understudy-0.4.0 → understudy-0.5.0}/README.md +20 -3
- {understudy-0.4.0 → understudy-0.5.0}/pyproject.toml +4 -1
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/__init__.py +55 -1
- understudy-0.5.0/src/understudy/agentic/__init__.py +53 -0
- understudy-0.5.0/src/understudy/agentic/check.py +198 -0
- understudy-0.5.0/src/understudy/agentic/metrics.py +170 -0
- understudy-0.5.0/src/understudy/agentic/models.py +264 -0
- understudy-0.5.0/src/understudy/agentic/runner.py +150 -0
- understudy-0.5.0/src/understudy/batch.py +53 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/check.py +56 -39
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/cli.py +568 -7
- understudy-0.5.0/src/understudy/diff.py +233 -0
- understudy-0.5.0/src/understudy/judge_backends.py +147 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/judges.py +69 -22
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/models.py +49 -10
- understudy-0.5.0/src/understudy/pytest_plugin.py +221 -0
- understudy-0.5.0/src/understudy/replay.py +196 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/runner.py +57 -36
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/storage.py +254 -209
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/suite.py +65 -60
- understudy-0.5.0/src/understudy/templates/init/conftest.py.template +70 -0
- understudy-0.5.0/src/understudy/templates/init/example_scene.yaml +36 -0
- understudy-0.5.0/src/understudy/templates/init/test_agent.py.template +43 -0
- understudy-0.5.0/src/understudy/templates/init/understudy.yaml.template +16 -0
- understudy-0.5.0/src/understudy/validation.py +173 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/adk/__init__.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/compare.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/http/__init__.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/langgraph/__init__.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/langgraph/tools.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/__init__.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/builtins.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/registry.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/templates/clarity.jinja +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/templates/policy_compliance.jinja +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/templates/tone_empathy.jinja +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/mocks.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/prompts/__init__.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/prompts/rubrics.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/reports.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/server/__init__.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/server/models.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/server/sessions.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/server/ui_simulator.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/simulator.py +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/templates/base.html +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/templates/comparison.html +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/templates/index.html +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/templates/run_detail.html +0 -0
- {understudy-0.4.0 → understudy-0.5.0}/src/understudy/trace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: understudy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Simulation and trace-based evaluation for agentic systems
|
|
5
5
|
Keywords: agent,evaluation,simulation,testing,llm
|
|
6
6
|
Author: Gaurav Sood
|
|
@@ -72,10 +72,27 @@ Testing with understudy is **4 steps**:
|
|
|
72
72
|
|
|
73
73
|
The key insight: **assert against the trace, not the prose**. Don't check what the agent said—check what it did (tool calls).
|
|
74
74
|
|
|
75
|
+
## Two Evaluation Paradigms
|
|
76
|
+
|
|
77
|
+
### Conversational Agent Evaluation
|
|
78
|
+
Simulate multi-turn conversations with personas to test dialogue agents.
|
|
79
|
+
- Use case: Customer service bots, assistants, chatbots
|
|
80
|
+
- Assert on tool calls: `trace.called("tool_name")`
|
|
81
|
+
|
|
82
|
+
### Agentic Flow Evaluation
|
|
83
|
+
Evaluate autonomous agents executing multi-step tasks.
|
|
84
|
+
- Use case: Code agents, research agents, task automation
|
|
85
|
+
- Assert on actions: `trace.performed("action")`
|
|
86
|
+
|
|
87
|
+
See [examples/README.md](examples/README.md) for complete examples of both paradigms.
|
|
88
|
+
|
|
75
89
|
**See real examples:**
|
|
76
|
-
- [Example scene](https://github.com/gojiplus/understudy/blob/main/
|
|
77
|
-
- [ADK test file](https://github.com/gojiplus/understudy/blob/main/
|
|
78
|
-
- [LangGraph test file](https://github.com/gojiplus/understudy/blob/main/
|
|
90
|
+
- [Example scene](https://github.com/gojiplus/understudy/blob/main/examples/scenes/return_eligible_backpack.yaml) — YAML defining a test scenario
|
|
91
|
+
- [ADK test file](https://github.com/gojiplus/understudy/blob/main/examples/adk/test_adk_returns.py) — pytest assertions against traces
|
|
92
|
+
- [LangGraph test file](https://github.com/gojiplus/understudy/blob/main/examples/langgraph/test_langgraph_returns.py) — same tests, different framework
|
|
93
|
+
- [Agentic test file](https://github.com/gojiplus/understudy/blob/main/examples/agentic/test_agentic.py) — agentic flow evaluation
|
|
94
|
+
- [Agentic scene](https://github.com/gojiplus/understudy/blob/main/examples/agentic_scenes/code_review_task.yaml) — task-based scenario
|
|
95
|
+
- [Example report](https://htmlpreview.github.io/?https://github.com/gojiplus/understudy/blob/main/examples/langgraph/report/index.html) — HTML report with metrics and transcripts
|
|
79
96
|
|
|
80
97
|
## Installation
|
|
81
98
|
|
|
@@ -20,10 +20,27 @@ Testing with understudy is **4 steps**:
|
|
|
20
20
|
|
|
21
21
|
The key insight: **assert against the trace, not the prose**. Don't check what the agent said—check what it did (tool calls).
|
|
22
22
|
|
|
23
|
+
## Two Evaluation Paradigms
|
|
24
|
+
|
|
25
|
+
### Conversational Agent Evaluation
|
|
26
|
+
Simulate multi-turn conversations with personas to test dialogue agents.
|
|
27
|
+
- Use case: Customer service bots, assistants, chatbots
|
|
28
|
+
- Assert on tool calls: `trace.called("tool_name")`
|
|
29
|
+
|
|
30
|
+
### Agentic Flow Evaluation
|
|
31
|
+
Evaluate autonomous agents executing multi-step tasks.
|
|
32
|
+
- Use case: Code agents, research agents, task automation
|
|
33
|
+
- Assert on actions: `trace.performed("action")`
|
|
34
|
+
|
|
35
|
+
See [examples/README.md](examples/README.md) for complete examples of both paradigms.
|
|
36
|
+
|
|
23
37
|
**See real examples:**
|
|
24
|
-
- [Example scene](https://github.com/gojiplus/understudy/blob/main/
|
|
25
|
-
- [ADK test file](https://github.com/gojiplus/understudy/blob/main/
|
|
26
|
-
- [LangGraph test file](https://github.com/gojiplus/understudy/blob/main/
|
|
38
|
+
- [Example scene](https://github.com/gojiplus/understudy/blob/main/examples/scenes/return_eligible_backpack.yaml) — YAML defining a test scenario
|
|
39
|
+
- [ADK test file](https://github.com/gojiplus/understudy/blob/main/examples/adk/test_adk_returns.py) — pytest assertions against traces
|
|
40
|
+
- [LangGraph test file](https://github.com/gojiplus/understudy/blob/main/examples/langgraph/test_langgraph_returns.py) — same tests, different framework
|
|
41
|
+
- [Agentic test file](https://github.com/gojiplus/understudy/blob/main/examples/agentic/test_agentic.py) — agentic flow evaluation
|
|
42
|
+
- [Agentic scene](https://github.com/gojiplus/understudy/blob/main/examples/agentic_scenes/code_review_task.yaml) — task-based scenario
|
|
43
|
+
- [Example report](https://htmlpreview.github.io/?https://github.com/gojiplus/understudy/blob/main/examples/langgraph/report/index.html) — HTML report with metrics and transcripts
|
|
27
44
|
|
|
28
45
|
## Installation
|
|
29
46
|
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "understudy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Simulation and trace-based evaluation for agentic systems"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -52,6 +52,9 @@ dev = [
|
|
|
52
52
|
[project.scripts]
|
|
53
53
|
understudy = "understudy.cli:main"
|
|
54
54
|
|
|
55
|
+
[project.entry-points.pytest11]
|
|
56
|
+
understudy = "understudy.pytest_plugin"
|
|
57
|
+
|
|
55
58
|
[project.urls]
|
|
56
59
|
Homepage = "https://github.com/gojiplus/understudy"
|
|
57
60
|
Documentation = "https://gojiplus.github.io/understudy"
|
|
@@ -7,8 +7,26 @@ not by reading the script, but by inspecting what actually happened.
|
|
|
7
7
|
|
|
8
8
|
from importlib.metadata import version
|
|
9
9
|
|
|
10
|
+
from . import agentic
|
|
11
|
+
from .agentic import (
|
|
12
|
+
AgenticApp,
|
|
13
|
+
AgenticCheckItem,
|
|
14
|
+
AgenticCheckResult,
|
|
15
|
+
AgenticExpectations,
|
|
16
|
+
AgenticMetricResult,
|
|
17
|
+
AgenticScene,
|
|
18
|
+
AgenticTrace,
|
|
19
|
+
Artifact,
|
|
20
|
+
Step,
|
|
21
|
+
StepResult,
|
|
22
|
+
Task,
|
|
23
|
+
check_agentic,
|
|
24
|
+
run_agentic,
|
|
25
|
+
)
|
|
10
26
|
from .check import CheckItem, CheckResult, EvaluationResult, check, evaluate, evaluate_batch
|
|
11
27
|
from .compare import ComparisonResult, SceneComparison, compare_runs
|
|
28
|
+
from .diff import ToolCallDiff, TraceDiff, diff_tool_sequences, diff_traces
|
|
29
|
+
from .judge_backends import CallbackBackend, JudgeBackend, LiteLLMBackend
|
|
12
30
|
from .judges import FailureAnalysis, FailureAnalyzer, Judge, JudgeResult
|
|
13
31
|
from .metrics import MetricRegistry, MetricResult
|
|
14
32
|
from .mocks import MockToolkit, ToolError
|
|
@@ -22,11 +40,14 @@ from .prompts import (
|
|
|
22
40
|
TONE_EMPATHY,
|
|
23
41
|
TOOL_USAGE_CORRECTNESS,
|
|
24
42
|
)
|
|
25
|
-
from .
|
|
43
|
+
from .pytest_plugin import AssertionHelpers
|
|
44
|
+
from .replay import ReplayResult, create_replay_scene, load_trace, replay
|
|
45
|
+
from .runner import AgentApp, AgentResponse, run, simulate, simulate_batch
|
|
26
46
|
from .simulator import Simulator, SimulatorBackend
|
|
27
47
|
from .storage import EvaluationStorage, RunStorage, TraceStorage
|
|
28
48
|
from .suite import SceneResult, Suite, SuiteResults
|
|
29
49
|
from .trace import AgentTransfer, StateSnapshot, ToolCall, Trace, TraceMetrics, Turn, TurnMetrics
|
|
50
|
+
from .validation import SceneValidationError
|
|
30
51
|
|
|
31
52
|
__version__ = version("understudy")
|
|
32
53
|
|
|
@@ -73,14 +94,32 @@ __all__ = [
|
|
|
73
94
|
"compare_runs",
|
|
74
95
|
"ComparisonResult",
|
|
75
96
|
"SceneComparison",
|
|
97
|
+
# diff
|
|
98
|
+
"diff_traces",
|
|
99
|
+
"diff_tool_sequences",
|
|
100
|
+
"TraceDiff",
|
|
101
|
+
"ToolCallDiff",
|
|
102
|
+
# replay
|
|
103
|
+
"replay",
|
|
104
|
+
"load_trace",
|
|
105
|
+
"create_replay_scene",
|
|
106
|
+
"ReplayResult",
|
|
76
107
|
# judges
|
|
77
108
|
"Judge",
|
|
78
109
|
"JudgeResult",
|
|
79
110
|
"FailureAnalyzer",
|
|
80
111
|
"FailureAnalysis",
|
|
112
|
+
# judge backends
|
|
113
|
+
"JudgeBackend",
|
|
114
|
+
"LiteLLMBackend",
|
|
115
|
+
"CallbackBackend",
|
|
81
116
|
# mocks
|
|
82
117
|
"MockToolkit",
|
|
83
118
|
"ToolError",
|
|
119
|
+
# validation
|
|
120
|
+
"SceneValidationError",
|
|
121
|
+
# pytest
|
|
122
|
+
"AssertionHelpers",
|
|
84
123
|
# simulator
|
|
85
124
|
"Simulator",
|
|
86
125
|
"SimulatorBackend",
|
|
@@ -92,4 +131,19 @@ __all__ = [
|
|
|
92
131
|
"TASK_COMPLETION",
|
|
93
132
|
"FACTUAL_GROUNDING",
|
|
94
133
|
"INSTRUCTION_FOLLOWING",
|
|
134
|
+
# agentic module
|
|
135
|
+
"agentic",
|
|
136
|
+
"AgenticApp",
|
|
137
|
+
"AgenticCheckItem",
|
|
138
|
+
"AgenticCheckResult",
|
|
139
|
+
"AgenticExpectations",
|
|
140
|
+
"AgenticMetricResult",
|
|
141
|
+
"AgenticScene",
|
|
142
|
+
"AgenticTrace",
|
|
143
|
+
"Artifact",
|
|
144
|
+
"Step",
|
|
145
|
+
"StepResult",
|
|
146
|
+
"Task",
|
|
147
|
+
"check_agentic",
|
|
148
|
+
"run_agentic",
|
|
95
149
|
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Agentic flow evaluation module for understudy.
|
|
2
|
+
|
|
3
|
+
This module provides evaluation capabilities for autonomous agent flows,
|
|
4
|
+
complementing the conversational agent evaluation in the main understudy module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .check import (
|
|
8
|
+
AgenticCheckItem,
|
|
9
|
+
AgenticCheckResult,
|
|
10
|
+
check_agentic,
|
|
11
|
+
)
|
|
12
|
+
from .metrics import (
|
|
13
|
+
AgenticMetricResult,
|
|
14
|
+
action_efficiency,
|
|
15
|
+
action_trajectory,
|
|
16
|
+
compute_all_metrics,
|
|
17
|
+
goal_completion,
|
|
18
|
+
reasoning_quality,
|
|
19
|
+
)
|
|
20
|
+
from .models import (
|
|
21
|
+
AgenticExpectations,
|
|
22
|
+
AgenticScene,
|
|
23
|
+
AgenticTrace,
|
|
24
|
+
Artifact,
|
|
25
|
+
Step,
|
|
26
|
+
Task,
|
|
27
|
+
)
|
|
28
|
+
from .runner import (
|
|
29
|
+
AgenticApp,
|
|
30
|
+
StepResult,
|
|
31
|
+
run_agentic,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AgenticApp",
|
|
36
|
+
"AgenticCheckItem",
|
|
37
|
+
"AgenticCheckResult",
|
|
38
|
+
"AgenticExpectations",
|
|
39
|
+
"AgenticMetricResult",
|
|
40
|
+
"AgenticScene",
|
|
41
|
+
"AgenticTrace",
|
|
42
|
+
"Artifact",
|
|
43
|
+
"Step",
|
|
44
|
+
"StepResult",
|
|
45
|
+
"Task",
|
|
46
|
+
"action_efficiency",
|
|
47
|
+
"action_trajectory",
|
|
48
|
+
"check_agentic",
|
|
49
|
+
"compute_all_metrics",
|
|
50
|
+
"goal_completion",
|
|
51
|
+
"reasoning_quality",
|
|
52
|
+
"run_agentic",
|
|
53
|
+
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Check: validate an agentic trace against expectations."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .models import AgenticExpectations, AgenticTrace
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class AgenticCheckItem:
|
|
12
|
+
"""A single check result for agentic evaluation."""
|
|
13
|
+
|
|
14
|
+
label: str
|
|
15
|
+
passed: bool
|
|
16
|
+
detail: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AgenticCheckResult:
|
|
21
|
+
"""Result of checking an agentic trace against expectations."""
|
|
22
|
+
|
|
23
|
+
checks: list[AgenticCheckItem] = field(default_factory=list)
|
|
24
|
+
metrics: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def passed(self) -> bool:
|
|
28
|
+
return all(c.passed for c in self.checks)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def failed_checks(self) -> list[AgenticCheckItem]:
|
|
32
|
+
return [c for c in self.checks if not c.passed]
|
|
33
|
+
|
|
34
|
+
def summary(self) -> str:
|
|
35
|
+
lines = []
|
|
36
|
+
for c in self.checks:
|
|
37
|
+
mark = "+" if c.passed else "-"
|
|
38
|
+
lines.append(f" {mark} {c.label}: {c.detail}")
|
|
39
|
+
for name, value in self.metrics.items():
|
|
40
|
+
lines.append(f" * {name}: {value}")
|
|
41
|
+
return "\n".join(lines)
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
n_checks = len(self.checks)
|
|
45
|
+
n_pass = sum(1 for c in self.checks if c.passed)
|
|
46
|
+
return f"AgenticCheckResult({n_pass}/{n_checks} passed)"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _matches_pattern(action: str, pattern: str) -> bool:
|
|
50
|
+
"""Check if an action matches a pattern (supports wildcards)."""
|
|
51
|
+
if "*" in pattern or "?" in pattern:
|
|
52
|
+
return fnmatch.fnmatch(action, pattern)
|
|
53
|
+
return action == pattern
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _evaluate_predicate(predicate: str, trace: AgenticTrace) -> bool:
|
|
57
|
+
"""Evaluate a goal predicate against the trace.
|
|
58
|
+
|
|
59
|
+
Supports:
|
|
60
|
+
- Simple expressions: "outcome == 'success'"
|
|
61
|
+
- Access to trace fields: outcome, total_steps, total_tokens, final_state
|
|
62
|
+
"""
|
|
63
|
+
local_vars = {
|
|
64
|
+
"outcome": trace.outcome,
|
|
65
|
+
"total_steps": trace.total_steps,
|
|
66
|
+
"total_tokens": trace.total_tokens,
|
|
67
|
+
"final_state": trace.final_state,
|
|
68
|
+
"artifacts": [a.model_dump() for a in trace.artifacts],
|
|
69
|
+
"steps": trace.steps,
|
|
70
|
+
}
|
|
71
|
+
try:
|
|
72
|
+
result = eval(predicate, {"__builtins__": {}}, local_vars)
|
|
73
|
+
return bool(result)
|
|
74
|
+
except Exception:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _compare_outputs(actual: dict, expected: dict) -> tuple[bool, str]:
|
|
79
|
+
"""Compare actual output against golden output."""
|
|
80
|
+
differences = []
|
|
81
|
+
|
|
82
|
+
for key, expected_value in expected.items():
|
|
83
|
+
if key not in actual:
|
|
84
|
+
differences.append(f"missing key: {key}")
|
|
85
|
+
elif actual[key] != expected_value:
|
|
86
|
+
differences.append(f"{key}: expected {expected_value}, got {actual[key]}")
|
|
87
|
+
|
|
88
|
+
if differences:
|
|
89
|
+
return False, "; ".join(differences)
|
|
90
|
+
return True, "output matches expected"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def check_agentic(trace: AgenticTrace, expectations: AgenticExpectations) -> AgenticCheckResult:
|
|
94
|
+
"""Validate an agentic trace against expectations.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
trace: The agentic execution trace.
|
|
98
|
+
expectations: The expectations to check against.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
An AgenticCheckResult with individual check outcomes.
|
|
102
|
+
"""
|
|
103
|
+
result = AgenticCheckResult()
|
|
104
|
+
performed_actions = set(trace.action_sequence())
|
|
105
|
+
|
|
106
|
+
if expectations.goal_predicate:
|
|
107
|
+
predicate_passed = _evaluate_predicate(expectations.goal_predicate, trace)
|
|
108
|
+
result.checks.append(
|
|
109
|
+
AgenticCheckItem(
|
|
110
|
+
label="goal_predicate",
|
|
111
|
+
passed=predicate_passed,
|
|
112
|
+
detail=(
|
|
113
|
+
f"predicate '{expectations.goal_predicate}' "
|
|
114
|
+
f"{'satisfied' if predicate_passed else 'NOT satisfied'}"
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if expectations.golden_output:
|
|
120
|
+
output_passed, output_detail = _compare_outputs(
|
|
121
|
+
trace.final_state, expectations.golden_output
|
|
122
|
+
)
|
|
123
|
+
result.checks.append(
|
|
124
|
+
AgenticCheckItem(
|
|
125
|
+
label="golden_output",
|
|
126
|
+
passed=output_passed,
|
|
127
|
+
detail=output_detail,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if expectations.max_steps is not None:
|
|
132
|
+
steps_ok = trace.total_steps <= expectations.max_steps
|
|
133
|
+
result.checks.append(
|
|
134
|
+
AgenticCheckItem(
|
|
135
|
+
label="max_steps",
|
|
136
|
+
passed=steps_ok,
|
|
137
|
+
detail=(
|
|
138
|
+
f"steps={trace.total_steps}, max={expectations.max_steps} "
|
|
139
|
+
f"({'OK' if steps_ok else 'EXCEEDED'})"
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if expectations.max_tokens is not None:
|
|
145
|
+
tokens_ok = trace.total_tokens <= expectations.max_tokens
|
|
146
|
+
result.checks.append(
|
|
147
|
+
AgenticCheckItem(
|
|
148
|
+
label="max_tokens",
|
|
149
|
+
passed=tokens_ok,
|
|
150
|
+
detail=(
|
|
151
|
+
f"tokens={trace.total_tokens}, max={expectations.max_tokens} "
|
|
152
|
+
f"({'OK' if tokens_ok else 'EXCEEDED'})"
|
|
153
|
+
),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if expectations.max_retries is not None:
|
|
158
|
+
retries = trace.retry_count()
|
|
159
|
+
retries_ok = retries <= expectations.max_retries
|
|
160
|
+
result.checks.append(
|
|
161
|
+
AgenticCheckItem(
|
|
162
|
+
label="max_retries",
|
|
163
|
+
passed=retries_ok,
|
|
164
|
+
detail=(
|
|
165
|
+
f"retries={retries}, max={expectations.max_retries} "
|
|
166
|
+
f"({'OK' if retries_ok else 'EXCEEDED'})"
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
for action in expectations.required_actions:
|
|
172
|
+
found = any(_matches_pattern(a, action) for a in performed_actions)
|
|
173
|
+
result.checks.append(
|
|
174
|
+
AgenticCheckItem(
|
|
175
|
+
label="required_action",
|
|
176
|
+
passed=found,
|
|
177
|
+
detail=f"{action} {'performed' if found else 'NOT performed'}",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
for pattern in expectations.forbidden_actions:
|
|
182
|
+
violations = [a for a in performed_actions if _matches_pattern(a, pattern)]
|
|
183
|
+
passed = len(violations) == 0
|
|
184
|
+
result.checks.append(
|
|
185
|
+
AgenticCheckItem(
|
|
186
|
+
label="forbidden_action",
|
|
187
|
+
passed=passed,
|
|
188
|
+
detail=f"{pattern} VIOLATED by {violations}" if violations else f"{pattern} OK",
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
result.metrics["outcome"] = trace.outcome
|
|
193
|
+
result.metrics["total_steps"] = trace.total_steps
|
|
194
|
+
result.metrics["total_tokens"] = trace.total_tokens
|
|
195
|
+
result.metrics["total_latency_ms"] = trace.total_latency_ms
|
|
196
|
+
result.metrics["retry_count"] = trace.retry_count()
|
|
197
|
+
|
|
198
|
+
return result
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Metrics: agentic-specific evaluation metrics."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .models import AgenticExpectations, AgenticTrace
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AgenticMetricResult:
|
|
11
|
+
"""Result of computing an agentic metric."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
value: Any
|
|
15
|
+
passed: bool | None = None
|
|
16
|
+
detail: str = ""
|
|
17
|
+
|
|
18
|
+
def __repr__(self) -> str:
|
|
19
|
+
if self.passed is not None:
|
|
20
|
+
status = "passed" if self.passed else "failed"
|
|
21
|
+
return f"AgenticMetricResult({self.name}: {status})"
|
|
22
|
+
return f"AgenticMetricResult({self.name}: {self.value})"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def goal_completion(trace: AgenticTrace) -> AgenticMetricResult:
|
|
26
|
+
"""Compute goal completion metric based on outcome.
|
|
27
|
+
|
|
28
|
+
Returns 1.0 for success, 0.0 for failure/error.
|
|
29
|
+
"""
|
|
30
|
+
success_outcomes = {"success", "completed", "done", "finished"}
|
|
31
|
+
completed = trace.outcome.lower() in success_outcomes
|
|
32
|
+
|
|
33
|
+
return AgenticMetricResult(
|
|
34
|
+
name="goal_completion",
|
|
35
|
+
value=1.0 if completed else 0.0,
|
|
36
|
+
passed=completed,
|
|
37
|
+
detail=f"outcome='{trace.outcome}'",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def reasoning_quality(trace: AgenticTrace) -> AgenticMetricResult:
|
|
42
|
+
"""Analyze the quality of reasoning steps.
|
|
43
|
+
|
|
44
|
+
Computes basic metrics about the reasoning process:
|
|
45
|
+
- Number of thinking steps
|
|
46
|
+
- Average reasoning length
|
|
47
|
+
- Think-before-act ratio
|
|
48
|
+
"""
|
|
49
|
+
thinking_steps = trace.thinking_steps()
|
|
50
|
+
action_steps = [s for s in trace.steps if s.step_type == "act"]
|
|
51
|
+
|
|
52
|
+
n_thinking = len(thinking_steps)
|
|
53
|
+
n_actions = len(action_steps)
|
|
54
|
+
|
|
55
|
+
if n_thinking == 0:
|
|
56
|
+
return AgenticMetricResult(
|
|
57
|
+
name="reasoning_quality",
|
|
58
|
+
value={
|
|
59
|
+
"thinking_steps": 0,
|
|
60
|
+
"action_steps": n_actions,
|
|
61
|
+
"think_act_ratio": 0.0,
|
|
62
|
+
"avg_reasoning_length": 0,
|
|
63
|
+
},
|
|
64
|
+
detail="No thinking steps recorded",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
total_reasoning_length = sum(len(s.reasoning or "") for s in thinking_steps)
|
|
68
|
+
avg_reasoning_length = total_reasoning_length / n_thinking
|
|
69
|
+
|
|
70
|
+
think_act_ratio = n_thinking / max(n_actions, 1)
|
|
71
|
+
|
|
72
|
+
return AgenticMetricResult(
|
|
73
|
+
name="reasoning_quality",
|
|
74
|
+
value={
|
|
75
|
+
"thinking_steps": n_thinking,
|
|
76
|
+
"action_steps": n_actions,
|
|
77
|
+
"think_act_ratio": round(think_act_ratio, 2),
|
|
78
|
+
"avg_reasoning_length": int(avg_reasoning_length),
|
|
79
|
+
},
|
|
80
|
+
detail=f"{n_thinking} thinking steps, ratio={think_act_ratio:.2f}",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def action_efficiency(
|
|
85
|
+
trace: AgenticTrace, expectations: AgenticExpectations | None = None
|
|
86
|
+
) -> AgenticMetricResult:
|
|
87
|
+
"""Compute action efficiency metrics.
|
|
88
|
+
|
|
89
|
+
Includes:
|
|
90
|
+
- Steps per action (lower is better)
|
|
91
|
+
- Retry rate (lower is better)
|
|
92
|
+
- Token efficiency
|
|
93
|
+
"""
|
|
94
|
+
total_steps = trace.total_steps
|
|
95
|
+
action_steps = [s for s in trace.steps if s.step_type == "act"]
|
|
96
|
+
n_actions = len(action_steps)
|
|
97
|
+
n_retries = trace.retry_count()
|
|
98
|
+
total_tokens = trace.total_tokens
|
|
99
|
+
|
|
100
|
+
if n_actions == 0:
|
|
101
|
+
return AgenticMetricResult(
|
|
102
|
+
name="action_efficiency",
|
|
103
|
+
value={
|
|
104
|
+
"total_steps": total_steps,
|
|
105
|
+
"action_steps": 0,
|
|
106
|
+
"retry_count": 0,
|
|
107
|
+
"retry_rate": 0.0,
|
|
108
|
+
"tokens_per_action": 0,
|
|
109
|
+
},
|
|
110
|
+
detail="No actions performed",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
retry_rate = n_retries / n_actions
|
|
114
|
+
tokens_per_action = total_tokens / n_actions if total_tokens > 0 else 0
|
|
115
|
+
|
|
116
|
+
efficiency_passed = None
|
|
117
|
+
if expectations:
|
|
118
|
+
checks = []
|
|
119
|
+
if expectations.max_steps is not None:
|
|
120
|
+
checks.append(total_steps <= expectations.max_steps)
|
|
121
|
+
if expectations.max_retries is not None:
|
|
122
|
+
checks.append(n_retries <= expectations.max_retries)
|
|
123
|
+
if expectations.max_tokens is not None:
|
|
124
|
+
checks.append(total_tokens <= expectations.max_tokens)
|
|
125
|
+
if checks:
|
|
126
|
+
efficiency_passed = all(checks)
|
|
127
|
+
|
|
128
|
+
return AgenticMetricResult(
|
|
129
|
+
name="action_efficiency",
|
|
130
|
+
value={
|
|
131
|
+
"total_steps": total_steps,
|
|
132
|
+
"action_steps": n_actions,
|
|
133
|
+
"retry_count": n_retries,
|
|
134
|
+
"retry_rate": round(retry_rate, 3),
|
|
135
|
+
"tokens_per_action": int(tokens_per_action),
|
|
136
|
+
"total_tokens": total_tokens,
|
|
137
|
+
"total_latency_ms": trace.total_latency_ms,
|
|
138
|
+
},
|
|
139
|
+
passed=efficiency_passed,
|
|
140
|
+
detail=f"{n_actions} actions, {n_retries} retries, {total_tokens} tokens",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def action_trajectory(trace: AgenticTrace) -> AgenticMetricResult:
|
|
145
|
+
"""Compute action trajectory metrics."""
|
|
146
|
+
sequence = trace.action_sequence()
|
|
147
|
+
unique_actions = list(set(sequence))
|
|
148
|
+
|
|
149
|
+
return AgenticMetricResult(
|
|
150
|
+
name="action_trajectory",
|
|
151
|
+
value={
|
|
152
|
+
"sequence": sequence,
|
|
153
|
+
"unique_actions": unique_actions,
|
|
154
|
+
"total_actions": len(sequence),
|
|
155
|
+
"unique_count": len(unique_actions),
|
|
156
|
+
},
|
|
157
|
+
detail=f"{len(sequence)} actions, {len(unique_actions)} unique",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def compute_all_metrics(
|
|
162
|
+
trace: AgenticTrace, expectations: AgenticExpectations | None = None
|
|
163
|
+
) -> dict[str, AgenticMetricResult]:
|
|
164
|
+
"""Compute all agentic metrics."""
|
|
165
|
+
return {
|
|
166
|
+
"goal_completion": goal_completion(trace),
|
|
167
|
+
"reasoning_quality": reasoning_quality(trace),
|
|
168
|
+
"action_efficiency": action_efficiency(trace, expectations),
|
|
169
|
+
"action_trajectory": action_trajectory(trace),
|
|
170
|
+
}
|