use crate::prompt::IntoPrompt;
use crate::tool::{ToolBox, ToolError};
use genai::Client;
use std::sync::Arc;
use thiserror::Error;
mod core;
mod model;
mod request;
mod stream;
pub use model::{ChatOptions, ReasoningEffort, StopReason, ToolChoice, Usage};
pub use request::{AgentRequest, AgentResult, Complete, RunOptions, Streaming};
pub use stream::{AgentEvent, AgentStream};
#[cfg(test)]
mod tests;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum AgentError {
#[error("failed to create the LLM client: {0}")]
ClientInit(#[source] LlmError),
#[error("LLM request failed: {0}")]
Llm(#[source] LlmError),
#[error("invalid answer schema: {0}")]
Schema(String),
#[error("failed to parse the model answer: {source}")]
InvalidAnswer {
raw: String,
#[source]
source: serde_json::Error,
},
#[error("the model answer was truncated by the token limit")]
MaxTokens {
raw: String,
},
#[error("no model set: use Agent::with_model or AgentRequest::with_model")]
MissingModel,
#[error("event listener failed: {0}")]
Listener(#[source] crate::listener::ListenerError),
#[error("no final answer after {0} iterations")]
MaxIterationsReached(usize),
#[error("tool '{tool}' failed: {source}")]
Tool {
tool: String,
#[source]
source: ToolError,
},
}
impl AgentError {
pub fn is_transient(&self) -> bool {
match self {
Self::Llm(err) => err.is_transient(),
_ => false,
}
}
}
#[derive(Error, Debug)]
#[error("{source}")]
pub struct LlmError {
status: Option<u16>,
transient: bool,
source: Box<dyn std::error::Error + Send + Sync>,
}
impl LlmError {
pub fn status(&self) -> Option<u16> {
self.status
}
pub fn is_transient(&self) -> bool {
self.transient
}
}
impl LlmError {
pub(crate) fn from_genai(err: genai::Error) -> Self {
let status = err.status().map(|status| status.as_u16());
let transient = match status {
Some(status) => status >= 500 || matches!(status, 408 | 425 | 429),
None => match &err {
genai::Error::WebModelCall { webc_error, .. }
| genai::Error::WebAdapterCall { webc_error, .. } => match webc_error {
genai::webc::Error::Reqwest(err) => err.is_timeout() || err.is_connect(),
_ => false,
},
genai::Error::WebStream { .. } => true,
_ => false,
},
};
Self {
status,
transient,
source: Box::new(err),
}
}
}
pub const DEFAULT_MAX_ITERATIONS: usize = 5;
#[derive(Clone)]
pub struct Agent {
client: Client,
system: String,
model: Option<String>,
toolbox: Option<Arc<dyn ToolBox>>,
max_iterations: usize,
chat_options: ChatOptions,
}
impl Agent {
pub fn new(model: &str, system: &str) -> Result<Self, AgentError> {
let client =
Client::new().map_err(|err| AgentError::ClientInit(LlmError::from_genai(err)))?;
Ok(Self::from_parts(client, Some(model.to_string()), system))
}
pub fn from_client(client: Client, model: &str, system: &str) -> Self {
Self::from_parts(client, Some(model.to_string()), system)
}
fn from_parts(client: Client, model: Option<String>, system: &str) -> Self {
Self {
client,
system: system.trim().to_string(),
model,
toolbox: None,
max_iterations: DEFAULT_MAX_ITERATIONS,
chat_options: ChatOptions::default(),
}
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn with_system(mut self, system: impl Into<String>) -> Self {
self.system = system.into().trim().to_string();
self
}
pub fn with_toolbox(self, toolbox: impl ToolBox) -> Self {
self.with_toolbox_arc(Arc::new(toolbox))
}
pub fn with_toolbox_arc(mut self, toolbox: Arc<dyn ToolBox>) -> Self {
self.toolbox = Some(toolbox);
self
}
pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
self.max_iterations = max_iterations;
self
}
pub fn with_chat_options(mut self, chat_options: ChatOptions) -> Self {
self.chat_options = chat_options;
self
}
pub fn system(&self) -> &str {
&self.system
}
pub fn model(&self) -> Option<&str> {
self.model.as_deref()
}
pub fn toolbox(&self) -> Option<&Arc<dyn ToolBox>> {
self.toolbox.as_ref()
}
pub fn max_iterations(&self) -> usize {
self.max_iterations
}
pub fn chat_options(&self) -> &ChatOptions {
&self.chat_options
}
pub fn run<D>(&self, prompt: impl IntoPrompt) -> AgentRequest<'_, D, Complete> {
AgentRequest::new(self, prompt.into_prompt())
}
pub fn run_stream<D>(&self, prompt: impl IntoPrompt) -> AgentRequest<'_, D, Streaming> {
AgentRequest::new(self, prompt.into_prompt())
}
}
impl Default for Agent {
fn default() -> Self {
let client = Client::new().expect("failed to create the LLM client");
Self::from_parts(client, None, "")
}
}