railtracks.evaluations

 1from .evaluators import JudgeEvaluator, LLMInferenceEvaluator, ToolUseEvaluator, metrics
 2from .point import extract_agent_data_points
 3from .runners._evaluate import evaluate
 4
 5__all__ = [
 6    "metrics",
 7    "evaluate",
 8    "extract_agent_data_points",
 9    "JudgeEvaluator",
10    "ToolUseEvaluator",
11    "LLMInferenceEvaluator",
12]
def evaluate( data: railtracks.evaluations.point.AgentDataPoint | list[railtracks.evaluations.point.AgentDataPoint], evaluators: list[railtracks.evaluations.evaluators.evaluator.Evaluator], agent_selection: bool = True, agents: list[str] | None = None, name: str | None = None, payload_callback: Optional[Callable[[dict[str, Any]], Any]] = None):
137def evaluate(
138    data: AgentDataPoint | list[AgentDataPoint],
139    evaluators: list[Evaluator],
140    agent_selection: bool = True,
141    agents: list[str] | None = None,
142    name: str | None = None,
143    payload_callback: Callable[[dict[str, Any]], Any] | None = None,
144):
145    """Evaluate agent data using the provided evaluators.
146
147    Args:
148        data: The agent data to be evaluated. Can be a single AgentDataPoint, a list of AgentDataPoints, or an EvaluationDataset.
149        evaluators: A list of Evaluator instances to run on the data.
150        agent_selection: If True and multiple agents are found in the data, prompts the user to select which agents to evaluate.
151                         If False, evaluates all agents without prompting.
152        agents: An optional list of agent names to evaluate. If provided, only these agents will be evaluated. Overrides agent_selection if both are provided.
153        name: An optional name for the evaluation, which will be included in the EvaluationResult.
154        payload_callback: An optional callback function that will be called with the evaluation results payload after evaluation is complete. Can be used for custom logging, notifications, etc.
155    Returns:
156        A list of EvaluationResult instances containing the results from each evaluator.
157    """
158    _check_evaluators(evaluators)
159
160    data_dict, agents = _setup_agent_data(data, agent_selection, agents)
161
162    evaluation_results: list[EvaluationResult] = []
163
164    for agent_name in agents:
165        logger.info(
166            f"Evaluation for {agent_name} with {len(data_dict[agent_name])} data points CREATED"
167        )
168
169        evaluator_results: list[EvaluatorResult] = []
170
171        start_time = datetime.now(timezone.utc)
172        for evaluator in evaluators:
173            logger.info(f"Evaluator: {evaluator.__class__.__name__} CREATED")
174            try:
175                result = evaluator.run(data_dict[agent_name])
176            except Exception as e:
177                logger.error(f"Evaluator {evaluator.__class__.__name__} FAILED: {e}")
178                continue
179
180            evaluator_results.append(result)
181            logger.info(f"Evaluator: {evaluator.__class__.__name__} DONE")
182
183        logger.info(f"Evaluation for {agent_name} DONE.")
184
185        metrics_map = {}
186        for er in evaluator_results:
187            metrics = er.metrics
188            for metric in metrics:
189                metrics_map[metric.identifier] = metric
190
191        end_time = datetime.now(timezone.utc)
192
193        evaluation_results.append(
194            EvaluationResult(
195                evaluation_name=name or None,
196                created_at=start_time,
197                completed_at=end_time,
198                agents=[
199                    {
200                        "agent_name": agent_name,
201                        "agent_node_ids": [
202                            {
203                                "session_id": adp.session_id,
204                                "agent_node_id": adp.identifier,
205                            }
206                            for adp in data_dict[agent_name]
207                        ],
208                    }
209                ],
210                metrics_map=metrics_map,
211                evaluator_results=evaluator_results,
212            )
213        )
214
215    logger.info("Evaluation DONE.")
216
217    if payload_callback is not None:
218        try:
219            for result in evaluation_results:
220                payload_callback(payload(result))
221        except Exception as e:
222            logger.error(f"Failed to execute payload callback: {e}")
223
224    try:
225        save(evaluation_results)
226    except Exception as e:
227        logger.error(f"Failed to save evaluation results: {e}")
228    return evaluation_results

Evaluate agent data using the provided evaluators.

Arguments:
  • data: The agent data to be evaluated. Can be a single AgentDataPoint, a list of AgentDataPoints, or an EvaluationDataset.
  • evaluators: A list of Evaluator instances to run on the data.
  • agent_selection: If True and multiple agents are found in the data, prompts the user to select which agents to evaluate. If False, evaluates all agents without prompting.
  • agents: An optional list of agent names to evaluate. If provided, only these agents will be evaluated. Overrides agent_selection if both are provided.
  • name: An optional name for the evaluation, which will be included in the EvaluationResult.
  • payload_callback: An optional callback function that will be called with the evaluation results payload after evaluation is complete. Can be used for custom logging, notifications, etc.
