systemprompt_evaluation/services/
replay.rs1use systemprompt_identifiers::{Actor, AgentName, ContextId, SessionId, TraceId, UserId};
7use systemprompt_models::RequestContext;
8use systemprompt_models::ai::{AiMessage, AiRequest, AiResponse, DynAiProvider, MessageRole};
9
10use crate::error::{EvaluationError, Result};
11use crate::models::{CanonicalMessage, CanonicalPrompt};
12
13const REPLAY_ACTOR_JOB: &str = "evaluation_replay";
14const REPLAY_AGENT: &str = "evaluation-replay";
15const REPLAY_MAX_OUTPUT_TOKENS: u32 = 8192;
16
17#[derive(Clone)]
18pub struct ReplayService {
19 ai: DynAiProvider,
20 created_by: UserId,
21 run_context: ContextId,
22}
23
24impl std::fmt::Debug for ReplayService {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("ReplayService")
27 .field("created_by", &self.created_by)
28 .finish_non_exhaustive()
29 }
30}
31
32impl ReplayService {
33 pub const fn new(ai: DynAiProvider, created_by: UserId, run_context: ContextId) -> Self {
34 Self {
35 ai,
36 created_by,
37 run_context,
38 }
39 }
40
41 pub async fn replay(&self, prompt: &CanonicalPrompt, repair_hint: &str) -> Result<AiResponse> {
45 let messages = build_messages(&prompt.messages, repair_hint)?;
46 let context = RequestContext::new(
47 SessionId::generate(),
48 TraceId::generate(),
49 self.run_context.clone(),
50 AgentName::new(REPLAY_AGENT),
51 )
52 .with_actor(Actor::job(self.created_by.clone(), REPLAY_ACTOR_JOB));
53
54 let mut builder = AiRequest::builder(
55 messages,
56 prompt.provider.clone(),
57 prompt.model.clone(),
58 REPLAY_MAX_OUTPUT_TOKENS,
59 context,
60 );
61 if let Some(system) = &prompt.system_prompt {
62 builder = builder.with_system_prompt(system.clone());
63 }
64 self.ai
65 .generate(&builder.build())
66 .await
67 .map_err(|e| EvaluationError::Ai(e.to_string()))
68 }
69}
70
71fn build_messages(canonical: &[CanonicalMessage], repair_hint: &str) -> Result<Vec<AiMessage>> {
72 if canonical.is_empty() {
73 return Err(EvaluationError::ReplaySource(
74 "canonical prompt has no messages".to_owned(),
75 ));
76 }
77 let last_user = canonical
78 .iter()
79 .rposition(|m| m.role == "user")
80 .ok_or_else(|| {
81 EvaluationError::ReplaySource("canonical prompt has no user turn".to_owned())
82 })?;
83
84 let mut messages = Vec::with_capacity(canonical.len() + 1);
85 for (idx, message) in canonical.iter().enumerate() {
86 if idx == last_user {
87 messages.push(AiMessage::system(format!(
88 "Apply this correction when answering the next user message: {repair_hint}"
89 )));
90 }
91 messages.push(to_ai_message(message));
92 }
93 Ok(messages)
94}
95
96fn to_ai_message(message: &CanonicalMessage) -> AiMessage {
97 let role = match message.role.as_str() {
98 "system" => MessageRole::System,
99 "assistant" => MessageRole::Assistant,
100 _ => MessageRole::User,
101 };
102 AiMessage {
103 role,
104 content: message.content.clone(),
105 parts: Vec::new(),
106 }
107}