use std::sync::Arc;
use adk_core::model::{Llm, LlmRequest};
use adk_core::types::Content;
use futures::StreamExt;
use super::profile::PersonaProfile;
use crate::error::{EvalError, Result};
pub struct UserSimulator {
llm: Arc<dyn Llm>,
persona: PersonaProfile,
}
impl UserSimulator {
pub fn new(llm: Arc<dyn Llm>, persona: PersonaProfile) -> Self {
Self { llm, persona }
}
pub fn persona(&self) -> &PersonaProfile {
&self.persona
}
pub async fn generate_message(&self, history: &[Content]) -> Result<Content> {
let system_prompt = self.build_system_prompt();
let mut contents = Vec::with_capacity(history.len() + 1);
contents.push(Content::new("user").with_text(&system_prompt));
contents
.push(Content::new("model").with_text("Understood. I will role-play as this persona."));
contents.extend_from_slice(history);
if history.is_empty() {
contents.push(
Content::new("user")
.with_text("Start the conversation as this persona. Send your first message."),
);
} else {
contents.push(
Content::new("user").with_text(
"Continue the conversation as this persona. Send your next message.",
),
);
}
let request = LlmRequest::new(self.llm.name(), contents);
let mut stream = self
.llm
.generate_content(request, false)
.await
.map_err(|e| EvalError::ExecutionError(format!("LLM generation failed: {e}")))?;
let mut response_text = String::new();
while let Some(result) = stream.next().await {
let response =
result.map_err(|e| EvalError::ExecutionError(format!("LLM stream error: {e}")))?;
if let Some(content) = &response.content {
for part in &content.parts {
if let Some(text) = part.text() {
response_text.push_str(text);
}
}
}
}
if response_text.is_empty() {
return Err(EvalError::ExecutionError(
"LLM returned empty response for persona message".to_string(),
));
}
Ok(Content::new("user").with_text(response_text))
}
fn build_system_prompt(&self) -> String {
let persona = &self.persona;
let traits = &persona.traits;
let mut prompt = format!(
"You are role-playing as a user persona named \"{}\".\n\
Description: {}\n\n\
Communication style: {}\n\
Verbosity: {:?}\n\
Expertise level: {:?}\n",
persona.name,
persona.description,
traits.communication_style,
traits.verbosity,
traits.expertise_level,
);
if !persona.goals.is_empty() {
prompt.push_str("\nGoals (pursue these during the conversation):\n");
for goal in &persona.goals {
prompt.push_str(&format!("- {goal}\n"));
}
}
if !persona.constraints.is_empty() {
prompt.push_str("\nConstraints (always respect these):\n");
for constraint in &persona.constraints {
prompt.push_str(&format!("- {constraint}\n"));
}
}
prompt.push_str(
"\nRespond ONLY with the persona's message. \
Do not include any meta-commentary, labels, or prefixes. \
Stay in character at all times.",
);
prompt
}
}