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.
Files changed (51) hide show
  1. {understudy-0.4.0 → understudy-0.5.0}/PKG-INFO +21 -4
  2. {understudy-0.4.0 → understudy-0.5.0}/README.md +20 -3
  3. {understudy-0.4.0 → understudy-0.5.0}/pyproject.toml +4 -1
  4. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/__init__.py +55 -1
  5. understudy-0.5.0/src/understudy/agentic/__init__.py +53 -0
  6. understudy-0.5.0/src/understudy/agentic/check.py +198 -0
  7. understudy-0.5.0/src/understudy/agentic/metrics.py +170 -0
  8. understudy-0.5.0/src/understudy/agentic/models.py +264 -0
  9. understudy-0.5.0/src/understudy/agentic/runner.py +150 -0
  10. understudy-0.5.0/src/understudy/batch.py +53 -0
  11. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/check.py +56 -39
  12. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/cli.py +568 -7
  13. understudy-0.5.0/src/understudy/diff.py +233 -0
  14. understudy-0.5.0/src/understudy/judge_backends.py +147 -0
  15. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/judges.py +69 -22
  16. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/models.py +49 -10
  17. understudy-0.5.0/src/understudy/pytest_plugin.py +221 -0
  18. understudy-0.5.0/src/understudy/replay.py +196 -0
  19. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/runner.py +57 -36
  20. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/storage.py +254 -209
  21. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/suite.py +65 -60
  22. understudy-0.5.0/src/understudy/templates/init/conftest.py.template +70 -0
  23. understudy-0.5.0/src/understudy/templates/init/example_scene.yaml +36 -0
  24. understudy-0.5.0/src/understudy/templates/init/test_agent.py.template +43 -0
  25. understudy-0.5.0/src/understudy/templates/init/understudy.yaml.template +16 -0
  26. understudy-0.5.0/src/understudy/validation.py +173 -0
  27. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/adk/__init__.py +0 -0
  28. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/compare.py +0 -0
  29. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/http/__init__.py +0 -0
  30. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/langgraph/__init__.py +0 -0
  31. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/langgraph/tools.py +0 -0
  32. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/__init__.py +0 -0
  33. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/builtins.py +0 -0
  34. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/registry.py +0 -0
  35. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/templates/clarity.jinja +0 -0
  36. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/templates/policy_compliance.jinja +0 -0
  37. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/metrics/templates/tone_empathy.jinja +0 -0
  38. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/mocks.py +0 -0
  39. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/prompts/__init__.py +0 -0
  40. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/prompts/rubrics.py +0 -0
  41. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/reports.py +0 -0
  42. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/server/__init__.py +0 -0
  43. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/server/models.py +0 -0
  44. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/server/sessions.py +0 -0
  45. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/server/ui_simulator.py +0 -0
  46. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/simulator.py +0 -0
  47. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/templates/base.html +0 -0
  48. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/templates/comparison.html +0 -0
  49. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/templates/index.html +0 -0
  50. {understudy-0.4.0 → understudy-0.5.0}/src/understudy/templates/run_detail.html +0 -0
  51. {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.4.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/example/scenes/return_eligible_backpack.yaml) — YAML defining a test scenario
77
- - [ADK test file](https://github.com/gojiplus/understudy/blob/main/example/adk/test_returns.py) — pytest assertions against traces
78
- - [LangGraph test file](https://github.com/gojiplus/understudy/blob/main/example/langgraph/test_returns.py) — same tests, different framework
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/example/scenes/return_eligible_backpack.yaml) — YAML defining a test scenario
25
- - [ADK test file](https://github.com/gojiplus/understudy/blob/main/example/adk/test_returns.py) — pytest assertions against traces
26
- - [LangGraph test file](https://github.com/gojiplus/understudy/blob/main/example/langgraph/test_returns.py) — same tests, different framework
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.4.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 .runner import AgentApp, AgentResponse, LiteLLMBackend, run, simulate, simulate_batch
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
+ }