use std::collections::HashMap;
use std::fmt;
use async_trait::async_trait;
use futures::stream::{Stream, StreamExt};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{error::LLMError, ToolCall};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChatRole {
User,
Assistant,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ImageMime {
JPEG,
PNG,
GIF,
WEBP,
}
impl ImageMime {
pub fn mime_type(&self) -> &'static str {
match self {
ImageMime::JPEG => "image/jpeg",
ImageMime::PNG => "image/png",
ImageMime::GIF => "image/gif",
ImageMime::WEBP => "image/webp",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum MessageType {
#[default]
Text,
Image((ImageMime, Vec<u8>)),
Pdf(Vec<u8>),
ImageURL(String),
ToolUse(Vec<ToolCall>),
ToolResult(Vec<ToolCall>),
}
pub enum ReasoningEffort {
Low,
Medium,
High,
}
#[derive(Debug, Clone)]
pub struct ChatMessage {
pub role: ChatRole,
pub message_type: MessageType,
pub content: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ParameterProperty {
#[serde(rename = "type")]
pub property_type: String,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Box<ParameterProperty>>,
#[serde(skip_serializing_if = "Option::is_none", rename = "enum")]
pub enum_list: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ParametersSchema {
#[serde(rename = "type")]
pub schema_type: String,
pub properties: HashMap<String, ParameterProperty>,
pub required: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct FunctionTool {
pub name: String,
pub description: String,
pub parameters: Value,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct StructuredOutputFormat {
pub name: String,
pub description: Option<String>,
pub schema: Option<Value>,
pub strict: Option<bool>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Tool {
#[serde(rename = "type")]
pub tool_type: String,
pub function: FunctionTool,
}
#[derive(Debug, Clone, Default)]
pub enum ToolChoice {
Any,
#[default]
Auto,
Tool(String),
None,
}
impl Serialize for ToolChoice {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
ToolChoice::Any => serializer.serialize_str("required"),
ToolChoice::Auto => serializer.serialize_str("auto"),
ToolChoice::None => serializer.serialize_str("none"),
ToolChoice::Tool(name) => {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("type", "function")?;
let mut function_obj = std::collections::HashMap::new();
function_obj.insert("name", name.as_str());
map.serialize_entry("function", &function_obj)?;
map.end()
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
pub trait ChatResponse: std::fmt::Debug + std::fmt::Display {
fn text(&self) -> Option<String>;
fn tool_calls(&self) -> Option<Vec<ToolCall>>;
fn thinking(&self) -> Option<String> {
None
}
fn usage(&self) -> Option<Usage> {
None
}
}
#[async_trait]
pub trait ChatProvider: Sync + Send {
async fn chat(&self, messages: &[ChatMessage]) -> Result<Box<dyn ChatResponse>, LLMError> {
self.chat_with_tools(messages, None).await
}
async fn chat_with_tools(
&self,
messages: &[ChatMessage],
tools: Option<&[Tool]>,
) -> Result<Box<dyn ChatResponse>, LLMError>;
async fn chat_stream(
&self,
_messages: &[ChatMessage],
) -> Result<std::pin::Pin<Box<dyn Stream<Item = Result<String, LLMError>> + Send>>, LLMError>
{
Err(LLMError::Generic(
"Streaming not supported for this provider".to_string(),
))
}
async fn memory_contents(&self) -> Option<Vec<ChatMessage>> {
None
}
async fn summarize_history(&self, msgs: &[ChatMessage]) -> Result<String, LLMError> {
let prompt = format!(
"Summarize in 2-3 sentences:\n{}",
msgs.iter()
.map(|m| format!("{:?}: {}", m.role, m.content))
.collect::<Vec<_>>()
.join("\n"),
);
let req = [ChatMessage::user().content(prompt).build()];
self.chat(&req)
.await?
.text()
.ok_or(LLMError::Generic("no text in summary response".into()))
}
}
impl fmt::Display for ReasoningEffort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReasoningEffort::Low => write!(f, "low"),
ReasoningEffort::Medium => write!(f, "medium"),
ReasoningEffort::High => write!(f, "high"),
}
}
}
impl ChatMessage {
pub fn user() -> ChatMessageBuilder {
ChatMessageBuilder::new(ChatRole::User)
}
pub fn assistant() -> ChatMessageBuilder {
ChatMessageBuilder::new(ChatRole::Assistant)
}
}
#[derive(Debug)]
pub struct ChatMessageBuilder {
role: ChatRole,
message_type: MessageType,
content: String,
}
impl ChatMessageBuilder {
pub fn new(role: ChatRole) -> Self {
Self {
role,
message_type: MessageType::default(),
content: String::new(),
}
}
pub fn content<S: Into<String>>(mut self, content: S) -> Self {
self.content = content.into();
self
}
pub fn image(mut self, image_mime: ImageMime, raw_bytes: Vec<u8>) -> Self {
self.message_type = MessageType::Image((image_mime, raw_bytes));
self
}
pub fn pdf(mut self, raw_bytes: Vec<u8>) -> Self {
self.message_type = MessageType::Pdf(raw_bytes);
self
}
pub fn image_url(mut self, url: impl Into<String>) -> Self {
self.message_type = MessageType::ImageURL(url.into());
self
}
pub fn tool_use(mut self, tools: Vec<ToolCall>) -> Self {
self.message_type = MessageType::ToolUse(tools);
self
}
pub fn tool_result(mut self, tools: Vec<ToolCall>) -> Self {
self.message_type = MessageType::ToolResult(tools);
self
}
pub fn build(self) -> ChatMessage {
ChatMessage {
role: self.role,
message_type: self.message_type,
content: self.content,
}
}
}
pub(crate) fn create_sse_stream<F>(
response: reqwest::Response,
parser: F,
) -> std::pin::Pin<Box<dyn Stream<Item = Result<String, LLMError>> + Send>>
where
F: Fn(&str) -> Result<Option<String>, LLMError> + Send + 'static,
{
let stream = response
.bytes_stream()
.map(move |chunk| match chunk {
Ok(bytes) => {
let text = String::from_utf8_lossy(&bytes);
parser(&text)
}
Err(e) => Err(LLMError::HttpError(e.to_string())),
})
.filter_map(|result| async move {
match result {
Ok(Some(content)) => Some(Ok(content)),
Ok(None) => None,
Err(e) => Some(Err(e)),
}
});
Box::pin(stream)
}