Returns:

A list of EvaluationResult instances containing the results from each evaluator.

def extract_agent_data_points( sources: list[str] | str | list[dict]) -> list[railtracks.evaluations.point.AgentDataPoint]:
401def extract_agent_data_points(
402    sources: list[str] | str | list[dict],
403) -> list[AgentDataPoint]:
404    """Extract AgentDataPoint instances from session payloads or session JSON files.
405
406    Args:
407        sources: One of:
408            - list[dict]: in-memory session payloads (e.g. from railtownai.get_agent_runs).
409            - list[str]: file paths to session JSON files.
410            - str: a directory path; all files inside are loaded.
411            A single file or single payload must be wrapped in a list. Mixed lists of
412            files and payloads are not supported.
413
414    Returns:
415        List of AgentDataPoint instances, one per agent execution found across all
416        provided payloads. Returns an empty list if no valid agent data is found.
417    """
418    if isinstance(sources, list) and sources and isinstance(sources[0], dict):
419        payloads: list[dict] = cast(list[dict], sources)
420    else:
421        file_sources = cast("list[str] | str", sources)
422        payloads = []
423        for file_path in resolve_file_paths(file_sources):
424            try:
425                payloads.append(load_session(file_path))
426            except (FileNotFoundError, ValueError) as e:
427                logger.error(str(e))
428
429    data_points: list[AgentDataPoint] = []
430    for payload in payloads:
431        data_points.extend(_data_points_from_payload(payload))
432    return data_points

Extract AgentDataPoint instances from session payloads or session JSON files.

Arguments:
  • sources: One of:
    • list[dict]: in-memory session payloads (e.g. from railtownai.get_agent_runs).
    • list[str]: file paths to session JSON files.
    • str: a directory path; all files inside are loaded. A single file or single payload must be wrapped in a list. Mixed lists of files and payloads are not supported.
Returns:

List of AgentDataPoint instances, one per agent execution found across all provided payloads. Returns an empty list if no valid agent data is found.

