Coverage for src/lexigram/ai/evaluation/evaluators/trajectory.py: 30%
56 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 01:17 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 01:17 +0800
1"""Agent trajectory evaluator."""
3from __future__ import annotations
5from typing import Any
7from lexigram.ai.evaluation.evaluators.base import BaseEvaluator
8from lexigram.contracts.ai.evaluation import (
9 EvaluationResult,
10 EvaluationScoreType,
11 EvaluatorProtocol,
12)
13from lexigram.contracts.ai.exceptions import EvaluationError
14from lexigram.logging import get_logger
15from lexigram.result import Ok, Result
17logger = get_logger(__name__)
20class TrajectoryEvaluator(BaseEvaluator, EvaluatorProtocol):
21 """Agent trajectory fidelity evaluation.
23 Evaluates whether an agent's execution trajectory follows the
24 expected path and reaches the expected final state.
25 """
27 def __init__(self) -> None:
28 super().__init__(EvaluationScoreType.TRAJECTORY_FIDELITY)
30 @property
31 def name(self) -> str:
32 return "trajectory"
34 async def evaluate(
35 self,
36 input: str,
37 output: str,
38 reference: str,
39 ) -> Result[EvaluationResult, Exception]:
40 details: dict[str, Any] = {}
42 try:
43 from lexigram.serialization import loads as json_loads
45 output_trajectory = json_loads(output)
46 reference_trajectory = json_loads(reference)
47 except (ValueError, EvaluationError) as e:
48 return Ok(
49 self._create_result(
50 0.0,
51 "Invalid trajectory format: expected JSON",
52 {"error": "invalid_json"},
53 )
54 )
56 steps_score = self._evaluate_steps(
57 output_trajectory.get("steps", []),
58 reference_trajectory.get("steps", []),
59 )
60 final_state_score = self._evaluate_final_state(
61 output_trajectory.get("final_state", {}),
62 reference_trajectory.get("final_state", {}),
63 )
65 score = (steps_score + final_state_score) / 2.0
67 details = {
68 "steps_score": steps_score,
69 "final_state_score": final_state_score,
70 "output_steps": len(output_trajectory.get("steps", [])),
71 "reference_steps": len(reference_trajectory.get("steps", [])),
72 }
74 feedback = f"Trajectory fidelity: {score:.2f}"
76 return Ok(self._create_result(score, feedback, details))
78 def _evaluate_steps(
79 self,
80 output_steps: list[dict[str, Any]],
81 reference_steps: list[dict[str, Any]],
82 ) -> float:
83 if not reference_steps:
84 return 1.0 if output_steps else 0.0
86 correct = 0
87 for i, ref_step in enumerate(reference_steps):
88 if i < len(output_steps):
89 out_step = output_steps[i]
90 if self._step_matches(out_step, ref_step):
91 correct += 1
93 return correct / len(reference_steps)
95 def _step_matches(
96 self,
97 output_step: dict[str, Any],
98 reference_step: dict[str, Any],
99 ) -> bool:
100 action_match = output_step.get("action") == reference_step.get("action")
101 if not action_match:
102 return False
104 if "tool" in reference_step:
105 return output_step.get("tool") == reference_step.get("tool")
107 return True
109 def _evaluate_final_state(
110 self,
111 output_state: dict[str, Any],
112 reference_state: dict[str, Any],
113 ) -> float:
114 if not reference_state:
115 return 1.0
117 matches = 0
118 total = len(reference_state)
120 for key, expected_value in reference_state.items():
121 if key in output_state:
122 if output_state[key] == expected_value:
123 matches += 1
125 return matches / total if total > 0 else 0.0
128__all__ = ["TrajectoryEvaluator"]