pub mod anthropic;
pub mod mock;
pub mod ollama;
pub mod openai;
pub mod retry;
use std::time::Duration;
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub(crate) fn http_client(timeout: Duration) -> reqwest::Client {
reqwest::Client::builder()
.timeout(timeout)
.connect_timeout(Duration::from_secs(10))
.build()
.unwrap_or_else(|_| reqwest::Client::new())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: String,
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
impl Message {
pub fn system(content: impl Into<String>) -> Self {
Self::plain("system", content)
}
pub fn user(content: impl Into<String>) -> Self {
Self::plain("user", content)
}
pub fn assistant(content: impl Into<String>) -> Self {
Self::plain("assistant", content)
}
fn plain(role: &str, content: impl Into<String>) -> Self {
Self { role: role.into(), content: content.into(), tool_calls: Vec::new(), tool_call_id: None }
}
pub fn assistant_tool_calls(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
Self { role: "assistant".into(), content: content.into(), tool_calls, tool_call_id: None }
}
pub fn tool_result(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: "tool".into(),
content: content.into(),
tool_calls: Vec::new(),
tool_call_id: Some(tool_call_id.into()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSchema {
pub name: String,
pub description: String,
pub parameters: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub args: Value,
}
#[derive(Debug, Clone, Default)]
pub struct LlmResponse {
pub text: Option<String>,
pub tool_calls: Vec<ToolCall>,
}
impl LlmResponse {
pub fn text(text: impl Into<String>) -> Self {
Self { text: Some(text.into()), tool_calls: Vec::new() }
}
pub fn is_tool_call(&self) -> bool {
!self.tool_calls.is_empty()
}
}
#[async_trait]
pub trait LlmService: Send + Sync {
async fn submit_prompt(&self, messages: Vec<Message>) -> Result<String>;
async fn chat(&self, messages: Vec<Message>, _tools: &[ToolSchema]) -> Result<LlmResponse> {
let text = self.submit_prompt(messages).await?;
Ok(LlmResponse::text(text))
}
}