use anyhow::Result;
use skilllite_evolution::feedback::{DecisionInput, FeedbackSignal as EvolutionFeedbackSignal};
use skilllite_evolution::{strip_think_blocks, EvolutionLlm, EvolutionMessage};
use super::llm::LlmClient;
use super::types::{ChatMessage, ExecutionFeedback, FeedbackSignal};
pub struct EvolutionLlmAdapter<'a> {
pub llm: &'a LlmClient,
}
#[async_trait::async_trait]
impl EvolutionLlm for EvolutionLlmAdapter<'_> {
async fn complete(
&self,
messages: &[EvolutionMessage],
model: &str,
temperature: f64,
) -> Result<String> {
let chat_messages: Vec<ChatMessage> = messages
.iter()
.map(|m| ChatMessage {
role: m.role.clone(),
content: m.content.clone(),
tool_calls: None,
tool_call_id: None,
name: None,
})
.collect();
let response = self
.llm
.chat_completion(model, &chat_messages, None, Some(temperature))
.await?;
let msg = response.choices.first().map(|c| &c.message);
let content = msg.and_then(|m| m.content.as_deref()).unwrap_or("").trim();
let has_reasoning_field = msg.and_then(|m| m.reasoning_content.as_ref()).is_some();
if has_reasoning_field {
Ok(content.to_string())
} else {
Ok(strip_think_blocks(content).to_string())
}
}
}
pub fn execution_feedback_to_decision_input(feedback: &ExecutionFeedback) -> DecisionInput {
DecisionInput {
total_tools: feedback.total_tools,
failed_tools: feedback.failed_tools,
replans: feedback.replans,
elapsed_ms: feedback.elapsed_ms,
task_completed: feedback.task_completed,
task_description: feedback.task_description.clone(),
rules_used: feedback.rules_used.clone(),
tools_detail: feedback
.tools_detail
.iter()
.map(|t| skilllite_evolution::feedback::ToolExecDetail {
tool: t.tool.clone(),
success: t.success,
})
.collect(),
}
}
pub fn to_evolution_feedback(signal: FeedbackSignal) -> EvolutionFeedbackSignal {
match signal {
FeedbackSignal::ExplicitPositive => EvolutionFeedbackSignal::ExplicitPositive,
FeedbackSignal::ExplicitNegative => EvolutionFeedbackSignal::ExplicitNegative,
FeedbackSignal::Neutral => EvolutionFeedbackSignal::Neutral,
}
}
pub use skilllite_evolution::feedback;
pub use skilllite_evolution::seed;
pub use skilllite_evolution::{
check_auto_rollback, format_evolution_changes, on_shutdown, query_changes_by_txn,
run_evolution, EvolutionMode,
};
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{ExecutionFeedback, ToolExecDetail};
#[test]
fn test_execution_feedback_to_decision_input_preserves_rules_used() {
let feedback = ExecutionFeedback {
total_tools: 2,
failed_tools: 0,
replans: 1,
iterations: 3,
elapsed_ms: 1200,
context_overflow_retries: 0,
task_completed: true,
task_description: Some("test task".to_string()),
rules_used: vec!["rule.alpha".to_string(), "rule.beta".to_string()],
tools_detail: vec![ToolExecDetail {
tool: "read_file".to_string(),
success: true,
}],
};
let input = execution_feedback_to_decision_input(&feedback);
assert_eq!(
input.rules_used,
vec!["rule.alpha".to_string(), "rule.beta".to_string()]
);
}
#[test]
fn test_execution_feedback_to_decision_input_preserves_tools_detail() {
let feedback = ExecutionFeedback {
total_tools: 2,
failed_tools: 1,
replans: 1,
iterations: 3,
elapsed_ms: 1200,
context_overflow_retries: 0,
task_completed: false,
task_description: Some("another test task".to_string()),
rules_used: vec!["rule.gamma".to_string()],
tools_detail: vec![
ToolExecDetail {
tool: "list_directory".to_string(),
success: true,
},
ToolExecDetail {
tool: "write_file".to_string(),
success: false,
},
],
};
let input = execution_feedback_to_decision_input(&feedback);
assert_eq!(input.tools_detail.len(), 2);
assert_eq!(input.tools_detail[0].tool, "list_directory".to_string());
assert!(input.tools_detail[0].success);
assert_eq!(input.tools_detail[1].tool, "write_file".to_string());
assert!(!input.tools_detail[1].success);
}
}