class JudgeEvaluator(railtracks.evaluations.evaluators.evaluator.Evaluator):
 37class JudgeEvaluator(Evaluator):
 38    def __init__(
 39        self,
 40        llm: rt.llm.ModelBase,
 41        metrics: list[Metric],
 42        system_prompt: str | None = None,
 43        timeout: float | None = None,
 44        reasoning: bool = True,
 45    ):
 46        """
 47        The JudgeEvaluator with a system prompt, LLM, metric, and reasoning flag.
 48
 49        Args:
 50            system_prompt: The system prompt template for the judge LLM.
 51            llm: The LLM model to be used as the judge.
 52            metrics: A list of Metrics to guide the evaluation.
 53            reasoning: A flag indicating whether the judge should provide reasoning for its evaluations.
 54        """
 55        # These are config not state
 56        self._metrics: dict[str, Metric] = {}
 57        for m in metrics:
 58            # LLMMetric/ToolMetric subclass Numerical but are usage-stat metrics,
 59            # not meant to be scored by the LLM judge.
 60            if isinstance(m, (LLMMetric, ToolMetric)) or not isinstance(
 61                m, (Categorical, Numerical)
 62            ):
 63                logger.warning(
 64                    f"JudgeEvaluator currently only supports Categorical and Numerical metrics, metric {m.name} of type {type(m)} will be skipped."
 65                )
 66            else:
 67                self._metrics[m.identifier] = m
 68        self._llm = llm
 69        self._reasoning: bool = reasoning
 70        self._template = self._load_yaml()
 71        self._system_prompt = (
 72            system_prompt
 73            if system_prompt is not None
 74            else self._template["system_prompt"]
 75        )
 76        super().__init__()
 77
 78        self.timeout = timeout
 79        self._judge = rt.agent_node(
 80            llm=self._llm,
 81            output_schema=JudgeResponseSchema,
 82        )
 83
 84    def run(
 85        self, data: list[AgentDataPoint]
 86    ) -> EvaluatorResult[Metric, MetricResult, CategoricalAggregateNode]:
 87        judge_outputs: list[JudgeOutput] = self._invoke(data)
 88
 89        self.agent_data_ids = {adp.identifier for adp in data}
 90        results: dict[Metric, list[MetricResult]] = defaultdict(list)
 91        forest = AggregateForest[CategoricalAggregateNode, MetricResult]()
 92
 93        for output in judge_outputs:
 94            metric = self._metrics[output.metric_id]
 95
 96            metric_result = MetricResult(
 97                result_name=f"JudgeResult/{metric.name}",
 98                metric_id=metric.identifier,
 99                agent_data_id=[UUID(output.adp_id)],
100                value=output.response.metric_value,
101            )
102            results[metric].append(metric_result)
103            forest.add_node(metric_result)
104
105            if self._reasoning:
106                reasoning_metric = Metric(name=f"{metric.name}_reasoning")
107                if output.response.reasoning is not None:
108                    results[reasoning_metric].append(
109                        MetricResult(
110                            result_name=f"JudgeReasoning/{metric.name}",
111                            metric_id=reasoning_metric.identifier,
112                            agent_data_id=[UUID(output.adp_id)],
113                            value=output.response.reasoning,
114                        )
115                    )
116                else:
117                    logger.warning(
118                        f"No reasoning returned for Judge Evaluator Metric: {metric.name}, AgentDataPoint ID: {output.adp_id}"
119                    )
120
121        self._aggregate_metrics(results, forest)
122
123        self._result = EvaluatorResult(
124            evaluator_name=self.name,
125            evaluator_id=self.identifier,
126            agent_data_ids=self.agent_data_ids,
127            metric_results=[item for sublist in results.values() for item in sublist],
128            aggregate_results=forest,
129            metrics=list(self._metrics.values()),
130        )
131        return self._result
132
133    def __repr__(self) -> str:
134        return (
135            f"JudgeEvaluator, "
136            f"llm={self._llm}, "
137            f"metrics={list(self._metrics.values())}, "
138            f"reasoning={self._reasoning})"
139        )
140
141    def _invoke(self, data: list[AgentDataPoint]) -> list[JudgeOutput]:
142        @rt.function_node
143        async def judge_flow():
144            output: list[JudgeOutput] = []
145            for metric in self._metrics.values():
146                logger.info(
147                    f"START Evaluating Metric: {metric.name} for {len(data)} AgentDataPoints"
148                )
149
150                for idx, adp in enumerate(data):
151                    user_message = self._generate_user_prompt(adp)
152                    system_message = self._generate_system_prompt(metric)
153                    message_history = rt.llm.MessageHistory(
154                        [
155                            rt.llm.SystemMessage(system_message),
156                            rt.llm.UserMessage(user_message),
157                        ]
158                    )
159                    res = await rt.call(
160                        self._judge,
161                        message_history,
162                    )
163                    output.append(
164                        JudgeOutput(
165                            metric_id=metric.identifier,
166                            adp_id=str(adp.identifier),
167                            response=res.structured,
168                        )
169                    )
170
171                    logger.info(
172                        f"AgentDataPoint ID: {adp.identifier} {idx + 1}/{len(data)} DONE"
173                    )
174
175            return output
176
177        judge_evaluator_flow = rt.Flow(
178            name="JudgeEvaluatorFlow",
179            entry_point=judge_flow,
180            timeout=self.timeout,
181            save_state=False,
182        )
183
184        return judge_evaluator_flow.invoke()
185
186    def _aggregate_metrics(
187        self,
188        results: dict[Metric, list[MetricResult]],
189        forest: AggregateForest[CategoricalAggregateNode, MetricResult],
190    ) -> None:
191        for metric in results:
192            if isinstance(metric, Numerical):
193                continue
194            elif isinstance(metric, Categorical):
195                aggregate_node = CategoricalAggregateNode(
196                    name=f"Aggregate/{metric.name}",
197                    metric=metric,
198                    children=[val.identifier for val in results[metric]],
199                    forest=forest,
200                )
201
202                forest.roots.append(aggregate_node.identifier)
203                forest.add_node(aggregate_node)
204
205    def _generate_user_prompt(self, data: AgentDataPoint) -> str:
206        return self._template["user"].format(
207            agent_input=data.agent_input,
208            agent_output=data.agent_output.get("message_history", ""),
209        )
210
211    def _generate_system_prompt(self, metric: Metric) -> str:
212        system_prompt: str = self._template["system_prompt"]
213
214        system_prompt += "\n" + self._template["metric"].format(metric=str(metric))
215
216        if isinstance(metric, Categorical):
217            category_names = ", ".join(c for c in metric.category_names)
218            system_prompt += (
219                f"\nYour metric_value must be exactly one of these category "
220                f"names: {category_names}."
221            )
222        elif isinstance(metric, Numerical):
223            system_prompt += f"\n{self._numeric_bounds_text(metric)}"
224            if metric.shots:
225                system_prompt += self._numeric_shots_text(metric)
226
227        if self._reasoning:
228            system_prompt += self._template["reasoning"]
229
230        return system_prompt
231
232    def _numeric_bounds_text(self, metric: Numerical) -> str:
233        if metric.min_value is not None and metric.max_value is not None:
234            return (
235                f"Your metric_value must be a single number between "
236                f"{metric.min_value} and {metric.max_value} inclusive."
237            )
238        if metric.min_value is not None:
239            return (
240                f"Your metric_value must be a single number of at least "
241                f"{metric.min_value}."
242            )
243        if metric.max_value is not None:
244            return (
245                f"Your metric_value must be a single number of at most "
246                f"{metric.max_value}."
247            )
248        return "Your metric_value must be a single number."
249
250    def _numeric_shots_text(self, metric: Numerical) -> str:
251        text = "\nUse the following anchor points to calibrate your scoring:"
252        for value, description in metric.shots or []:
253            text += f"\n- A score of {value} means: {description}"
254        text += (
255            "\nFor scores between the provided anchor points, "
256            "interpolate based on how closely the agent's output "
257            "matches the descriptions of the nearest anchors."
258        )
259        return text
260
261    def _load_yaml(self):
262        yaml_path = Path(__file__).parent / "judge_evaluator.yaml"
263        with open(yaml_path, "r") as f:
264            template = yaml.safe_load(f)
265
266        return template
267
268    def _get_config(self) -> dict:
269        return {
270            "llm": self._llm.model_name(),
271            "llm_provider": self._llm.model_provider(),
272            "system_prompt": self._system_prompt,
273            "metrics": sorted(self._metrics.keys()),
274            "reasoning": self._reasoning,
275        }

