use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::automaton::infra::llm_interface::{CallResult, LlmCall, ToolDef};
#[derive(Debug, Clone)]
pub struct ToolCallerConfig {
pub system_prompt: Option<String>,
pub max_iterations: u32,
}
impl Default for ToolCallerConfig {
fn default() -> Self {
Self {
system_prompt: None,
max_iterations: 5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallerResult {
pub response: String,
pub tools_called: Vec<String>,
pub iterations: u32,
}
pub struct ToolCallerAgent {
config: ToolCallerConfig,
llm: Arc<dyn LlmCall>,
}
impl ToolCallerAgent {
pub fn new(config: ToolCallerConfig, llm: Arc<dyn LlmCall>) -> Self {
let max_iterations = if config.max_iterations == 0 {
5
} else {
config.max_iterations
};
Self {
config: ToolCallerConfig {
max_iterations,
..config
},
llm,
}
}
pub async fn run(
&self,
query: &str,
tools: Vec<ToolDef>,
executor: impl Fn(&str, &str) -> Result<String, String>,
) -> Result<ToolCallerResult, String> {
let system = self
.config
.system_prompt
.clone()
.unwrap_or_else(|| "You are a helpful AI assistant.".to_string());
let mut messages: Vec<Value> = vec![
json!({ "role": "system", "content": system }),
json!({ "role": "user", "content": query }),
];
let mut tools_called: Vec<String> = Vec::new();
let mut iteration = 0u32;
let mut final_response = String::new();
loop {
iteration += 1;
if iteration > self.config.max_iterations {
return Err(format!(
"ToolCallerAgent exceeded max_iterations ({})",
self.config.max_iterations
));
}
let result = self.llm.call_with_tools(messages.clone(), &tools).await?;
match result {
CallResult::Text(text) => {
final_response = text;
break;
}
CallResult::ToolCalls(calls) => {
if calls.is_empty() {
break;
}
let tool_calls_json: Vec<Value> = calls
.iter()
.map(|tc| {
json!({
"id": tc.id,
"type": "function",
"function": {
"name": tc.name,
"arguments": tc.arguments,
}
})
})
.collect();
messages.push(json!({
"role": "assistant",
"content": null,
"tool_calls": tool_calls_json,
}));
for call in &calls {
tools_called.push(call.name.clone());
let tool_result = match executor(&call.name, &call.arguments) {
Ok(out) => out,
Err(e) => format!("Tool error: {}", e),
};
messages.push(json!({
"role": "tool",
"tool_call_id": call.id,
"content": tool_result,
}));
}
}
}
}
Ok(ToolCallerResult {
response: final_response,
tools_called,
iterations: iteration,
})
}
}