ragbits-evaluate 0.5.0__py3-none-any.whl → 1.4.0.dev202602030301__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.
- ragbits/evaluate/agent_simulation/__init__.py +87 -0
- ragbits/evaluate/agent_simulation/context.py +118 -0
- ragbits/evaluate/agent_simulation/conversation.py +333 -0
- ragbits/evaluate/agent_simulation/deepeval_evaluator.py +92 -0
- ragbits/evaluate/agent_simulation/logger.py +165 -0
- ragbits/evaluate/agent_simulation/metrics/__init__.py +19 -0
- ragbits/evaluate/agent_simulation/metrics/builtin.py +221 -0
- ragbits/evaluate/agent_simulation/metrics/collectors.py +142 -0
- ragbits/evaluate/agent_simulation/models.py +37 -0
- ragbits/evaluate/agent_simulation/results.py +200 -0
- ragbits/evaluate/agent_simulation/scenarios.py +129 -0
- ragbits/evaluate/agent_simulation/simulation.py +243 -0
- ragbits/evaluate/cli.py +150 -0
- ragbits/evaluate/config.py +11 -0
- ragbits/evaluate/dataloaders/__init__.py +3 -0
- ragbits/evaluate/dataloaders/base.py +95 -0
- ragbits/evaluate/dataloaders/document_search.py +61 -0
- ragbits/evaluate/dataloaders/exceptions.py +25 -0
- ragbits/evaluate/dataloaders/gaia.py +78 -0
- ragbits/evaluate/dataloaders/hotpot_qa.py +95 -0
- ragbits/evaluate/dataloaders/human_eval.py +70 -0
- ragbits/evaluate/dataloaders/question_answer.py +56 -0
- ragbits/evaluate/dataset_generator/pipeline.py +4 -4
- ragbits/evaluate/dataset_generator/prompts/qa.py +2 -4
- ragbits/evaluate/dataset_generator/tasks/corpus_generation.py +2 -4
- ragbits/evaluate/dataset_generator/tasks/text_generation/base.py +3 -5
- ragbits/evaluate/dataset_generator/tasks/text_generation/qa.py +3 -3
- ragbits/evaluate/evaluator.py +178 -50
- ragbits/evaluate/factories/__init__.py +42 -0
- ragbits/evaluate/metrics/__init__.py +2 -23
- ragbits/evaluate/metrics/base.py +40 -17
- ragbits/evaluate/metrics/document_search.py +40 -23
- ragbits/evaluate/metrics/gaia.py +84 -0
- ragbits/evaluate/metrics/hotpot_qa.py +51 -0
- ragbits/evaluate/metrics/human_eval.py +105 -0
- ragbits/evaluate/metrics/question_answer.py +222 -0
- ragbits/evaluate/optimizer.py +138 -86
- ragbits/evaluate/pipelines/__init__.py +37 -0
- ragbits/evaluate/pipelines/base.py +34 -10
- ragbits/evaluate/pipelines/document_search.py +72 -67
- ragbits/evaluate/pipelines/gaia.py +249 -0
- ragbits/evaluate/pipelines/hotpot_qa.py +342 -0
- ragbits/evaluate/pipelines/human_eval.py +323 -0
- ragbits/evaluate/pipelines/question_answer.py +96 -0
- ragbits/evaluate/utils.py +86 -59
- {ragbits_evaluate-0.5.0.dist-info → ragbits_evaluate-1.4.0.dev202602030301.dist-info}/METADATA +33 -9
- ragbits_evaluate-1.4.0.dev202602030301.dist-info/RECORD +59 -0
- {ragbits_evaluate-0.5.0.dist-info → ragbits_evaluate-1.4.0.dev202602030301.dist-info}/WHEEL +1 -1
- ragbits/evaluate/callbacks/base.py +0 -22
- ragbits/evaluate/callbacks/neptune.py +0 -26
- ragbits/evaluate/loaders/__init__.py +0 -21
- ragbits/evaluate/loaders/base.py +0 -24
- ragbits/evaluate/loaders/hf.py +0 -25
- ragbits_evaluate-0.5.0.dist-info/RECORD +0 -33
- /ragbits/evaluate/{callbacks/__init__.py → py.typed} +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Logging functionality for agent simulation scenarios."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
from zoneinfo import ZoneInfo
|
|
7
|
+
|
|
8
|
+
from ragbits.agents.tool import ToolCallResult
|
|
9
|
+
from ragbits.core.llms import Usage
|
|
10
|
+
from ragbits.evaluate.agent_simulation.models import Personality, Scenario, Task
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConversationLogger:
|
|
14
|
+
"""Handles logging of conversation sessions to a file."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, log_file: str | None) -> None:
|
|
17
|
+
"""Initialize logger with optional log file path."""
|
|
18
|
+
self.log_path: Path | None = None
|
|
19
|
+
if log_file:
|
|
20
|
+
self.log_path = Path(log_file)
|
|
21
|
+
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
|
|
23
|
+
def initialize_session(
|
|
24
|
+
self,
|
|
25
|
+
scenario: Scenario,
|
|
26
|
+
agent_model_name: str | None,
|
|
27
|
+
sim_user_model_name: str | None,
|
|
28
|
+
checker_model_name: str | None,
|
|
29
|
+
personality: Personality | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Initialize a new logging session with scenario metadata."""
|
|
32
|
+
if not self.log_path:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
now_warsaw = datetime.now(ZoneInfo("Europe/Warsaw"))
|
|
36
|
+
with self.log_path.open("a", encoding="utf-8") as f:
|
|
37
|
+
f.write("\n" + "=" * 80 + "\n")
|
|
38
|
+
f.write(f"Session start: {now_warsaw.isoformat()}\n")
|
|
39
|
+
f.write(f"Scenario: {scenario.name}\n")
|
|
40
|
+
f.write(f"Tasks: {len(scenario.tasks)}\n")
|
|
41
|
+
for i, task in enumerate(scenario.tasks, 1):
|
|
42
|
+
f.write(f" Task {i}: {task.task}\n")
|
|
43
|
+
f.write(f" Expected: {task.expected_result}\n")
|
|
44
|
+
f.write(f"Agent model: {agent_model_name or 'default'}\n")
|
|
45
|
+
f.write(f"Simulated user model: {sim_user_model_name or 'default'}\n")
|
|
46
|
+
f.write(f"Goal checker model: {checker_model_name or 'default'}\n")
|
|
47
|
+
if personality:
|
|
48
|
+
f.write(f"Personality: {personality.name}\n")
|
|
49
|
+
f.write(f"Personality description: {personality.description}\n")
|
|
50
|
+
else:
|
|
51
|
+
f.write("Personality: none (default)\n")
|
|
52
|
+
|
|
53
|
+
def log_turn(
|
|
54
|
+
self,
|
|
55
|
+
turn_idx: int,
|
|
56
|
+
task: Task | None,
|
|
57
|
+
user_msg: str,
|
|
58
|
+
assistant_msg: str | None = None,
|
|
59
|
+
tool_calls: list[ToolCallResult] | None = None,
|
|
60
|
+
usage: Usage | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Log a conversation turn to the log file."""
|
|
63
|
+
if not self.log_path:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
with self.log_path.open("a", encoding="utf-8") as f:
|
|
67
|
+
if task:
|
|
68
|
+
f.write(f"Turn {turn_idx} - Task: {task.task}\n")
|
|
69
|
+
f.write(f"Turn {turn_idx} - User: {user_msg}\n")
|
|
70
|
+
if assistant_msg:
|
|
71
|
+
f.write(f"Turn {turn_idx} - Assistant: {assistant_msg}\n")
|
|
72
|
+
if tool_calls:
|
|
73
|
+
for tool_call in tool_calls:
|
|
74
|
+
f.write(f"Turn {turn_idx} - Tool: {tool_call.name}({tool_call.arguments})\n")
|
|
75
|
+
if usage:
|
|
76
|
+
f.write(
|
|
77
|
+
f"Turn {turn_idx} - Assistant token usage: {usage.total_tokens} total "
|
|
78
|
+
f"({usage.prompt_tokens} prompt + {usage.completion_tokens} completion), "
|
|
79
|
+
f"estimated cost: ${usage.estimated_cost:.6f}\n"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def log_task_check(self, turn_idx: int, task_done: bool, reason: str) -> None:
|
|
83
|
+
"""Log task completion check result."""
|
|
84
|
+
if self.log_path:
|
|
85
|
+
with self.log_path.open("a", encoding="utf-8") as f:
|
|
86
|
+
f.write(f"Turn {turn_idx} - Task check: done={task_done} reason={reason}\n")
|
|
87
|
+
|
|
88
|
+
def log_task_transition(self, next_task: Task) -> None:
|
|
89
|
+
"""Log transition to next task."""
|
|
90
|
+
if self.log_path:
|
|
91
|
+
with self.log_path.open("a", encoding="utf-8") as f:
|
|
92
|
+
f.write(f"Moving to next task: {next_task.task}\n")
|
|
93
|
+
|
|
94
|
+
def log_tool_check(
|
|
95
|
+
self,
|
|
96
|
+
turn_idx: int,
|
|
97
|
+
tools_used_correctly: bool,
|
|
98
|
+
reason: str,
|
|
99
|
+
tool_calls: list[ToolCallResult],
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Log tool usage check result."""
|
|
102
|
+
if self.log_path:
|
|
103
|
+
with self.log_path.open("a", encoding="utf-8") as f:
|
|
104
|
+
tool_names = [tc.name for tc in tool_calls] if tool_calls else []
|
|
105
|
+
f.write(
|
|
106
|
+
f"Turn {turn_idx} - Tool check: appropriate={tools_used_correctly} "
|
|
107
|
+
f"tools_called={tool_names} reason={reason}\n"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def log_total_usage(self, usage: Usage) -> None:
|
|
111
|
+
"""Log total assistant token usage for the entire conversation."""
|
|
112
|
+
if not self.log_path:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
with self.log_path.open("a", encoding="utf-8") as f:
|
|
116
|
+
f.write("\n--- Total Assistant Token Usage ---\n")
|
|
117
|
+
f.write(
|
|
118
|
+
f"Total assistant tokens: {usage.total_tokens} "
|
|
119
|
+
f"({usage.prompt_tokens} prompt + {usage.completion_tokens} completion)\n"
|
|
120
|
+
)
|
|
121
|
+
f.write(f"Total estimated cost: ${usage.estimated_cost:.6f}\n")
|
|
122
|
+
f.write("--- End Total Assistant Token Usage ---\n")
|
|
123
|
+
|
|
124
|
+
def log_deepeval_metrics(self, metrics: dict[str, Any] | dict[str, dict[str, float | str | None]]) -> None:
|
|
125
|
+
"""Log DeepEval evaluation metrics to the log file.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
metrics: Dictionary of metric names to their evaluation results, or error dict
|
|
129
|
+
"""
|
|
130
|
+
if not self.log_path:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
with self.log_path.open("a", encoding="utf-8") as f:
|
|
134
|
+
f.write("\n--- DeepEval Evaluation Metrics ---\n")
|
|
135
|
+
if "error" in metrics and isinstance(metrics["error"], str):
|
|
136
|
+
# Handle error case
|
|
137
|
+
f.write(f"DeepEval Error: {metrics['error']}\n")
|
|
138
|
+
else:
|
|
139
|
+
# Handle normal metrics case
|
|
140
|
+
for metric_name, result in metrics.items():
|
|
141
|
+
if not isinstance(result, dict):
|
|
142
|
+
continue
|
|
143
|
+
score = result.get("score")
|
|
144
|
+
reason = result.get("reason")
|
|
145
|
+
success = result.get("success")
|
|
146
|
+
error = result.get("error")
|
|
147
|
+
|
|
148
|
+
f.write(f"Metric: {metric_name}\n")
|
|
149
|
+
if error:
|
|
150
|
+
f.write(f" Error: {error}\n")
|
|
151
|
+
else:
|
|
152
|
+
if score is not None:
|
|
153
|
+
f.write(f" Score: {score:.4f}\n")
|
|
154
|
+
if success is not None:
|
|
155
|
+
f.write(f" Success: {success}\n")
|
|
156
|
+
if reason:
|
|
157
|
+
f.write(f" Reason: {reason}\n")
|
|
158
|
+
f.write("--- End DeepEval Metrics ---\n")
|
|
159
|
+
|
|
160
|
+
def finalize_session(self) -> None:
|
|
161
|
+
"""Finalize the logging session."""
|
|
162
|
+
if self.log_path:
|
|
163
|
+
with self.log_path.open("a", encoding="utf-8") as f:
|
|
164
|
+
end_time = datetime.now(ZoneInfo("Europe/Warsaw")).isoformat()
|
|
165
|
+
f.write(f"Session end: {end_time}\n")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Metrics collection components for agent simulation."""
|
|
2
|
+
|
|
3
|
+
from ragbits.evaluate.agent_simulation.metrics.builtin import (
|
|
4
|
+
LatencyMetricCollector,
|
|
5
|
+
TokenUsageMetricCollector,
|
|
6
|
+
ToolUsageMetricCollector,
|
|
7
|
+
)
|
|
8
|
+
from ragbits.evaluate.agent_simulation.metrics.collectors import (
|
|
9
|
+
CompositeMetricCollector,
|
|
10
|
+
MetricCollector,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CompositeMetricCollector",
|
|
15
|
+
"LatencyMetricCollector",
|
|
16
|
+
"MetricCollector",
|
|
17
|
+
"TokenUsageMetricCollector",
|
|
18
|
+
"ToolUsageMetricCollector",
|
|
19
|
+
]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Built-in metric collectors for common simulation metrics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ragbits.evaluate.agent_simulation.results import TurnResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LatencyMetricCollector:
|
|
13
|
+
"""Tracks response latency per turn.
|
|
14
|
+
|
|
15
|
+
Measures the wall-clock time from turn start to turn end,
|
|
16
|
+
providing average, min, max, and per-turn latency metrics.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
>>> collector = LatencyMetricCollector()
|
|
20
|
+
>>> result = await run_simulation(
|
|
21
|
+
... scenario=scenario,
|
|
22
|
+
... chat=chat,
|
|
23
|
+
... metric_collectors=[collector],
|
|
24
|
+
... )
|
|
25
|
+
>>> print(result.metrics.custom["latency_avg_ms"])
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
"""Initialize the latency collector."""
|
|
30
|
+
self._turn_start: float | None = None
|
|
31
|
+
self._latencies: list[float] = []
|
|
32
|
+
|
|
33
|
+
def on_turn_start(self, turn_index: int, task_index: int, user_message: str) -> None:
|
|
34
|
+
"""Record the start time for this turn.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
turn_index: 1-based index of the current turn.
|
|
38
|
+
task_index: 0-based index of the current task.
|
|
39
|
+
user_message: The user message (unused).
|
|
40
|
+
"""
|
|
41
|
+
self._turn_start = time.perf_counter()
|
|
42
|
+
|
|
43
|
+
def on_turn_end(self, turn_result: TurnResult) -> None:
|
|
44
|
+
"""Calculate and store the latency for this turn.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
turn_result: The result of the completed turn (unused directly).
|
|
48
|
+
"""
|
|
49
|
+
if self._turn_start is not None:
|
|
50
|
+
latency_ms = (time.perf_counter() - self._turn_start) * 1000
|
|
51
|
+
self._latencies.append(latency_ms)
|
|
52
|
+
self._turn_start = None
|
|
53
|
+
|
|
54
|
+
def on_conversation_end(self, all_turns: list[TurnResult]) -> dict[str, Any]:
|
|
55
|
+
"""Return latency metrics.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
all_turns: List of all turn results (unused).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary with latency_avg_ms, latency_max_ms, latency_min_ms,
|
|
62
|
+
and latency_per_turn_ms.
|
|
63
|
+
"""
|
|
64
|
+
if not self._latencies:
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"latency_avg_ms": sum(self._latencies) / len(self._latencies),
|
|
69
|
+
"latency_max_ms": max(self._latencies),
|
|
70
|
+
"latency_min_ms": min(self._latencies),
|
|
71
|
+
"latency_per_turn_ms": self._latencies.copy(),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def reset(self) -> None:
|
|
75
|
+
"""Reset collector state for a new conversation."""
|
|
76
|
+
self._turn_start = None
|
|
77
|
+
self._latencies = []
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TokenUsageMetricCollector:
|
|
81
|
+
"""Tracks token usage and estimated cost per turn.
|
|
82
|
+
|
|
83
|
+
Aggregates token counts from each turn to provide total and
|
|
84
|
+
per-turn token usage statistics.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> collector = TokenUsageMetricCollector()
|
|
88
|
+
>>> result = await run_simulation(
|
|
89
|
+
... scenario=scenario,
|
|
90
|
+
... chat=chat,
|
|
91
|
+
... metric_collectors=[collector],
|
|
92
|
+
... )
|
|
93
|
+
>>> print(result.metrics.custom["tokens_total"])
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self) -> None:
|
|
97
|
+
"""Initialize the token usage collector."""
|
|
98
|
+
self._turn_tokens: list[dict[str, int]] = []
|
|
99
|
+
|
|
100
|
+
def on_turn_start(self, turn_index: int, task_index: int, user_message: str) -> None:
|
|
101
|
+
"""No-op for token collector.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
turn_index: 1-based index of the current turn.
|
|
105
|
+
task_index: 0-based index of the current task.
|
|
106
|
+
user_message: The user message (unused).
|
|
107
|
+
"""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def on_turn_end(self, turn_result: TurnResult) -> None:
|
|
111
|
+
"""Record token usage from the turn result.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
turn_result: The result of the completed turn.
|
|
115
|
+
"""
|
|
116
|
+
if turn_result.token_usage:
|
|
117
|
+
self._turn_tokens.append(turn_result.token_usage.copy())
|
|
118
|
+
else:
|
|
119
|
+
self._turn_tokens.append({"total": 0, "prompt": 0, "completion": 0})
|
|
120
|
+
|
|
121
|
+
def on_conversation_end(self, all_turns: list[TurnResult]) -> dict[str, Any]:
|
|
122
|
+
"""Return aggregated token usage metrics.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
all_turns: List of all turn results (unused).
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dictionary with tokens_total, tokens_prompt, tokens_completion,
|
|
129
|
+
tokens_avg_per_turn, and tokens_per_turn.
|
|
130
|
+
"""
|
|
131
|
+
if not self._turn_tokens:
|
|
132
|
+
return {}
|
|
133
|
+
|
|
134
|
+
total = sum(t.get("total", 0) for t in self._turn_tokens)
|
|
135
|
+
prompt = sum(t.get("prompt", 0) for t in self._turn_tokens)
|
|
136
|
+
completion = sum(t.get("completion", 0) for t in self._turn_tokens)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"tokens_total": total,
|
|
140
|
+
"tokens_prompt": prompt,
|
|
141
|
+
"tokens_completion": completion,
|
|
142
|
+
"tokens_avg_per_turn": total / len(self._turn_tokens) if self._turn_tokens else 0,
|
|
143
|
+
"tokens_per_turn": [t.get("total", 0) for t in self._turn_tokens],
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def reset(self) -> None:
|
|
147
|
+
"""Reset collector state for a new conversation."""
|
|
148
|
+
self._turn_tokens = []
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ToolUsageMetricCollector:
|
|
152
|
+
"""Tracks tool call patterns during the conversation.
|
|
153
|
+
|
|
154
|
+
Records which tools were called, how often, and on which turns,
|
|
155
|
+
providing insights into agent behavior.
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
>>> collector = ToolUsageMetricCollector()
|
|
159
|
+
>>> result = await run_simulation(
|
|
160
|
+
... scenario=scenario,
|
|
161
|
+
... chat=chat,
|
|
162
|
+
... metric_collectors=[collector],
|
|
163
|
+
... )
|
|
164
|
+
>>> print(result.metrics.custom["tools_unique"])
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self) -> None:
|
|
168
|
+
"""Initialize the tool usage collector."""
|
|
169
|
+
self._tool_calls: list[list[str]] = [] # tool names per turn
|
|
170
|
+
self._tool_counts: dict[str, int] = {} # total count per tool
|
|
171
|
+
|
|
172
|
+
def on_turn_start(self, turn_index: int, task_index: int, user_message: str) -> None:
|
|
173
|
+
"""No-op for tool usage collector.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
turn_index: 1-based index of the current turn.
|
|
177
|
+
task_index: 0-based index of the current task.
|
|
178
|
+
user_message: The user message (unused).
|
|
179
|
+
"""
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
def on_turn_end(self, turn_result: TurnResult) -> None:
|
|
183
|
+
"""Record tool calls from the turn result.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
turn_result: The result of the completed turn.
|
|
187
|
+
"""
|
|
188
|
+
tool_names: list[str] = []
|
|
189
|
+
if turn_result.tool_calls:
|
|
190
|
+
for tc in turn_result.tool_calls:
|
|
191
|
+
name = tc.get("name", "unknown")
|
|
192
|
+
tool_names.append(name)
|
|
193
|
+
self._tool_counts[name] = self._tool_counts.get(name, 0) + 1
|
|
194
|
+
|
|
195
|
+
self._tool_calls.append(tool_names)
|
|
196
|
+
|
|
197
|
+
def on_conversation_end(self, all_turns: list[TurnResult]) -> dict[str, Any]:
|
|
198
|
+
"""Return tool usage metrics.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
all_turns: List of all turn results (unused).
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Dictionary with tools_total_calls, tools_unique, tools_counts,
|
|
205
|
+
tools_per_turn, and turns_with_tools.
|
|
206
|
+
"""
|
|
207
|
+
total_calls = sum(len(tools) for tools in self._tool_calls)
|
|
208
|
+
turns_with_tools = sum(1 for tools in self._tool_calls if tools)
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"tools_total_calls": total_calls,
|
|
212
|
+
"tools_unique": list(self._tool_counts.keys()),
|
|
213
|
+
"tools_counts": self._tool_counts.copy(),
|
|
214
|
+
"tools_per_turn": self._tool_calls.copy(),
|
|
215
|
+
"turns_with_tools": turns_with_tools,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
def reset(self) -> None:
|
|
219
|
+
"""Reset collector state for a new conversation."""
|
|
220
|
+
self._tool_calls = []
|
|
221
|
+
self._tool_counts = {}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Base protocol and composite collector for metrics collection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ragbits.evaluate.agent_simulation.results import TurnResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class MetricCollector(Protocol):
|
|
13
|
+
"""Protocol for collecting metrics during conversation simulation.
|
|
14
|
+
|
|
15
|
+
Implement this protocol to create custom metric collectors that can
|
|
16
|
+
be passed to run_simulation(). Collectors receive callbacks at various
|
|
17
|
+
points during the simulation lifecycle.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> class CustomCollector:
|
|
21
|
+
... def on_turn_start(self, turn_index: int, task_index: int, user_message: str) -> None:
|
|
22
|
+
... print(f"Turn {turn_index} starting")
|
|
23
|
+
...
|
|
24
|
+
... def on_turn_end(self, turn_result: TurnResult) -> None:
|
|
25
|
+
... print(f"Turn completed: {turn_result.task_completed}")
|
|
26
|
+
...
|
|
27
|
+
... def on_conversation_end(self, all_turns: list[TurnResult]) -> dict[str, Any]:
|
|
28
|
+
... return {"total_turns_tracked": len(all_turns)}
|
|
29
|
+
...
|
|
30
|
+
... def reset(self) -> None:
|
|
31
|
+
... pass
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def on_turn_start(self, turn_index: int, task_index: int, user_message: str) -> None:
|
|
35
|
+
"""Called before agent processes a turn.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
turn_index: 1-based index of the current turn.
|
|
39
|
+
task_index: 0-based index of the current task.
|
|
40
|
+
user_message: The user message being sent to the agent.
|
|
41
|
+
"""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
def on_turn_end(self, turn_result: TurnResult) -> None:
|
|
45
|
+
"""Called after a turn completes.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
turn_result: The result of the completed turn.
|
|
49
|
+
"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def on_conversation_end(self, all_turns: list[TurnResult]) -> dict[str, Any]:
|
|
53
|
+
"""Called when the conversation ends, returns computed metrics.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
all_turns: List of all turn results from the conversation.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dictionary of metric names to values.
|
|
60
|
+
"""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
def reset(self) -> None:
|
|
64
|
+
"""Reset collector state for a new conversation."""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CompositeMetricCollector:
|
|
69
|
+
"""Combines multiple metric collectors into a single interface.
|
|
70
|
+
|
|
71
|
+
This collector delegates all method calls to its child collectors,
|
|
72
|
+
aggregating their results at the end of the conversation.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> from ragbits.evaluate.agent_simulation.metrics import (
|
|
76
|
+
... LatencyMetricCollector,
|
|
77
|
+
... TokenUsageMetricCollector,
|
|
78
|
+
... CompositeMetricCollector,
|
|
79
|
+
... )
|
|
80
|
+
>>> composite = CompositeMetricCollector(
|
|
81
|
+
... [
|
|
82
|
+
... LatencyMetricCollector(),
|
|
83
|
+
... TokenUsageMetricCollector(),
|
|
84
|
+
... ]
|
|
85
|
+
... )
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, collectors: list[MetricCollector] | None = None) -> None:
|
|
89
|
+
"""Initialize with a list of metric collectors.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
collectors: List of collectors to combine. Defaults to empty list.
|
|
93
|
+
"""
|
|
94
|
+
self._collectors: list[MetricCollector] = collectors or []
|
|
95
|
+
|
|
96
|
+
def add(self, collector: MetricCollector) -> None:
|
|
97
|
+
"""Add a collector to the composite.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
collector: Collector to add.
|
|
101
|
+
"""
|
|
102
|
+
self._collectors.append(collector)
|
|
103
|
+
|
|
104
|
+
def on_turn_start(self, turn_index: int, task_index: int, user_message: str) -> None:
|
|
105
|
+
"""Delegate to all child collectors.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
turn_index: 1-based index of the current turn.
|
|
109
|
+
task_index: 0-based index of the current task.
|
|
110
|
+
user_message: The user message being sent to the agent.
|
|
111
|
+
"""
|
|
112
|
+
for collector in self._collectors:
|
|
113
|
+
collector.on_turn_start(turn_index, task_index, user_message)
|
|
114
|
+
|
|
115
|
+
def on_turn_end(self, turn_result: TurnResult) -> None:
|
|
116
|
+
"""Delegate to all child collectors.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
turn_result: The result of the completed turn.
|
|
120
|
+
"""
|
|
121
|
+
for collector in self._collectors:
|
|
122
|
+
collector.on_turn_end(turn_result)
|
|
123
|
+
|
|
124
|
+
def on_conversation_end(self, all_turns: list[TurnResult]) -> dict[str, Any]:
|
|
125
|
+
"""Aggregate metrics from all child collectors.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
all_turns: List of all turn results from the conversation.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dictionary combining all collector metrics.
|
|
132
|
+
"""
|
|
133
|
+
combined: dict[str, Any] = {}
|
|
134
|
+
for collector in self._collectors:
|
|
135
|
+
metrics = collector.on_conversation_end(all_turns)
|
|
136
|
+
combined.update(metrics)
|
|
137
|
+
return combined
|
|
138
|
+
|
|
139
|
+
def reset(self) -> None:
|
|
140
|
+
"""Reset all child collectors."""
|
|
141
|
+
for collector in self._collectors:
|
|
142
|
+
collector.reset()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Data models for agent simulation scenarios."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Turn:
|
|
8
|
+
"""A single conversation turn between user and assistant."""
|
|
9
|
+
|
|
10
|
+
user: str
|
|
11
|
+
assistant: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Task:
|
|
16
|
+
"""A single task with its expected result."""
|
|
17
|
+
|
|
18
|
+
task: str
|
|
19
|
+
expected_result: str
|
|
20
|
+
expected_tools: list[str] | None = None
|
|
21
|
+
"""Optional list of tool names that should be used to complete this task."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Scenario:
|
|
26
|
+
"""A scenario containing multiple tasks to be completed sequentially."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
tasks: list[Task]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Personality:
|
|
34
|
+
"""A personality definition for the simulated user."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
description: str
|