Helper class that provides a standard way to create an ABC using inheritance.

JudgeEvaluator( llm: railtracks.llm.ModelBase, metrics: list[railtracks.evaluations.evaluators.metrics.Metric], system_prompt: str | None = None, timeout: float | None = None, reasoning: bool = True)
38    def __init__(
39        self,
40        llm: rt.llm.ModelBase,
41        metrics: list[Metric],
42        system_prompt: str | None = None,
43        timeout: float | None = None,
44        reasoning: bool = True,
45    ):
46        """
47        The JudgeEvaluator with a system prompt, LLM, metric, and reasoning flag.
48
49        Args:
50            system_prompt: The system prompt template for the judge LLM.
51            llm: The LLM model to be used as the judge.
52            metrics: A list of Metrics to guide the evaluation.
53            reasoning: A flag indicating whether the judge should provide reasoning for its evaluations.
54        """
55        # These are config not state
56        self._metrics: dict[str, Metric] = {}
57        for m in metrics:
58            # LLMMetric/ToolMetric subclass Numerical but are usage-stat metrics,
59            # not meant to be scored by the LLM judge.
60            if isinstance(m, (LLMMetric, ToolMetric)) or not isinstance(
61                m, (Categorical, Numerical)
62            ):
63                logger.warning(
64                    f"JudgeEvaluator currently only supports Categorical and Numerical metrics, metric {m.name} of type {type(m)} will be skipped."
65                )
66            else:
67                self._metrics[m.identifier] = m
68        self._llm = llm
69        self._reasoning: bool = reasoning
70        self._template = self._load_yaml()
71        self._system_prompt = (
72            system_prompt
73            if system_prompt is not None
74            else self._template["system_prompt"]
75        )
76        super().__init__()
77
78        self.timeout = timeout
79        self._judge = rt.agent_node(
80            llm=self._llm,
81            output_schema=JudgeResponseSchema,
82        )

The JudgeEvaluator with a system prompt, LLM, metric, and reasoning flag.

Arguments:
  • system_prompt: The system prompt template for the judge LLM.
  • llm: The LLM model to be used as the judge.
  • metrics: A list of Metrics to guide the evaluation.
  • reasoning: A flag indicating whether the judge should provide reasoning for its evaluations.
