use std::sync::Arc;
use autoagents_llm::LLMProvider;
use autoagents_llm::chat::{ChatMessage, ChatRole, FunctionTool, MessageType, Tool as LlmTool};
use serde_json::Value;
use crate::error::AgentError;
use crate::state::{LlmResponse, Message, Role, ToolCall, ToolSchema};
pub fn to_autoagents_messages(messages: &[Message]) -> Vec<ChatMessage> {
messages
.iter()
.map(|m| {
let role = match m.role {
Role::System => ChatRole::System,
Role::User => ChatRole::User,
Role::Assistant => ChatRole::Assistant,
Role::Tool => ChatRole::Tool,
};
ChatMessage {
role,
message_type: MessageType::default(),
content: render_content(m),
}
})
.collect()
}
fn render_content(m: &Message) -> String {
if m.tool_calls.is_empty() {
m.content.clone()
} else {
let calls_json = serde_json::to_string(&m.tool_calls).unwrap_or_else(|_| "[]".into());
format!("{}\n```tool_calls\n{calls_json}\n```", m.content)
}
}
pub fn to_autoagents_tools(schemas: &[ToolSchema]) -> Vec<LlmTool> {
schemas
.iter()
.map(|s| LlmTool {
tool_type: "function".to_string(),
function: FunctionTool {
name: s.name.clone(),
description: s.description.clone(),
parameters: s.args_schema.clone(),
},
})
.collect()
}
pub async fn chat(
llm: &Arc<dyn LLMProvider>,
messages: &[Message],
tools: &[ToolSchema],
) -> Result<LlmResponse, AgentError> {
let chat_msgs = to_autoagents_messages(messages);
let llm_tools = to_autoagents_tools(tools);
let response = llm
.chat_with_tools(&chat_msgs, Some(&llm_tools), None)
.await
.map_err(|e| AgentError::Llm(e.to_string()))?;
tracing::debug!("LLM response received with {:?}", response);
if let Some(thought) = response.thinking() {
tracing::info!("Agent thought: {}", thought);
}
if let Some(native_calls) = response.tool_calls()
&& !native_calls.is_empty()
{
let mut calls = Vec::with_capacity(native_calls.len());
for tc in native_calls {
let args: Value = if tc.function.arguments.trim().is_empty() {
Value::Object(serde_json::Map::default())
} else {
serde_json::from_str(&tc.function.arguments).map_err(|e| {
AgentError::ResponseParse(format!(
"tool '{}' arguments not valid JSON: {e}",
tc.function.name
))
})?
};
calls.push(ToolCall {
id: tc.id,
name: tc.function.name,
args,
});
}
return Ok(LlmResponse::UseTools { calls });
}
let text = response.text().unwrap_or_default();
parse_response(&text)
}
pub fn parse_response(text: &str) -> Result<LlmResponse, AgentError> {
if let Some((before_fence, fence_body)) = extract_fence(text, "tool_calls") {
let calls: Vec<ToolCall> = serde_json::from_str(fence_body)
.map_err(|e| AgentError::ResponseParse(format!("tool_calls JSON: {e}")))?;
if !calls.is_empty() {
let _ = before_fence; return Ok(LlmResponse::UseTools { calls });
}
}
let answer = text.trim().to_string();
if answer.is_empty() {
return Err(AgentError::ResponseParse("empty LLM response".into()));
}
Ok(LlmResponse::Final { answer })
}
fn extract_fence<'a>(text: &'a str, tag: &str) -> Option<(&'a str, &'a str)> {
let opener = format!("```{tag}");
let start = text.find(&opener)?;
let after_open = start + opener.len();
let after_open = text[after_open..].find('\n').map(|i| after_open + i + 1)?;
let rest = &text[after_open..];
let end = rest.find("```")?;
Some((&text[..start], &rest[..end]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_final_answer() {
let r = parse_response("The answer is 42.").unwrap();
let LlmResponse::Final { answer } = r else {
unreachable!("expected Final, got {r:?}");
};
assert_eq!(answer, "The answer is 42.");
}
#[test]
fn parses_tool_calls_fence() {
let text = "I'll use add.\n```tool_calls\n[{\"id\":\"1\",\"name\":\"add\",\"args\":{\"a\":1,\"b\":2}}]\n```";
let r = parse_response(text).unwrap();
let LlmResponse::UseTools { calls } = r else {
unreachable!("expected UseTools, got {r:?}");
};
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "add");
assert_eq!(calls[0].id, "1");
}
#[test]
fn rejects_empty_response() {
assert!(parse_response("").is_err());
assert!(parse_response(" ").is_err());
}
#[test]
fn converts_tool_schema_to_llm_tool() {
let schemas = vec![ToolSchema {
name: "add".into(),
description: "Add two numbers".into(),
args_schema: serde_json::json!({"type": "object"}),
}];
let tools = to_autoagents_tools(&schemas);
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].function.name, "add");
assert_eq!(tools[0].tool_type, "function");
}
}