use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NormalizedMessage {
pub id: Uuid,
pub role: MessageRole,
pub content: String,
pub source_provider: String,
pub source_model: Option<String>,
pub attachments: Vec<Attachment>,
pub tool_calls: Vec<ToolCall>,
pub token_count: Option<usize>,
pub timestamp: DateTime<Utc>,
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
User,
Assistant,
System,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attachment {
pub id: Uuid,
pub attachment_type: AttachmentType,
pub name: Option<String>,
pub mime_type: String,
pub content: String,
pub url: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AttachmentType {
Image,
File,
Code,
Audio,
Video,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
pub result: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationContext {
pub id: Uuid,
pub title: String,
pub system_prompt: Option<String>,
pub messages: Vec<NormalizedMessage>,
pub summary: Option<ConversationSummary>,
pub tools: Vec<ToolDefinition>,
pub provider_history: Vec<ProviderSwitch>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationSummary {
pub text: String,
pub topics: Vec<String>,
pub entities: Vec<String>,
pub goals: Vec<String>,
pub up_to_message_id: Uuid,
pub generated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderSwitch {
pub from_provider: String,
pub from_model: Option<String>,
pub to_provider: String,
pub to_model: Option<String>,
pub reason: Option<String>,
pub switched_at: DateTime<Utc>,
}
pub trait ProviderAdapter: Send + Sync {
fn provider_name(&self) -> &str;
fn to_provider_format(&self, context: &ConversationContext) -> ProviderMessages;
fn from_provider_format(&self, response: &ProviderResponse) -> NormalizedMessage;
fn capabilities(&self) -> ProviderCapabilities;
fn estimate_tokens(&self, messages: &[NormalizedMessage]) -> usize;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderMessages {
pub messages: Vec<serde_json::Value>,
pub system: Option<String>,
pub tools: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderResponse {
pub provider: String,
pub model: String,
pub content: String,
pub tool_calls: Vec<ToolCall>,
pub usage: Option<UsageStats>,
pub raw: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageStats {
pub prompt_tokens: usize,
pub completion_tokens: usize,
pub total_tokens: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderCapabilities {
pub vision: bool,
pub tools: bool,
pub system_messages: bool,
pub max_context: usize,
pub streaming: bool,
}
pub struct OpenAIAdapter;
impl ProviderAdapter for OpenAIAdapter {
fn provider_name(&self) -> &str {
"openai"
}
fn to_provider_format(&self, context: &ConversationContext) -> ProviderMessages {
let mut messages: Vec<serde_json::Value> = vec![];
if let Some(ref system) = context.system_prompt {
messages.push(serde_json::json!({
"role": "system",
"content": system
}));
}
for msg in &context.messages {
let role = match msg.role {
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
MessageRole::System => "system",
MessageRole::Tool => "tool",
};
let mut message = serde_json::json!({
"role": role,
"content": msg.content
});
if !msg.tool_calls.is_empty() && msg.role == MessageRole::Assistant {
message["tool_calls"] = serde_json::json!(msg.tool_calls.iter().map(|tc| {
serde_json::json!({
"id": tc.id,
"type": "function",
"function": {
"name": tc.name,
"arguments": tc.arguments.to_string()
}
})
}).collect::<Vec<_>>());
}
if !msg.attachments.is_empty() {
let content_parts: Vec<serde_json::Value> = std::iter::once(
serde_json::json!({ "type": "text", "text": msg.content })
).chain(msg.attachments.iter().filter(|a| a.attachment_type == AttachmentType::Image).map(|a| {
if let Some(ref url) = a.url {
serde_json::json!({
"type": "image_url",
"image_url": { "url": url }
})
} else {
serde_json::json!({
"type": "image_url",
"image_url": { "url": format!("data:{};base64,{}", a.mime_type, a.content) }
})
}
})).collect();
message["content"] = serde_json::json!(content_parts);
}
messages.push(message);
}
let tools = if !context.tools.is_empty() {
Some(context.tools.iter().map(|t| {
serde_json::json!({
"type": "function",
"function": {
"name": t.name,
"description": t.description,
"parameters": t.parameters
}
})
}).collect())
} else {
None
};
ProviderMessages {
messages,
system: None, tools,
}
}
fn from_provider_format(&self, response: &ProviderResponse) -> NormalizedMessage {
NormalizedMessage {
id: Uuid::new_v4(),
role: MessageRole::Assistant,
content: response.content.clone(),
source_provider: "openai".to_string(),
source_model: Some(response.model.clone()),
attachments: vec![],
tool_calls: response.tool_calls.clone(),
token_count: response.usage.as_ref().map(|u| u.completion_tokens),
timestamp: Utc::now(),
metadata: HashMap::new(),
}
}
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities {
vision: true,
tools: true,
system_messages: true,
max_context: 128000,
streaming: true,
}
}
fn estimate_tokens(&self, messages: &[NormalizedMessage]) -> usize {
messages.iter().map(|m| m.content.len() / 4).sum()
}
}
pub struct AnthropicAdapter;
impl ProviderAdapter for AnthropicAdapter {
fn provider_name(&self) -> &str {
"anthropic"
}
fn to_provider_format(&self, context: &ConversationContext) -> ProviderMessages {
let mut messages: Vec<serde_json::Value> = vec![];
for msg in &context.messages {
let role = match msg.role {
MessageRole::User | MessageRole::Tool => "user",
MessageRole::Assistant => "assistant",
MessageRole::System => continue, };
let mut content_parts: Vec<serde_json::Value> = vec![];
content_parts.push(serde_json::json!({
"type": "text",
"text": msg.content
}));
for attachment in &msg.attachments {
if attachment.attachment_type == AttachmentType::Image {
content_parts.push(serde_json::json!({
"type": "image",
"source": {
"type": "base64",
"media_type": attachment.mime_type,
"data": attachment.content
}
}));
}
}
messages.push(serde_json::json!({
"role": role,
"content": content_parts
}));
}
let tools = if !context.tools.is_empty() {
Some(context.tools.iter().map(|t| {
serde_json::json!({
"name": t.name,
"description": t.description,
"input_schema": t.parameters
})
}).collect())
} else {
None
};
ProviderMessages {
messages,
system: context.system_prompt.clone(),
tools,
}
}
fn from_provider_format(&self, response: &ProviderResponse) -> NormalizedMessage {
NormalizedMessage {
id: Uuid::new_v4(),
role: MessageRole::Assistant,
content: response.content.clone(),
source_provider: "anthropic".to_string(),
source_model: Some(response.model.clone()),
attachments: vec![],
tool_calls: response.tool_calls.clone(),
token_count: response.usage.as_ref().map(|u| u.completion_tokens),
timestamp: Utc::now(),
metadata: HashMap::new(),
}
}
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities {
vision: true,
tools: true,
system_messages: true,
max_context: 200000,
streaming: true,
}
}
fn estimate_tokens(&self, messages: &[NormalizedMessage]) -> usize {
messages.iter().map(|m| m.content.len() / 4).sum()
}
}
pub struct ContinuationManager {
adapters: HashMap<String, Box<dyn ProviderAdapter>>,
contexts: HashMap<Uuid, ConversationContext>,
}
impl ContinuationManager {
pub fn new() -> Self {
let mut adapters: HashMap<String, Box<dyn ProviderAdapter>> = HashMap::new();
adapters.insert("openai".to_string(), Box::new(OpenAIAdapter));
adapters.insert("anthropic".to_string(), Box::new(AnthropicAdapter));
Self {
adapters,
contexts: HashMap::new(),
}
}
pub fn register_adapter(&mut self, adapter: Box<dyn ProviderAdapter>) {
self.adapters.insert(adapter.provider_name().to_string(), adapter);
}
pub fn create_context(&mut self, title: &str, system_prompt: Option<&str>) -> Uuid {
let id = Uuid::new_v4();
let context = ConversationContext {
id,
title: title.to_string(),
system_prompt: system_prompt.map(String::from),
messages: vec![],
summary: None,
tools: vec![],
provider_history: vec![],
created_at: Utc::now(),
updated_at: Utc::now(),
metadata: HashMap::new(),
};
self.contexts.insert(id, context);
id
}
pub fn add_message(&mut self, context_id: Uuid, message: NormalizedMessage) -> bool {
if let Some(context) = self.contexts.get_mut(&context_id) {
context.messages.push(message);
context.updated_at = Utc::now();
true
} else {
false
}
}
pub fn switch_provider(
&mut self,
context_id: Uuid,
to_provider: &str,
to_model: Option<&str>,
reason: Option<&str>,
) -> Option<ProviderMessages> {
let context = self.contexts.get_mut(&context_id)?;
let adapter = self.adapters.get(to_provider)?;
let last_provider = context.provider_history.last();
let switch = ProviderSwitch {
from_provider: last_provider.map(|p| p.to_provider.clone()).unwrap_or_default(),
from_model: last_provider.and_then(|p| p.to_model.clone()),
to_provider: to_provider.to_string(),
to_model: to_model.map(String::from),
reason: reason.map(String::from),
switched_at: Utc::now(),
};
context.provider_history.push(switch);
context.updated_at = Utc::now();
Some(adapter.to_provider_format(context))
}
pub fn get_provider_messages(&self, context_id: Uuid, provider: &str) -> Option<ProviderMessages> {
let context = self.contexts.get(&context_id)?;
let adapter = self.adapters.get(provider)?;
Some(adapter.to_provider_format(context))
}
pub fn process_response(&mut self, context_id: Uuid, response: &ProviderResponse) -> Option<NormalizedMessage> {
let adapter = self.adapters.get(&response.provider)?;
let message = adapter.from_provider_format(response);
if let Some(context) = self.contexts.get_mut(&context_id) {
context.messages.push(message.clone());
context.updated_at = Utc::now();
}
Some(message)
}
pub fn get_context(&self, context_id: Uuid) -> Option<&ConversationContext> {
self.contexts.get(&context_id)
}
pub fn estimate_tokens(&self, context_id: Uuid, provider: &str) -> Option<usize> {
let context = self.contexts.get(&context_id)?;
let adapter = self.adapters.get(provider)?;
Some(adapter.estimate_tokens(&context.messages))
}
pub fn compress_context(&mut self, context_id: Uuid, summary_text: &str, topics: Vec<String>) -> bool {
if let Some(context) = self.contexts.get_mut(&context_id) {
let last_message_id = context.messages.last().map(|m| m.id).unwrap_or(Uuid::nil());
context.summary = Some(ConversationSummary {
text: summary_text.to_string(),
topics,
entities: vec![],
goals: vec![],
up_to_message_id: last_message_id,
generated_at: Utc::now(),
});
context.updated_at = Utc::now();
true
} else {
false
}
}
}
impl Default for ContinuationManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_context() {
let mut manager = ContinuationManager::new();
let id = manager.create_context("Test Conversation", Some("You are helpful."));
let context = manager.get_context(id).unwrap();
assert_eq!(context.title, "Test Conversation");
assert_eq!(context.system_prompt.as_deref(), Some("You are helpful."));
}
#[test]
fn test_add_message() {
let mut manager = ContinuationManager::new();
let id = manager.create_context("Test", None);
let message = NormalizedMessage {
id: Uuid::new_v4(),
role: MessageRole::User,
content: "Hello!".to_string(),
source_provider: "openai".to_string(),
source_model: Some("gpt-4".to_string()),
attachments: vec![],
tool_calls: vec![],
token_count: None,
timestamp: Utc::now(),
metadata: HashMap::new(),
};
assert!(manager.add_message(id, message));
assert_eq!(manager.get_context(id).unwrap().messages.len(), 1);
}
#[test]
fn test_provider_switch() {
let mut manager = ContinuationManager::new();
let id = manager.create_context("Test", Some("System prompt"));
let message = NormalizedMessage {
id: Uuid::new_v4(),
role: MessageRole::User,
content: "Hello!".to_string(),
source_provider: "openai".to_string(),
source_model: None,
attachments: vec![],
tool_calls: vec![],
token_count: None,
timestamp: Utc::now(),
metadata: HashMap::new(),
};
manager.add_message(id, message);
let messages = manager.switch_provider(id, "anthropic", Some("claude-sonnet-4-20250514"), Some("Better for writing"));
assert!(messages.is_some());
let context = manager.get_context(id).unwrap();
assert_eq!(context.provider_history.len(), 1);
assert_eq!(context.provider_history[0].to_provider, "anthropic");
}
}