timeout
def run( self, data: list[railtracks.evaluations.point.AgentDataPoint]) -> railtracks.evaluations.result.evaluator_results.EvaluatorResult[Metric, MetricResult, CategoricalAggregateNode]:
 84    def run(
 85        self, data: list[AgentDataPoint]
 86    ) -> EvaluatorResult[Metric, MetricResult, CategoricalAggregateNode]:
 87        judge_outputs: list[JudgeOutput] = self._invoke(data)
 88
 89        self.agent_data_ids = {adp.identifier for adp in data}
 90        results: dict[Metric, list[MetricResult]] = defaultdict(list)
 91        forest = AggregateForest[CategoricalAggregateNode, MetricResult]()
 92
 93        for output in judge_outputs:
 94            metric = self._metrics[output.metric_id]
 95
 96            metric_result = MetricResult(
 97                result_name=f"JudgeResult/{metric.name}",
 98                metric_id=metric.identifier,
 99                agent_data_id=[UUID(output.adp_id)],
100                value=output.response.metric_value,
101            )
102            results[metric].append(metric_result)
103            forest.add_node(metric_result)
104
105            if self._reasoning:
106                reasoning_metric = Metric(name=f"{metric.name}_reasoning")
107                if output.response.reasoning is not None:
108                    results[reasoning_metric].append(
109                        MetricResult(
110                            result_name=f"JudgeReasoning/{metric.name}",
111                            metric_id=reasoning_metric.identifier,
112                            agent_data_id=[UUID(output.adp_id)],
113                            value=output.response.reasoning,
114                        )
115                    )
116                else:
117                    logger.warning(
118                        f"No reasoning returned for Judge Evaluator Metric: {metric.name}, AgentDataPoint ID: {output.adp_id}"
119                    )
120
121        self._aggregate_metrics(results, forest)
122
123        self._result = EvaluatorResult(
124            evaluator_name=self.name,
125            evaluator_id=self.identifier,
126            agent_data_ids=self.agent_data_ids,
127            metric_results=[item for sublist in results.values() for item in sublist],
128            aggregate_results=forest,
129            metrics=list(self._metrics.values()),
130        )
131        return self._result
class ToolUseEvaluator(railtracks.evaluations.evaluators.evaluator.Evaluator):
 48class ToolUseEvaluator(Evaluator):
 49    """
 50    Evaluator that analyzes tool usage patterns across agent runs.
 51
 52    Computes per-call and aggregated metrics for each tool, including
 53    runtime, failure rate, and usage count.
 54    """
 55
 56    def __init__(
 57        self,
 58    ):
 59        super().__init__()
 60
 61    def run(
 62        self, data: list[AgentDataPoint]
 63    ) -> EvaluatorResult[ToolMetric, ToolMetricResult, ToolAggregateNode]:
 64        """
 65        Run the evaluator over a list of agent data points.
 66
 67        Args:
 68            data: A list of AgentDataPoint instances to evaluate.
 69
 70        Returns:
 71            An EvaluatorResult containing per-call metric results and
 72            aggregated nodes across runs.
 73        """
 74        agent_data_ids: set[UUID] = {adp.identifier for adp in data}
 75        forest = AggregateForest[ToolAggregateNode, ToolMetricResult]()
 76
 77        results = self._extract_tool_stats(data, forest)
 78        self._aggregate_per_run(results, forest)
 79        self._aggregate_across_runs(results, forest)
 80
 81        metrics = list(results.keys())
 82
 83        return EvaluatorResult(
 84            evaluator_name=self.name,
 85            evaluator_id=self.identifier,
 86            agent_data_ids=agent_data_ids,
 87            metrics=metrics,
 88            metric_results=[item for sublist in results.values() for item in sublist],
 89            aggregate_results=forest,
 90        )
 91
 92    def _extract_tool_stats(
 93        self,
 94        data: list[AgentDataPoint],
 95        forest: AggregateForest[ToolAggregateNode, ToolMetricResult],
 96    ) -> dict[ToolMetric, list[ToolMetricResult]]:
 97        """
 98        Retrieve tool usage statistics from the agent data points.
 99        There is no aggregation at this level, so results are at the tool call level.
100
101        Args:
102            data: A list of AgentDataPoint instances.
103        """
104
105        results: dict[ToolMetric, list[ToolMetricResult]] = defaultdict(list)
106        # (agent_datapoint_id, tool_name): stats_dict
107        stats: dict[tuple[UUID, str], ToolStats] = defaultdict(
108            lambda: {"usage_count": 0, "failure_count": 0, "runtimes": []}
109        )
110
111        for datapoint in data:
112            for tool in datapoint.tool_details.calls:
113                tool_name = tool.name
114                key = (datapoint.identifier, tool_name)
115
116                stats[key]["usage_count"] += 1
117
118                failed = tool.status == Status.FAILED
119                if failed:
120                    stats[key]["failure_count"] += 1
121
122                # not added to the forest, nothing aggregates per-call failures
123                results[METRICS["ToolFailure"]].append(
124                    ToolMetricResult(
125                        result_name=f"{METRICS['ToolFailure'].name}/{tool_name}",
126                        agent_data_id=[datapoint.identifier],
127                        metric_id=METRICS["ToolFailure"].identifier,
128                        tool_name=tool_name,
129                        tool_node_id=tool.identifier,
130                        value=1 if failed else 0,
131                    )
132                )
133
134                runtime = tool.runtime
135
136                if runtime is not None:
137                    stats[key]["runtimes"].append(runtime)
138
139                    metric_result = ToolMetricResult(
140                        result_name=f"{METRICS['Runtime'].name}/{tool_name}",
141                        agent_data_id=[datapoint.identifier],
142                        metric_id=METRICS["Runtime"].identifier,
143                        tool_name=tool_name,
144                        tool_node_id=tool.identifier,
145                        value=runtime,
146                    )
147                    forest.add_node(metric_result)
148                    results[METRICS["Runtime"]].append(metric_result)
149
150        for key, tool_data in stats.items():
151            adp_id, tool_name = key
152
153            failure_rate = (
154                tool_data["failure_count"] / tool_data["usage_count"]
155                if tool_data["usage_count"] > 0
156                else 0.0
157            )
158
159            tmr = ToolMetricResult(
160                result_name=f"{METRICS['FailureRate'].name}/{tool_name}",
161                agent_data_id=[adp_id],
162                metric_id=METRICS["FailureRate"].identifier,
163                tool_name=tool_name,
164                tool_node_id=None,
165                value=failure_rate,
166            )
167            forest.add_node(tmr)
168            results[METRICS["FailureRate"]].append(tmr)
169
170            tmr = ToolMetricResult(
171                result_name=f"{METRICS['UsageCount'].name}/{tool_name}",
172                agent_data_id=[adp_id],
173                metric_id=METRICS["UsageCount"].identifier,
174                tool_name=tool_name,
175                tool_node_id=None,
176                value=tool_data["usage_count"],
177            )
178            forest.add_node(tmr)
179            results[METRICS["UsageCount"]].append(tmr)
180
181        return results
182
183    def _aggregate_per_run(
184        self,
185        results: dict[ToolMetric, list[ToolMetricResult]],
186        forest: AggregateForest[ToolAggregateNode, ToolMetricResult],
187    ) -> None:
188        metric_results = results[METRICS["Runtime"]]
189        metric_results_by_adp_id: dict[UUID, list[ToolMetricResult]] = defaultdict(list)
190
191        values: dict[UUID, dict[str, list[ToolMetricResult]]] = defaultdict(dict)
192
193        for result in metric_results:
194            for adp_id in result.agent_data_id:
195                metric_results_by_adp_id[adp_id].append(result)
196
197        for adp_id in metric_results_by_adp_id:
198            values[adp_id] = defaultdict(list)
199
200            for tmr in metric_results_by_adp_id[adp_id]:
201                values[adp_id][tmr.tool_name].append(tmr)
202
203            for tool_name in values[adp_id]:
204                aggregate_node = ToolAggregateNode(
205                    name=f"Aggregate/{METRICS['Runtime'].name}",
206                    metric=METRICS["Runtime"],
207                    tool_name=tool_name,
208                    children=[tmr.identifier for tmr in values[adp_id][tool_name]],
209                    forest=forest,
210                )
211                forest.roots.append(aggregate_node.identifier)
212                forest.add_node(aggregate_node)
213
214    def _aggregate_across_runs(
215        self,
216        results: dict[ToolMetric, list[ToolMetricResult]],
217        forest: AggregateForest[ToolAggregateNode, ToolMetricResult],
218    ) -> None:
219        """
220        Aggregates the ToolUseEvaluator metrics across runs on an agent level.
221        This is a separate step from the initial extraction to allow for more flexible aggregation strategies in the future.
222
223        Args:
224            results: A dictionary of ToolMetric to list of ToolMetricResult at the tool call level.
225
226        Returns:
227            A list of ToolAggregateNode instances containing the aggregated results at the run level.
228        """
229
230        for metric in [METRICS["FailureRate"], METRICS["UsageCount"]]:
231            metric_results = results[metric]
232            values: dict[str, list[ToolMetricResult]] = defaultdict(list)
233
234            for tmr in metric_results:
235                values[tmr.tool_name].append(tmr)
236
237            for tool_name, vals in values.items():
238                aggregate_node = ToolAggregateNode(
239                    name=f"Aggregate/{metric.name}",
240                    metric=metric,
241                    tool_name=tool_name,
242                    children=[val.identifier for val in vals],
243                    forest=forest,
244                )
245                forest.roots.append(aggregate_node.identifier)
246                forest.add_node(aggregate_node)
247
248        ## Aggregation of Runtime ------------------------------
249        tool_breakdown = defaultdict(list)
250        for root_id in forest.roots:
251            agg = forest.get(root_id)
252            if isinstance(agg, ToolMetricResult):
253                raise ValueError(
254                    f"Expected root nodes in the forest to be ToolAggregateNodes, but got {type(agg)}"
255                )
256            if agg.metric == METRICS["Runtime"]:
257                tool_breakdown[agg.tool_name].append(agg)
258
259        for tool_name in tool_breakdown:
260            parent = ToolAggregateNode(
261                name=f"Aggregate/{METRICS['Runtime'].name}",
262                metric=METRICS["Runtime"],
263                tool_name=tool_name,
264                children=[
265                    tool_agg.identifier for tool_agg in tool_breakdown[tool_name]
266                ],
267                forest=forest,
268            )
269            forest.add_node(parent)
270            forest.roots.append(parent.identifier)

