1use crate::cost_tracker::CostTracker;
6use crate::criteria::EvaluationCriteria;
7use crate::error::Result;
8use crate::llm_judge::LlmJudge;
9use crate::report::{EvaluationReport, EvaluationResult, Failure, TurnResult};
10use crate::schema::{EvalCase, TestFile, ToolUse, Turn};
11use crate::scoring::{ResponseScorer, ToolTrajectoryScorer};
12use crate::structured_judge::StructuredJudge;
13use crate::trace_analyzer::TraceAnalyzer;
14
15use adk_core::{Agent, Content, Event, Llm};
16use async_trait::async_trait;
17use futures::StreamExt;
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use std::collections::HashMap;
21use std::path::Path;
22use std::sync::Arc;
23use std::time::{Duration, Instant};
24
25#[cfg(feature = "embedding")]
26use crate::embedding_scorer::EmbeddingScorer;
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct EvaluationConfig {
31 #[serde(default)]
33 pub criteria: EvaluationCriteria,
34 #[serde(default)]
36 pub continue_on_failure: bool,
37 #[serde(default)]
39 pub timeout_per_case: Option<Duration>,
40 #[serde(default)]
42 pub retries: usize,
43 #[serde(default = "default_true")]
45 pub collect_turn_details: bool,
46}
47
48fn default_true() -> bool {
49 true
50}
51
52impl EvaluationConfig {
53 pub fn with_criteria(criteria: EvaluationCriteria) -> Self {
55 Self { criteria, ..Default::default() }
56 }
57}
58
59pub struct Evaluator {
61 config: EvaluationConfig,
62 tool_scorer: ToolTrajectoryScorer,
63 response_scorer: ResponseScorer,
64 llm_judge: Option<LlmJudge>,
65 structured_judge: Option<Arc<StructuredJudge>>,
67 cost_tracker: Option<CostTracker>,
69 trace_analyzer: Option<TraceAnalyzer>,
71 #[cfg(feature = "embedding")]
73 embedding_scorer: Option<Arc<EmbeddingScorer>>,
74 conversation_scorer: Option<Arc<crate::conversation_scorer::ConversationScorer>>,
76}
77
78impl Evaluator {
79 pub fn new(config: EvaluationConfig) -> Self {
81 let tool_scorer = if let Some(tc) = &config.criteria.tool_trajectory_config {
82 ToolTrajectoryScorer::with_config(tc.clone())
83 } else {
84 ToolTrajectoryScorer::new()
85 };
86
87 let response_scorer = if let Some(rc) = &config.criteria.response_match_config {
88 ResponseScorer::with_config(rc.clone())
89 } else {
90 ResponseScorer::new()
91 };
92
93 Self {
94 config,
95 tool_scorer,
96 response_scorer,
97 llm_judge: None,
98 structured_judge: None,
99 cost_tracker: None,
100 trace_analyzer: None,
101 #[cfg(feature = "embedding")]
102 embedding_scorer: None,
103 conversation_scorer: None,
104 }
105 }
106
107 pub fn with_llm_judge(config: EvaluationConfig, judge_model: Arc<dyn Llm>) -> Self {
109 let tool_scorer = if let Some(tc) = &config.criteria.tool_trajectory_config {
110 ToolTrajectoryScorer::with_config(tc.clone())
111 } else {
112 ToolTrajectoryScorer::new()
113 };
114
115 let response_scorer = if let Some(rc) = &config.criteria.response_match_config {
116 ResponseScorer::with_config(rc.clone())
117 } else {
118 ResponseScorer::new()
119 };
120
121 Self {
122 config,
123 tool_scorer,
124 response_scorer,
125 llm_judge: Some(LlmJudge::new(judge_model)),
126 structured_judge: None,
127 cost_tracker: None,
128 trace_analyzer: None,
129 #[cfg(feature = "embedding")]
130 embedding_scorer: None,
131 conversation_scorer: None,
132 }
133 }
134
135 pub fn set_llm_judge(&mut self, judge_model: Arc<dyn Llm>) {
137 self.llm_judge = Some(LlmJudge::new(judge_model));
138 }
139
140 pub fn has_llm_judge(&self) -> bool {
142 self.llm_judge.is_some()
143 }
144
145 pub fn set_structured_judge(&mut self, judge: Arc<StructuredJudge>) {
147 self.structured_judge = Some(judge);
148 }
149
150 pub fn set_cost_tracker(&mut self, tracker: CostTracker) {
152 self.cost_tracker = Some(tracker);
153 }
154
155 pub fn set_trace_analyzer(&mut self, analyzer: TraceAnalyzer) {
157 self.trace_analyzer = Some(analyzer);
158 }
159
160 #[cfg(feature = "embedding")]
162 pub fn set_embedding_scorer(&mut self, scorer: Arc<EmbeddingScorer>) {
163 self.embedding_scorer = Some(scorer);
164 }
165
166 pub fn set_conversation_scorer(
168 &mut self,
169 scorer: Arc<crate::conversation_scorer::ConversationScorer>,
170 ) {
171 self.conversation_scorer = Some(scorer);
172 }
173
174 pub fn has_structured_judge(&self) -> bool {
176 self.structured_judge.is_some()
177 }
178
179 pub fn has_cost_tracker(&self) -> bool {
181 self.cost_tracker.is_some()
182 }
183
184 pub fn has_trace_analyzer(&self) -> bool {
186 self.trace_analyzer.is_some()
187 }
188
189 #[cfg(feature = "embedding")]
191 pub fn has_embedding_scorer(&self) -> bool {
192 self.embedding_scorer.is_some()
193 }
194
195 pub fn has_conversation_scorer(&self) -> bool {
197 self.conversation_scorer.is_some()
198 }
199
200 pub async fn evaluate_file(
202 &self,
203 agent: Arc<dyn Agent>,
204 path: impl AsRef<Path>,
205 ) -> Result<EvaluationReport> {
206 let test_file = TestFile::load(path)?;
207 self.evaluate_test_file(agent, &test_file).await
208 }
209
210 pub async fn evaluate_test_file(
212 &self,
213 agent: Arc<dyn Agent>,
214 test_file: &TestFile,
215 ) -> Result<EvaluationReport> {
216 let started_at = chrono::Utc::now();
217 let run_id = format!("{}_{}", test_file.eval_set_id, uuid::Uuid::new_v4());
218 let mut results = Vec::new();
219
220 for eval_case in &test_file.eval_cases {
221 let result = self.evaluate_case(agent.clone(), eval_case).await;
222
223 match result {
224 Ok(r) => {
225 let passed = r.passed;
226 results.push(r);
227 if !passed && !self.config.continue_on_failure {
228 break;
229 }
230 }
231 Err(e) => {
232 results.push(EvaluationResult::failed(
234 &eval_case.eval_id,
235 HashMap::new(),
236 vec![Failure::new(
237 "execution",
238 Value::Null,
239 Value::String(e.to_string()),
240 0.0,
241 1.0,
242 )],
243 Duration::from_secs(0),
244 ));
245 if !self.config.continue_on_failure {
246 break;
247 }
248 }
249 }
250 }
251
252 Ok(EvaluationReport::new(&run_id, results, started_at))
253 }
254
255 pub async fn evaluate_case(
257 &self,
258 agent: Arc<dyn Agent>,
259 eval_case: &EvalCase,
260 ) -> Result<EvaluationResult> {
261 let start = Instant::now();
262 let mut all_scores: HashMap<String, f64> = HashMap::new();
263 let mut all_failures: Vec<Failure> = Vec::new();
264 let mut turn_results: Vec<TurnResult> = Vec::new();
265 let mut all_events: Vec<Event> = Vec::new();
266
267 for turn in &eval_case.conversation {
269 let turn_result = self.execute_turn(agent.clone(), turn).await?;
270
271 let (scores, failures) = self.score_turn(turn, &turn_result).await;
273
274 for (criterion, score) in &scores {
276 all_scores
277 .entry(criterion.clone())
278 .and_modify(|s| *s = (*s + score) / 2.0)
279 .or_insert(*score);
280 }
281 all_failures.extend(failures);
282
283 if self.config.collect_turn_details {
284 turn_results.push(turn_result);
285 }
286 }
287
288 let case_events = self.collect_case_events(agent.clone(), eval_case).await;
291 if let Ok(events) = case_events {
292 all_events = events;
293 }
294
295 let duration = start.elapsed();
296
297 let cost_metrics = self
299 .cost_tracker
300 .as_ref()
301 .map(|tracker| tracker.extract_metrics(&all_events, duration));
302
303 let trace_analysis =
305 self.trace_analyzer.as_ref().map(|analyzer| analyzer.analyze(&all_events));
306
307 let mut verdicts = Vec::new();
309 if let Some(judge) = &self.structured_judge
310 && let Some(last_turn_result) = turn_results.last()
311 && let (Some(expected), Some(actual)) =
312 (&last_turn_result.expected_response, &last_turn_result.actual_response)
313 {
314 match judge.judge(expected, actual, "overall_quality").await {
315 Ok(verdict) => {
316 all_scores.insert("structured_judge".to_string(), verdict.score);
317 verdicts.push(verdict);
318 }
319 Err(e) => {
320 tracing::warn!("Structured judge failed: {e}");
321 let fallback = crate::structured_judge::StructuredVerdict {
323 score: 0.0,
324 reasoning: format!("Judge error: {e}"),
325 verdict: crate::structured_judge::Verdict::Fail,
326 };
327 verdicts.push(fallback);
328 }
329 }
330 }
331
332 #[cfg(feature = "embedding")]
334 if let Some(scorer) = &self.embedding_scorer
335 && let Some(last_turn_result) = turn_results.last()
336 && let (Some(expected), Some(actual)) =
337 (&last_turn_result.expected_response, &last_turn_result.actual_response)
338 {
339 match scorer.score(expected, actual).await {
340 Ok(score) => {
341 all_scores.insert("embedding_similarity".to_string(), score);
342 }
343 Err(e) => {
344 tracing::warn!("Embedding scorer failed: {e}");
345 }
346 }
347 }
348
349 let passed = all_failures.is_empty();
350
351 let mut result = if passed {
352 EvaluationResult::passed(&eval_case.eval_id, all_scores, duration)
353 } else {
354 EvaluationResult::failed(&eval_case.eval_id, all_scores, all_failures, duration)
355 };
356
357 if self.config.collect_turn_details {
358 result = result.with_turn_results(turn_results);
359 }
360
361 result.cost_metrics = cost_metrics;
363 result.trace_analysis = trace_analysis;
364 result.verdicts = verdicts;
365
366 Ok(result)
367 }
368
369 async fn collect_case_events(
372 &self,
373 agent: Arc<dyn Agent>,
374 eval_case: &EvalCase,
375 ) -> Result<Vec<Event>> {
376 if self.cost_tracker.is_none() && self.trace_analyzer.is_none() {
378 return Ok(Vec::new());
379 }
380
381 if let Some(first_turn) = eval_case.conversation.first() {
383 let input_content = first_turn.user_content.to_adk_content();
384 self.run_agent(agent, input_content).await
385 } else {
386 Ok(Vec::new())
387 }
388 }
389
390 async fn execute_turn(&self, agent: Arc<dyn Agent>, turn: &Turn) -> Result<TurnResult> {
392 let input_content = turn.user_content.to_adk_content();
394
395 let events = self.run_agent(agent, input_content).await?;
397
398 let (actual_response, actual_tool_calls) = self.extract_from_events(&events);
400
401 let expected_response = turn.final_response.as_ref().map(|c| c.get_text());
403 let expected_tool_calls =
404 turn.intermediate_data.as_ref().map(|d| d.tool_uses.clone()).unwrap_or_default();
405
406 Ok(TurnResult {
407 invocation_id: turn.invocation_id.clone(),
408 actual_response,
409 expected_response,
410 actual_tool_calls,
411 expected_tool_calls,
412 scores: HashMap::new(),
413 })
414 }
415
416 async fn run_agent(&self, agent: Arc<dyn Agent>, input: Content) -> Result<Vec<Event>> {
418 let invocation_id = uuid::Uuid::new_v4().to_string();
420 let ctx = Arc::new(EvalInvocationContext::new(invocation_id, input, agent.clone()));
421
422 let stream = agent.run(ctx).await.map_err(|e| {
424 crate::error::EvalError::ExecutionError(format!("Agent run failed: {}", e))
425 })?;
426
427 let events: Vec<Event> = stream.filter_map(|r| async { r.ok() }).collect().await;
429
430 Ok(events)
431 }
432
433 fn extract_from_events(&self, events: &[Event]) -> (Option<String>, Vec<ToolUse>) {
435 let mut response_text = String::new();
436 let mut tool_calls = Vec::new();
437 let complete_ids: std::collections::HashSet<_> = events
438 .iter()
439 .filter(|event| {
440 !event.llm_response.partial
441 && event
442 .llm_response
443 .provider_metadata
444 .as_ref()
445 .and_then(|metadata| metadata.get("content_complete"))
446 .and_then(serde_json::Value::as_bool)
447 == Some(true)
448 })
449 .map(|event| (&event.invocation_id, &event.id))
450 .collect();
451
452 for event in events {
453 if event.llm_response.partial
454 && complete_ids.contains(&(&event.invocation_id, &event.id))
455 {
456 continue;
457 }
458 if let Some(content) = event.content() {
460 for part in &content.parts {
461 if let Some(text) = part.text() {
463 response_text.push_str(text);
464 }
465 if let adk_core::Part::FunctionCall { name, args, .. } = part {
467 tool_calls.push(ToolUse {
468 name: name.clone(),
469 args: args.clone(),
470 expected_response: None,
471 });
472 }
473 }
474 }
475 }
476
477 let response = if response_text.is_empty() { None } else { Some(response_text) };
478
479 (response, tool_calls)
480 }
481
482 async fn score_turn(
484 &self,
485 turn: &Turn,
486 result: &TurnResult,
487 ) -> (HashMap<String, f64>, Vec<Failure>) {
488 let mut scores = HashMap::new();
489 let mut failures = Vec::new();
490
491 if let Some(threshold) = self.config.criteria.tool_trajectory_score {
493 let score =
494 self.tool_scorer.score(&result.expected_tool_calls, &result.actual_tool_calls);
495 scores.insert("tool_trajectory".to_string(), score);
496
497 if score < threshold {
498 failures.push(
499 Failure::new(
500 "tool_trajectory",
501 serde_json::to_value(&result.expected_tool_calls).unwrap_or_default(),
502 serde_json::to_value(&result.actual_tool_calls).unwrap_or_default(),
503 score,
504 threshold,
505 )
506 .with_details(&format!(
507 "Expected {} tool calls, got {}",
508 result.expected_tool_calls.len(),
509 result.actual_tool_calls.len()
510 )),
511 );
512 }
513 }
514
515 if let Some(threshold) = self.config.criteria.response_similarity {
517 if let (Some(expected), Some(actual)) =
518 (&result.expected_response, &result.actual_response)
519 {
520 let score = self.response_scorer.score(expected, actual);
521 scores.insert("response_similarity".to_string(), score);
522
523 if score < threshold {
524 failures.push(
525 Failure::new(
526 "response_similarity",
527 Value::String(expected.clone()),
528 Value::String(actual.clone()),
529 score,
530 threshold,
531 )
532 .with_details("Response text differs from expected"),
533 );
534 }
535 } else if result.expected_response.is_some() && result.actual_response.is_none() {
536 scores.insert("response_similarity".to_string(), 0.0);
537 failures.push(
538 Failure::new(
539 "response_similarity",
540 Value::String(result.expected_response.clone().unwrap_or_default()),
541 Value::Null,
542 0.0,
543 threshold,
544 )
545 .with_details("No response received"),
546 );
547 }
548 }
549
550 if let Some(threshold) = self.config.criteria.semantic_match_score
552 && let Some(judge) = &self.llm_judge
553 && let (Some(expected), Some(actual)) =
554 (&result.expected_response, &result.actual_response)
555 {
556 match judge
557 .semantic_match(
558 expected,
559 actual,
560 self.config.criteria.semantic_match_config.as_ref(),
561 )
562 .await
563 {
564 Ok(semantic_result) => {
565 scores.insert("semantic_match".to_string(), semantic_result.score);
566 if semantic_result.score < threshold {
567 failures.push(
568 Failure::new(
569 "semantic_match",
570 Value::String(expected.clone()),
571 Value::String(actual.clone()),
572 semantic_result.score,
573 threshold,
574 )
575 .with_details(&semantic_result.reasoning),
576 );
577 }
578 }
579 Err(e) => {
580 failures.push(
582 Failure::new(
583 "semantic_match",
584 Value::String(expected.clone()),
585 Value::String(actual.clone()),
586 0.0,
587 threshold,
588 )
589 .with_details(&format!("LLM judge error: {}", e)),
590 );
591 }
592 }
593 }
594
595 if let Some(threshold) = self.config.criteria.rubric_quality_score
597 && let Some(judge) = &self.llm_judge
598 && let Some(rubric_config) = &self.config.criteria.rubric_config
599 && let Some(actual) = &result.actual_response
600 {
601 let context = turn.user_content.get_text();
603 match judge.evaluate_rubrics(actual, &context, rubric_config).await {
604 Ok(rubric_result) => {
605 scores.insert("rubric_quality".to_string(), rubric_result.overall_score);
606 for rs in &rubric_result.rubric_scores {
608 scores.insert(format!("rubric_{}", rs.name), rs.score);
609 }
610 if rubric_result.overall_score < threshold {
611 let details = rubric_result
612 .rubric_scores
613 .iter()
614 .map(|rs| format!("{}: {:.2} - {}", rs.name, rs.score, rs.reasoning))
615 .collect::<Vec<_>>()
616 .join("; ");
617 failures.push(
618 Failure::new(
619 "rubric_quality",
620 Value::Number(
621 serde_json::Number::from_f64(threshold)
622 .unwrap_or(serde_json::Number::from(0)),
623 ),
624 Value::Number(
625 serde_json::Number::from_f64(rubric_result.overall_score)
626 .unwrap_or(serde_json::Number::from(0)),
627 ),
628 rubric_result.overall_score,
629 threshold,
630 )
631 .with_details(&details),
632 );
633 }
634 }
635 Err(e) => {
636 failures.push(
637 Failure::new("rubric_quality", Value::Null, Value::Null, 0.0, threshold)
638 .with_details(&format!("LLM judge error: {}", e)),
639 );
640 }
641 }
642 }
643
644 if let Some(threshold) = self.config.criteria.safety_score
646 && let Some(judge) = &self.llm_judge
647 && let Some(actual) = &result.actual_response
648 {
649 match judge.evaluate_safety(actual).await {
650 Ok(safety_result) => {
651 scores.insert("safety".to_string(), safety_result.score);
652 if safety_result.score < threshold {
653 failures.push(
654 Failure::new(
655 "safety",
656 Value::Number(
657 serde_json::Number::from_f64(threshold)
658 .unwrap_or(serde_json::Number::from(0)),
659 ),
660 Value::Number(
661 serde_json::Number::from_f64(safety_result.score)
662 .unwrap_or(serde_json::Number::from(0)),
663 ),
664 safety_result.score,
665 threshold,
666 )
667 .with_details(&format!(
668 "Safety issues: {}",
669 safety_result.issues.join(", ")
670 )),
671 );
672 }
673 }
674 Err(e) => {
675 failures.push(
676 Failure::new("safety", Value::Null, Value::Null, 0.0, threshold)
677 .with_details(&format!("LLM judge error: {}", e)),
678 );
679 }
680 }
681 }
682
683 if let Some(threshold) = self.config.criteria.hallucination_score
685 && let Some(judge) = &self.llm_judge
686 && let Some(actual) = &result.actual_response
687 {
688 let context = turn.user_content.get_text();
689 let ground_truth = result.expected_response.as_deref();
690 match judge.detect_hallucinations(actual, &context, ground_truth).await {
691 Ok(hallucination_result) => {
692 scores.insert("hallucination".to_string(), hallucination_result.score);
693 if hallucination_result.score < threshold {
694 failures.push(
695 Failure::new(
696 "hallucination",
697 Value::Number(
698 serde_json::Number::from_f64(threshold)
699 .unwrap_or(serde_json::Number::from(0)),
700 ),
701 Value::Number(
702 serde_json::Number::from_f64(hallucination_result.score)
703 .unwrap_or(serde_json::Number::from(0)),
704 ),
705 hallucination_result.score,
706 threshold,
707 )
708 .with_details(&format!(
709 "Hallucinations detected: {}",
710 hallucination_result.issues.join(", ")
711 )),
712 );
713 }
714 }
715 Err(e) => {
716 failures.push(
717 Failure::new("hallucination", Value::Null, Value::Null, 0.0, threshold)
718 .with_details(&format!("LLM judge error: {}", e)),
719 );
720 }
721 }
722 }
723
724 (scores, failures)
725 }
726
727 pub async fn evaluate_cases_parallel(
729 &self,
730 agent: Arc<dyn Agent>,
731 cases: &[EvalCase],
732 concurrency: usize,
733 ) -> Vec<Result<EvaluationResult>> {
734 use futures::stream::{self, StreamExt};
735
736 let results: Vec<_> = stream::iter(cases)
737 .map(|case| {
738 let agent = agent.clone();
739 async move { self.evaluate_case(agent, case).await }
740 })
741 .buffer_unordered(concurrency)
742 .collect()
743 .await;
744
745 results
746 }
747
748 pub async fn evaluate_directory(
750 &self,
751 agent: Arc<dyn Agent>,
752 dir: impl AsRef<Path>,
753 ) -> Result<Vec<EvaluationReport>> {
754 let mut reports = Vec::new();
755
756 let entries = std::fs::read_dir(dir)?;
757 for entry in entries {
758 let entry = entry?;
759 let path = entry.path();
760
761 if path.extension().is_some_and(|ext| ext == "json")
762 && let Some(name) = path.file_name().and_then(|n| n.to_str())
763 && name.ends_with(".test.json")
764 {
765 let report = self.evaluate_file(agent.clone(), &path).await?;
766 reports.push(report);
767 }
768 }
769
770 Ok(reports)
771 }
772
773 #[cfg(feature = "personas")]
787 pub async fn evaluate_multi_turn(
788 &self,
789 agent: Arc<dyn Agent>,
790 simulator: &crate::personas::UserSimulator,
791 num_turns: usize,
792 ) -> Result<Vec<Content>> {
793 let mut history: Vec<Content> = Vec::new();
794
795 for _turn_idx in 0..num_turns {
796 let user_message = simulator.generate_message(&history).await?;
798 history.push(user_message.clone());
799
800 let events = self.run_agent(agent.clone(), user_message).await?;
802
803 let (response_text, _tool_calls) = self.extract_from_events(&events);
805 if let Some(text) = response_text {
806 history.push(Content::new("model").with_text(text));
807 }
808 }
809
810 Ok(history)
811 }
812}
813
814impl Default for Evaluator {
815 fn default() -> Self {
816 Self::new(EvaluationConfig::default())
817 }
818}
819
820struct EvalInvocationContext {
826 invocation_id: String,
827 user_content: Content,
828 agent: Arc<dyn Agent>,
829 session: EvalSession,
830 run_config: adk_core::RunConfig,
831 ended: std::sync::atomic::AtomicBool,
832}
833
834impl EvalInvocationContext {
835 fn new(invocation_id: String, user_content: Content, agent: Arc<dyn Agent>) -> Self {
836 let session_id = format!("eval-session-{}", uuid::Uuid::new_v4());
837 Self {
838 invocation_id,
839 user_content,
840 agent,
841 session: EvalSession::new(session_id),
842 run_config: adk_core::RunConfig::default(),
843 ended: std::sync::atomic::AtomicBool::new(false),
844 }
845 }
846}
847
848impl adk_core::ReadonlyContext for EvalInvocationContext {
849 fn invocation_id(&self) -> &str {
850 &self.invocation_id
851 }
852
853 fn agent_name(&self) -> &str {
854 self.agent.name()
855 }
856
857 fn user_id(&self) -> &str {
858 "eval_user"
859 }
860
861 fn app_name(&self) -> &str {
862 "eval_app"
863 }
864
865 fn session_id(&self) -> &str {
866 &self.session.id
867 }
868
869 fn branch(&self) -> &str {
870 "main"
871 }
872
873 fn user_content(&self) -> &Content {
874 &self.user_content
875 }
876}
877
878#[async_trait]
879impl adk_core::CallbackContext for EvalInvocationContext {
880 fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
881 None
882 }
883}
884
885#[async_trait]
886impl adk_core::InvocationContext for EvalInvocationContext {
887 fn agent(&self) -> Arc<dyn Agent> {
888 self.agent.clone()
889 }
890
891 fn memory(&self) -> Option<Arc<dyn adk_core::Memory>> {
892 None
893 }
894
895 fn session(&self) -> &dyn adk_core::Session {
896 &self.session
897 }
898
899 fn run_config(&self) -> &adk_core::RunConfig {
900 &self.run_config
901 }
902
903 fn end_invocation(&self) {
904 self.ended.store(true, std::sync::atomic::Ordering::SeqCst);
905 }
906
907 fn ended(&self) -> bool {
908 self.ended.load(std::sync::atomic::Ordering::SeqCst)
909 }
910}
911
912struct EvalSession {
914 id: String,
915 state: EvalState,
916}
917
918impl EvalSession {
919 fn new(id: String) -> Self {
920 Self { id, state: EvalState::new() }
921 }
922}
923
924impl adk_core::Session for EvalSession {
925 fn id(&self) -> &str {
926 &self.id
927 }
928
929 fn app_name(&self) -> &str {
930 "eval_app"
931 }
932
933 fn user_id(&self) -> &str {
934 "eval_user"
935 }
936
937 fn state(&self) -> &dyn adk_core::State {
938 &self.state
939 }
940
941 fn conversation_history(&self) -> Vec<Content> {
942 vec![]
943 }
944}
945
946struct EvalState {
948 data: std::sync::RwLock<HashMap<String, serde_json::Value>>,
949}
950
951impl EvalState {
952 fn new() -> Self {
953 Self { data: std::sync::RwLock::new(HashMap::new()) }
954 }
955}
956
957impl adk_core::State for EvalState {
958 fn get(&self, key: &str) -> Option<serde_json::Value> {
959 self.data.read().ok()?.get(key).cloned()
960 }
961
962 fn set(&mut self, key: String, value: serde_json::Value) {
963 if let Err(msg) = adk_core::validate_state_key(&key) {
964 tracing::warn!(key = %key, "rejecting invalid state key: {msg}");
965 return;
966 }
967 if let Ok(mut data) = self.data.write() {
968 data.insert(key, value);
969 }
970 }
971
972 fn all(&self) -> HashMap<String, serde_json::Value> {
973 self.data.read().ok().map(|d| d.clone()).unwrap_or_default()
974 }
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980
981 #[test]
982 fn complete_snapshots_replace_partial_text_and_calls() {
983 let evaluator = Evaluator::new(EvaluationConfig::default());
984 let mut partial = Event::with_id("response", "invocation");
985 partial.llm_response.partial = true;
986 partial.llm_response.content = Some(Content::new("model").with_text("Draft"));
987 let mut final_event = partial.clone();
988 final_event.llm_response.partial = false;
989 final_event.llm_response.provider_metadata =
990 Some(serde_json::json!({"content_complete":true}));
991 final_event.llm_response.content = Some(Content::new("model").with_text("Corrected"));
992 let (text, calls) = evaluator.extract_from_events(&[partial, final_event]);
993 assert_eq!(text.as_deref(), Some("Corrected"));
994 assert!(calls.is_empty());
995 }
996
997 #[test]
998 fn test_evaluator_creation() {
999 let config = EvaluationConfig::with_criteria(
1000 EvaluationCriteria::exact_tools().with_response_similarity(0.8),
1001 );
1002 let evaluator = Evaluator::new(config);
1003 assert!(evaluator.config.criteria.tool_trajectory_score.is_some());
1004 assert!(evaluator.config.criteria.response_similarity.is_some());
1005 }
1006
1007 #[tokio::test]
1008 async fn test_turn_scoring() {
1009 let config = EvaluationConfig::with_criteria(EvaluationCriteria {
1010 tool_trajectory_score: Some(1.0),
1011 response_similarity: Some(0.8),
1012 ..Default::default()
1013 });
1014 let evaluator = Evaluator::new(config);
1015
1016 let turn = Turn {
1017 invocation_id: "test".to_string(),
1018 user_content: crate::schema::ContentData::text("Hello"),
1019 final_response: Some(crate::schema::ContentData::model_response("Hi there!")),
1020 intermediate_data: Some(crate::schema::IntermediateData {
1021 tool_uses: vec![ToolUse::new("greet")],
1022 ..Default::default()
1023 }),
1024 };
1025
1026 let result = TurnResult {
1027 invocation_id: "test".to_string(),
1028 actual_response: Some("Hi there!".to_string()),
1029 expected_response: Some("Hi there!".to_string()),
1030 actual_tool_calls: vec![ToolUse::new("greet")],
1031 expected_tool_calls: vec![ToolUse::new("greet")],
1032 scores: HashMap::new(),
1033 };
1034
1035 let (scores, failures) = evaluator.score_turn(&turn, &result).await;
1036 assert!(failures.is_empty());
1037 assert_eq!(scores.get("tool_trajectory"), Some(&1.0));
1038 assert_eq!(scores.get("response_similarity"), Some(&1.0));
1039 }
1040}