pub mod anthropic;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::borrow::Cow;
#[derive(Debug, Clone)]
pub struct ToolSpec {
pub name: Cow<'static, str>,
pub description: Cow<'static, str>,
pub input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub input: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ContentPart {
Text(String),
ToolUse {
id: String,
name: String,
input: Value,
},
ToolResult {
tool_use_id: String,
content: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: Role,
pub parts: Vec<ContentPart>,
}
impl ChatMessage {
pub fn user(text: impl Into<String>) -> Self {
Self {
role: Role::User,
parts: vec![ContentPart::Text(text.into())],
}
}
pub fn assistant(text: impl Into<String>) -> Self {
Self {
role: Role::Assistant,
parts: vec![ContentPart::Text(text.into())],
}
}
pub fn assistant_with_tools(text: Option<String>, tool_calls: Vec<ToolCall>) -> Self {
let mut parts = Vec::new();
if let Some(t) = text.filter(|s| !s.is_empty()) {
parts.push(ContentPart::Text(t));
}
for call in tool_calls {
parts.push(ContentPart::ToolUse {
id: call.id,
name: call.name,
input: call.input,
});
}
Self {
role: Role::Assistant,
parts,
}
}
pub fn tool_results(results: Vec<(String, String)>) -> Self {
let parts = results
.into_iter()
.map(|(id, content)| ContentPart::ToolResult {
tool_use_id: id,
content,
})
.collect();
Self {
role: Role::User,
parts,
}
}
pub fn text(&self) -> Option<String> {
let texts: Vec<&str> = self
.parts
.iter()
.filter_map(|p| {
if let ContentPart::Text(t) = p {
Some(t.as_str())
} else {
None
}
})
.collect();
if texts.is_empty() {
None
} else {
Some(texts.join(""))
}
}
}
#[derive(Debug, Clone)]
pub struct ChatResponse {
pub text: Option<String>,
pub tool_calls: Vec<ToolCall>,
}
impl ChatResponse {
pub fn text_only(text: String) -> Self {
Self {
text: Some(text),
tool_calls: vec![],
}
}
pub fn has_tool_calls(&self) -> bool {
!self.tool_calls.is_empty()
}
pub fn text_or_empty(&self) -> &str {
self.text.as_deref().unwrap_or("")
}
}
#[async_trait]
pub trait Provider: Send + Sync {
fn name(&self) -> &str;
async fn chat(
&self,
system: Option<&str>,
messages: &[ChatMessage],
tools: Option<&[ToolSpec]>,
) -> anyhow::Result<ChatResponse>;
}