Evaluator that analyzes tool usage patterns across agent runs.

Computes per-call and aggregated metrics for each tool, including runtime, failure rate, and usage count.

def run( self, data: list[railtracks.evaluations.point.AgentDataPoint]) -> railtracks.evaluations.result.evaluator_results.EvaluatorResult[ToolMetric, ToolMetricResult, ToolAggregateNode]:
61    def run(
62        self, data: list[AgentDataPoint]
63    ) -> EvaluatorResult[ToolMetric, ToolMetricResult, ToolAggregateNode]:
64        """
65        Run the evaluator over a list of agent data points.
66
67        Args:
68            data: A list of AgentDataPoint instances to evaluate.
69
70        Returns:
71            An EvaluatorResult containing per-call metric results and
72            aggregated nodes across runs.
73        """
74        agent_data_ids: set[UUID] = {adp.identifier for adp in data}
75        forest = AggregateForest[ToolAggregateNode, ToolMetricResult]()
76
77        results = self._extract_tool_stats(data, forest)
78        self._aggregate_per_run(results, forest)
79        self._aggregate_across_runs(results, forest)
80
81        metrics = list(results.keys())
82
83        return EvaluatorResult(
84            evaluator_name=self.name,
85            evaluator_id=self.identifier,
86            agent_data_ids=agent_data_ids,
87            metrics=metrics,
88            metric_results=[item for sublist in results.values() for item in sublist],
89            aggregate_results=forest,
90        )

