use crate::{
completion::{CompletionModel, CompletionRequest, ModelChoice},
message::{AssistantContent, Message, ToolCall, ToolResult, UserContent},
tool::{ToolError, ToolSet},
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AgentError<M: std::error::Error + 'static> {
#[error("Completion error: {0}")]
Completion(M),
#[error("Tool error: {0}")]
Tool(ToolError),
#[error("Exceeded maximum iterations ({0}) without a final text reply")]
MaxIterations(u32),
}
pub struct Agent<M> {
model: M,
preamble: Option<String>,
tools: ToolSet,
temperature: Option<f64>,
max_tokens: Option<u32>,
thinking: Option<bool>,
max_iterations: u32,
context: Vec<String>,
}
impl<M: CompletionModel> Agent<M> {
pub fn builder(model: M) -> AgentBuilder<M> {
AgentBuilder::new(model)
}
pub async fn prompt(&self, prompt: &str) -> Result<String, AgentError<M::Error>> {
self.chat(prompt, vec![]).await
}
pub async fn chat(
&self,
prompt: &str,
history: Vec<Message>,
) -> Result<String, AgentError<M::Error>> {
let mut messages = self.build_messages(prompt, history);
for _ in 0..self.max_iterations {
let request = self.build_request(messages.clone());
let response = self.model.complete(request).await.map_err(AgentError::Completion)?;
match response.choice {
ModelChoice::Message(text) => return Ok(text),
ModelChoice::ToolCall(calls) => {
messages.push(Message::Assistant {
content: calls
.iter()
.map(|c| AssistantContent::ToolCall(c.clone()))
.collect(),
});
let mut results: Vec<UserContent> = Vec::with_capacity(calls.len());
for call in &calls {
let result = self.dispatch_tool(call).await;
results.push(UserContent::ToolResult(result));
}
messages.push(Message::User { content: results });
}
}
}
Err(AgentError::MaxIterations(self.max_iterations))
}
fn build_messages(&self, prompt: &str, mut history: Vec<Message>) -> Vec<Message> {
let mut messages: Vec<Message> = Vec::new();
if let Some(preamble) = &self.preamble {
messages.push(Message::system(preamble));
}
if !self.context.is_empty() {
let combined = self.context.join("\n\n");
messages.push(Message::user(combined));
}
messages.append(&mut history);
messages.push(Message::user(prompt));
messages
}
fn build_request(&self, messages: Vec<Message>) -> CompletionRequest {
let mut req = CompletionRequest::new(messages);
req.tools = self.tools.definitions();
req.temperature = self.temperature;
req.max_tokens = self.max_tokens;
req.thinking = self.thinking;
req
}
async fn dispatch_tool(&self, call: &ToolCall) -> ToolResult {
let result = self.tools.call(&call.name, call.arguments.clone()).await;
let content = match result {
Ok(v) => v.to_string(),
Err(e) => format!("Error: {e}"),
};
ToolResult {
call_id: call.id.clone(),
name: call.name.clone(),
content,
}
}
}
pub struct AgentBuilder<M> {
model: M,
preamble: Option<String>,
tools: ToolSet,
temperature: Option<f64>,
max_tokens: Option<u32>,
thinking: Option<bool>,
max_iterations: u32,
context: Vec<String>,
}
impl<M: CompletionModel> AgentBuilder<M> {
fn new(model: M) -> Self {
Self {
model,
preamble: None,
tools: ToolSet::new(),
temperature: None,
max_tokens: None,
thinking: None,
max_iterations: 10,
context: Vec::new(),
}
}
pub fn preamble(mut self, preamble: impl Into<String>) -> Self {
self.preamble = Some(preamble.into());
self
}
pub fn tool<T: crate::tool::Tool + 'static>(mut self, tool: T) -> Self {
self.tools.add(tool);
self
}
pub fn temperature(mut self, temperature: f64) -> Self {
self.temperature = Some(temperature);
self
}
pub fn max_tokens(mut self, max_tokens: u32) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn max_iterations(mut self, n: u32) -> Self {
self.max_iterations = n;
self
}
pub fn thinking(mut self, enabled: bool) -> Self {
self.thinking = Some(enabled);
self
}
pub fn context(mut self, doc: impl Into<String>) -> Self {
self.context.push(doc.into());
self
}
pub fn build(self) -> Agent<M> {
Agent {
model: self.model,
preamble: self.preamble,
tools: self.tools,
temperature: self.temperature,
max_tokens: self.max_tokens,
thinking: self.thinking,
max_iterations: self.max_iterations,
context: self.context,
}
}
}