use crate::tool::{ToolBox, ToolError};
use genai::Client;
use genai::resolver::{AuthData, Endpoint};
use std::sync::Arc;
use thiserror::Error;
mod core;
mod model;
mod request;
mod stream;
pub use model::{ChatOptions, Provider, 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("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;
pub const DEFAULT_TEMPERATURE: f64 = 0.2;
#[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(system: &str) -> Result<Self, AgentError> {
let client =
Client::new().map_err(|err| AgentError::ClientInit(LlmError::from_genai(err)))?;
Ok(Self::from_client(client, system))
}
pub fn new_with_url(base_url: &str, api_key: &str, system: &str) -> Result<Self, AgentError> {
Self::new_with_provider(Provider::OpenAI, Some(base_url), api_key, system)
}
pub fn new_with_provider(
provider: Provider,
base_url: Option<&str>,
api_key: &str,
system: &str,
) -> Result<Self, AgentError> {
let adapter = provider.into_genai();
let auth = AuthData::from_single(api_key);
let builder = Client::builder().with_adapter_kind(adapter);
let builder = match base_url {
Some(base_url) => builder.append_provider_config(
adapter,
(Endpoint::from_owned(base_url.to_string()), auth),
),
None => builder.append_provider_config(adapter, auth),
};
let client = builder
.build()
.map_err(|err| AgentError::ClientInit(LlmError::from_genai(err)))?;
Ok(Self::from_client(client, system))
}
fn from_client(client: Client, system: &str) -> Self {
Self {
client,
system: system.trim().to_string(),
model: None,
toolbox: None,
max_iterations: DEFAULT_MAX_ITERATIONS,
chat_options: ChatOptions::default().with_temperature(DEFAULT_TEMPERATURE),
}
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
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 Into<String>) -> AgentRequest<'_, D, Complete> {
AgentRequest::new(self, prompt.into())
}
pub fn run_stream<D>(&self, prompt: impl Into<String>) -> AgentRequest<'_, D, Streaming> {
AgentRequest::new(self, prompt.into())
}
}