Run the evaluator over a list of agent data points.

Arguments:
  • data: A list of AgentDataPoint instances to evaluate.
Returns:

An EvaluatorResult containing per-call metric results and aggregated nodes across runs.

class LLMInferenceEvaluator(railtracks.evaluations.evaluators.evaluator.Evaluator):
 19class LLMInferenceEvaluator(Evaluator):
 20    """
 21    Evaluator that analyzes LLM inference statistics across agent runs.
 22
 23    Computes per-call and aggregated metrics for each LLM invocation,
 24    including input/output token counts, token cost, and latency.
 25    """
 26
 27    def __init__(
 28        self,
 29    ):
 30        super().__init__()
 31
 32    def run(
 33        self, data: list[AgentDataPoint]
 34    ) -> EvaluatorResult[LLMMetric, LLMMetricResult, LLMInferenceAggregateNode]:
 35        """
 36        Run the evaluator over a list of agent data points.
 37
 38        Args:
 39            data: A list of AgentDataPoint instances to evaluate.
 40
 41        Returns:
 42            An EvaluatorResult containing per-call metric results and
 43            aggregated nodes grouped by model and call index.
 44        """
 45        agent_data_ids: set[UUID] = {adp.identifier for adp in data}
 46        forest = AggregateForest[LLMInferenceAggregateNode, LLMMetricResult]()
 47
 48        results = self._retrieve_llm_states(data, forest)
 49        self._aggregate_metrics(results, forest)
 50
 51        metrics = list(results.keys())
 52
 53        return EvaluatorResult(
 54            evaluator_name=self.name,
 55            evaluator_id=self.identifier,
 56            agent_data_ids=agent_data_ids,
 57            metrics=metrics,
 58            metric_results=[item for sublist in results.values() for item in sublist],
 59            aggregate_results=forest,
 60        )
 61
 62    def _retrieve_llm_states(
 63        self,
 64        data: list[AgentDataPoint],
 65        forest: AggregateForest[LLMInferenceAggregateNode, LLMMetricResult],
 66    ) -> dict[LLMMetric, list[LLMMetricResult]]:
 67        results: dict[LLMMetric, list[LLMMetricResult]] = defaultdict(list)
 68
 69        for datapoint in data:
 70            llm_details = datapoint.llm_details
 71
 72            for call in llm_details.calls:
 73                # Input Tokens
 74                metric = LLMMetric(
 75                    name="InputTokens",
 76                    min_value=0,
 77                )
 78
 79                metric_result = LLMMetricResult(
 80                    result_name="InputTokens",
 81                    metric_id=metric.identifier,
 82                    agent_data_id=[datapoint.identifier],
 83                    value=call.input_tokens,
 84                    llm_call_index=call.index,
 85                    model_name=call.model_name,
 86                    model_provider=call.model_provider,
 87                )
 88                results[metric].append(metric_result)
 89                forest.add_node(metric_result)
 90
 91                # Output Tokens
 92                metric = LLMMetric(
 93                    name="OutputTokens",
 94                    min_value=0,
 95                )
 96
 97                metric_result = LLMMetricResult(
 98                    result_name="OutputTokens",
 99                    metric_id=metric.identifier,
100                    agent_data_id=[datapoint.identifier],
101                    value=call.output_tokens,
102                    llm_call_index=call.index,
103                    model_name=call.model_name,
104                    model_provider=call.model_provider,
105                )
106                results[metric].append(metric_result)
107                forest.add_node(metric_result)
108
109                # Total Cost
110                metric = LLMMetric(
111                    name="TokenCost",
112                    min_value=0.0,
113                )
114
115                metric_result = LLMMetricResult(
116                    result_name="TokenCost",
117                    metric_id=metric.identifier,
118                    agent_data_id=[datapoint.identifier],
119                    value=call.total_cost,
120                    llm_call_index=call.index,
121                    model_name=call.model_name,
122                    model_provider=call.model_provider,
123                )
124                results[metric].append(metric_result)
125                forest.add_node(metric_result)
126
127                # Latency
128                metric = LLMMetric(
129                    name="Latency",
130                    min_value=0.0,
131                )
132                metric_result = LLMMetricResult(
133                    result_name="Latency",
134                    metric_id=metric.identifier,
135                    agent_data_id=[datapoint.identifier],
136                    value=call.latency,
137                    llm_call_index=call.index,
138                    model_name=call.model_name,
139                    model_provider=call.model_provider,
140                )
141                results[metric].append(metric_result)
142                forest.add_node(metric_result)
143
144        return results
145
146    def _aggregate_metrics(
147        self,
148        results: dict[LLMMetric, list[LLMMetricResult]],
149        forest: AggregateForest[LLMInferenceAggregateNode, LLMMetricResult],
150    ) -> None:
151        for metric in results:
152            metric_results = results[metric]
153            values: dict[tuple[str, str, int], list[LLMMetricResult]] = defaultdict(
154                list
155            )
156            for mr in metric_results:
157                if isinstance(mr.value, (int, float)):
158                    key = (mr.model_name, mr.model_provider, mr.llm_call_index)
159                    values[key].append(mr)
160
161            for (model_name, model_provider, llm_call_index), vals in values.items():
162                aggregate_node = LLMInferenceAggregateNode(
163                    name=f"Aggregate/{metric.name}/{model_name}/{model_provider}/Call_{llm_call_index}",
164                    metric=metric,
165                    children=[val.identifier for val in vals],
166                    model_name=model_name,
167                    model_provider=model_provider,
168                    llm_call_index=llm_call_index,
169                    forest=forest,
170                )
171
172                forest.roots.append(aggregate_node.identifier)
173                forest.add_node(aggregate_node)

