pub mod sqlite;
pub mod memory;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::core::error::StorageError;
use crate::core::session::Session;
pub use sqlite::SqliteStorage;
pub use memory::MemoryStorage;
#[async_trait]
pub trait Storage: Send + Sync {
async fn create_session(&self, session: &Session) -> Result<String, StorageError>;
async fn get_session(&self, id: &str) -> Result<Option<Session>, StorageError>;
async fn update_session(&self, session: &Session) -> Result<(), StorageError>;
async fn delete_session(&self, id: &str) -> Result<(), StorageError>;
async fn list_sessions(&self) -> Result<Vec<Session>, StorageError>;
async fn save_message(&self, message: &Message) -> Result<String, StorageError>;
async fn get_messages(&self, session_id: &str) -> Result<Vec<Message>, StorageError>;
async fn delete_message(&self, id: &str) -> Result<(), StorageError>;
async fn search_messages(&self, query: &str) -> Result<Vec<Message>, StorageError>;
async fn get_statistics(&self) -> Result<StorageStatistics, StorageError>;
async fn cleanup(&self, older_than: DateTime<Utc>) -> Result<u64, StorageError>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub id: String,
pub session_id: String,
pub role: MessageRole,
pub content: MessageContent,
pub timestamp: DateTime<Utc>,
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum MessageRole {
User,
Assistant,
System,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageContent {
Text(String),
Structured(Vec<ContentPart>),
ToolCall {
name: String,
parameters: serde_json::Value,
id: String,
},
ToolResult {
result: String,
tool_call_id: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ContentPart {
Text {
text: String,
},
Image {
data: String,
mime_type: String,
description: Option<String>,
},
File {
path: String,
content: String,
mime_type: String,
},
Code {
language: String,
code: String,
},
ToolCall {
name: String,
parameters: serde_json::Value,
},
ToolResult {
call_id: String,
result: String,
success: bool,
},
}
#[derive(Debug, Clone)]
pub struct StorageStatistics {
pub total_sessions: u64,
pub total_messages: u64,
pub storage_size: u64,
pub oldest_session: Option<DateTime<Utc>>,
pub newest_session: Option<DateTime<Utc>>,
pub avg_messages_per_session: f64,
}
impl MessageContent {
pub fn as_text(&self) -> String {
match self {
MessageContent::Text(text) => text.clone(),
MessageContent::Structured(parts) => {
parts
.iter()
.filter_map(|part| match part {
ContentPart::Text { text } => Some(text.clone()),
ContentPart::Code { code, .. } => Some(code.clone()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
MessageContent::ToolCall { name, parameters, .. } => {
format!("Tool call: {} with parameters: {}", name, parameters)
}
MessageContent::ToolResult { result, .. } => result.clone(),
}
}
pub fn has_images(&self) -> bool {
match self {
MessageContent::Text(_) => false,
MessageContent::Structured(parts) => {
parts.iter().any(|part| matches!(part, ContentPart::Image { .. }))
}
MessageContent::ToolCall { .. } | MessageContent::ToolResult { .. } => false,
}
}
pub fn has_files(&self) -> bool {
match self {
MessageContent::Text(_) => false,
MessageContent::Structured(parts) => {
parts.iter().any(|part| matches!(part, ContentPart::File { .. }))
}
MessageContent::ToolCall { .. } | MessageContent::ToolResult { .. } => false,
}
}
pub fn has_tool_calls(&self) -> bool {
match self {
MessageContent::Text(_) => false,
MessageContent::Structured(parts) => {
parts.iter().any(|part| matches!(part, ContentPart::ToolCall { .. }))
}
MessageContent::ToolCall { .. } => true,
MessageContent::ToolResult { .. } => false,
}
}
pub fn get_tool_calls(&self) -> Vec<&ContentPart> {
match self {
MessageContent::Text(_) => Vec::new(),
MessageContent::Structured(parts) => {
parts
.iter()
.filter(|part| matches!(part, ContentPart::ToolCall { .. }))
.collect()
}
MessageContent::ToolCall { .. } | MessageContent::ToolResult { .. } => Vec::new(),
}
}
}
impl std::fmt::Display for MessageRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageRole::User => write!(f, "user"),
MessageRole::Assistant => write!(f, "assistant"),
MessageRole::System => write!(f, "system"),
MessageRole::Tool => write!(f, "tool"),
}
}
}
impl std::str::FromStr for MessageRole {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"user" => Ok(MessageRole::User),
"assistant" => Ok(MessageRole::Assistant),
"system" => Ok(MessageRole::System),
"tool" => Ok(MessageRole::Tool),
_ => Err(format!("Unknown message role: {}", s)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_content_as_text() {
let text_content = MessageContent::Text("Hello, world!".to_string());
assert_eq!(text_content.as_text(), "Hello, world!");
let structured_content = MessageContent::Structured(vec![
ContentPart::Text { text: "Hello".to_string() },
ContentPart::Code {
language: "rust".to_string(),
code: "println!(\"world\");".to_string()
},
]);
assert_eq!(structured_content.as_text(), "Hello\nprintln!(\"world\");");
}
#[test]
fn test_message_content_has_images() {
let text_content = MessageContent::Text("Hello".to_string());
assert!(!text_content.has_images());
let image_content = MessageContent::Structured(vec![
ContentPart::Text { text: "Look at this:".to_string() },
ContentPart::Image {
data: "base64data".to_string(),
mime_type: "image/png".to_string(),
description: None,
},
]);
assert!(image_content.has_images());
}
#[test]
fn test_message_role_display() {
assert_eq!(MessageRole::User.to_string(), "user");
assert_eq!(MessageRole::Assistant.to_string(), "assistant");
assert_eq!(MessageRole::System.to_string(), "system");
assert_eq!(MessageRole::Tool.to_string(), "tool");
}
#[test]
fn test_message_role_from_str() {
assert_eq!("user".parse::<MessageRole>().unwrap(), MessageRole::User);
assert_eq!("ASSISTANT".parse::<MessageRole>().unwrap(), MessageRole::Assistant);
assert!("invalid".parse::<MessageRole>().is_err());
}
#[test]
fn test_message_content_tool_calls() {
let content = MessageContent::Structured(vec![
ContentPart::Text { text: "I'll help you with that.".to_string() },
ContentPart::ToolCall {
name: "file_read".to_string(),
parameters: serde_json::json!({"path": "test.txt"}),
},
]);
assert!(content.has_tool_calls());
assert_eq!(content.get_tool_calls().len(), 1);
}
}