use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
#[async_trait]
pub trait LLMProvider: Send + Sync {
fn name(&self) -> &str;
async fn chat(&self, messages: Vec<ChatMessage>) -> anyhow::Result<String>;
async fn chat_stream(
&self,
messages: Vec<ChatMessage>,
tx: mpsc::Sender<String>,
) -> anyhow::Result<()> {
let response = self.chat(messages).await?;
tx.send(response)
.await
.map_err(|e| anyhow::anyhow!("Failed to send: {}", e))?;
Ok(())
}
fn model_info(&self) -> Option<ModelInfo> {
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: String,
pub content: String,
}
impl ChatMessage {
pub fn system(content: impl Into<String>) -> Self {
Self {
role: "system".to_string(),
content: content.into(),
}
}
pub fn user(content: impl Into<String>) -> Self {
Self {
role: "user".to_string(),
content: content.into(),
}
}
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: "assistant".to_string(),
content: content.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
pub name: String,
pub version: Option<String>,
pub context_window: Option<usize>,
pub max_output_tokens: Option<usize>,
}
pub fn parse_llm_json<T: serde::de::DeserializeOwned>(response: &str) -> anyhow::Result<T> {
let json_str = extract_json_block(response).unwrap_or(response);
serde_json::from_str(json_str).map_err(|e| anyhow::anyhow!("JSON parse error: {}", e))
}
pub fn extract_json_block(text: &str) -> Option<&str> {
if let Some(start) = text.find("```json") {
let content = &text[start + 7..];
if let Some(end) = content.find("```") {
return Some(content[..end].trim());
}
}
if let Some(start) = text.find("```") {
let content = &text[start + 3..];
if let Some(end) = content.find("```") {
let block = content[..end].trim();
if block.starts_with('{') || block.starts_with('[') {
return Some(block);
}
}
}
if let Some(start) = text.find('{')
&& let Some(end) = text.rfind('}')
&& end > start
{
return Some(&text[start..=end]);
}
if let Some(start) = text.find('[')
&& let Some(end) = text.rfind(']')
&& end > start
{
return Some(&text[start..=end]);
}
None
}
#[derive(Debug, Clone, Default)]
pub struct ConversationHistory {
messages: Vec<ChatMessage>,
max_messages: Option<usize>,
}
impl ConversationHistory {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_messages(mut self, max: usize) -> Self {
self.max_messages = Some(max);
self
}
pub fn push(&mut self, message: ChatMessage) {
self.messages.push(message);
if let Some(max) = self.max_messages {
while self.messages.len() > max {
if let Some(idx) = self.messages.iter().position(|m| m.role != "system") {
self.messages.remove(idx);
} else {
break;
}
}
}
}
pub fn add_system(&mut self, content: impl Into<String>) {
self.push(ChatMessage::system(content));
}
pub fn add_user(&mut self, content: impl Into<String>) {
self.push(ChatMessage::user(content));
}
pub fn add_assistant(&mut self, content: impl Into<String>) {
self.push(ChatMessage::assistant(content));
}
pub fn messages(&self) -> &[ChatMessage] {
&self.messages
}
pub fn to_vec(&self) -> Vec<ChatMessage> {
self.messages.clone()
}
pub fn clear_except_system(&mut self) {
self.messages.retain(|m| m.role == "system");
}
pub fn clear(&mut self) {
self.messages.clear();
}
pub fn len(&self) -> usize {
self.messages.len()
}
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chat_message() {
let system = ChatMessage::system("You are a helpful assistant.");
assert_eq!(system.role, "system");
let user = ChatMessage::user("Hello!");
assert_eq!(user.role, "user");
let assistant = ChatMessage::assistant("Hi there!");
assert_eq!(assistant.role, "assistant");
}
#[test]
fn test_extract_json_block() {
let text = r#"Here is the result:
```json
{"name": "test", "value": 42}
```
That's all."#;
let json = extract_json_block(text).unwrap();
assert_eq!(json, r#"{"name": "test", "value": 42}"#);
let text2 = r#"The result is {"name": "test"}"#;
let json2 = extract_json_block(text2).unwrap();
assert_eq!(json2, r#"{"name": "test"}"#);
}
#[test]
fn test_conversation_history() {
let mut history = ConversationHistory::new().with_max_messages(5);
history.add_system("System prompt");
history.add_user("Hello");
history.add_assistant("Hi");
history.add_user("How are you?");
history.add_assistant("I'm fine");
assert_eq!(history.len(), 5);
history.add_user("Another message");
assert_eq!(history.len(), 5);
assert_eq!(history.messages()[0].role, "system");
}
}