Evaluator that analyzes LLM inference statistics across agent runs.

Computes per-call and aggregated metrics for each LLM invocation, including input/output token counts, token cost, and latency.

def run( self, data: list[railtracks.evaluations.point.AgentDataPoint]) -> railtracks.evaluations.result.evaluator_results.EvaluatorResult[LLMMetric, LLMMetricResult, LLMInferenceAggregateNode]:
32    def run(
33        self, data: list[AgentDataPoint]
34    ) -> EvaluatorResult[LLMMetric, LLMMetricResult, LLMInferenceAggregateNode]:
35        """
36        Run the evaluator over a list of agent data points.
37
38        Args:
39            data: A list of AgentDataPoint instances to evaluate.
40
41        Returns:
42            An EvaluatorResult containing per-call metric results and
43            aggregated nodes grouped by model and call index.
44        """
45        agent_data_ids: set[UUID] = {adp.identifier for adp in data}
46        forest = AggregateForest[LLMInferenceAggregateNode, LLMMetricResult]()
47
48        results = self._retrieve_llm_states(data, forest)
49        self._aggregate_metrics(results, forest)
50
51        metrics = list(results.keys())
52
53        return EvaluatorResult(
54            evaluator_name=self.name,
55            evaluator_id=self.identifier,
56            agent_data_ids=agent_data_ids,
57            metrics=metrics,
58            metric_results=[item for sublist in results.values() for item in sublist],
59            aggregate_results=forest,
60        )

Run the evaluator over a list of agent data points.

Arguments:
  • data: A list of AgentDataPoint instances to evaluate.
Returns:

An EvaluatorResult containing per-call metric results and aggregated nodes grouped by model and call index.