use chrono::Utc;
use echo_core::error::Result;
use echo_core::llm::types::Message;
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewConversation {
pub conversation_id: String,
#[serde(default = "default_user_id")]
pub user_id: String,
pub agent_type: Option<String>,
pub title: Option<String>,
}
fn default_user_id() -> String {
"default".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conversation {
pub id: i64,
pub conversation_id: String,
pub user_id: String,
pub agent_type: Option<String>,
pub title: Option<String>,
pub summary: Option<String>,
pub compressed_before_id: Option<i64>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationMeta {
pub id: i64,
pub conversation_id: String,
pub user_id: String,
pub title: Option<String>,
pub message_count: usize,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredMessage {
pub id: Option<i64>,
pub conversation_id: String,
pub role: String,
pub content: Option<String>,
pub attachments_json: Option<String>,
pub tool_calls_json: Option<String>,
pub tool_result_json: Option<String>,
pub created_at: String,
}
#[derive(Debug, Clone, Default)]
pub struct ConversationFilter {
pub user_id: Option<String>,
pub agent_type: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
pub trait ConversationStore: Send + Sync {
fn create_conversation<'a>(
&'a self,
conv: NewConversation,
) -> BoxFuture<'a, Result<Conversation>>;
fn get_conversation<'a>(
&'a self,
conversation_id: &'a str,
) -> BoxFuture<'a, Result<Option<Conversation>>>;
fn list_conversations<'a>(
&'a self,
filter: ConversationFilter,
) -> BoxFuture<'a, Result<Vec<ConversationMeta>>>;
fn update_conversation<'a>(
&'a self,
conversation_id: &'a str,
title: Option<&'a str>,
summary: Option<&'a str>,
compressed_before_id: Option<i64>,
) -> BoxFuture<'a, Result<()>>;
fn delete_conversation<'a>(&'a self, conversation_id: &'a str) -> BoxFuture<'a, Result<()>>;
fn save_messages<'a>(
&'a self,
conversation_id: &'a str,
messages: &'a [StoredMessage],
) -> BoxFuture<'a, Result<()>>;
fn get_messages<'a>(
&'a self,
conversation_id: &'a str,
) -> BoxFuture<'a, Result<Vec<StoredMessage>>>;
fn count_messages<'a>(&'a self, conversation_id: &'a str) -> BoxFuture<'a, Result<usize>>;
fn ensure_conversation<'a>(
&'a self,
conv: NewConversation,
) -> BoxFuture<'a, Result<Conversation>> {
Box::pin(async move {
let conversation_id = conv.conversation_id.clone();
if let Some(existing) = self.get_conversation(&conversation_id).await? {
Ok(existing)
} else {
self.create_conversation(conv).await
}
})
}
}
pub fn project_messages(conversation_id: &str, messages: &[Message]) -> Result<Vec<StoredMessage>> {
messages
.iter()
.map(|message| project_message(conversation_id, message))
.collect()
}
pub fn project_message(conversation_id: &str, message: &Message) -> Result<StoredMessage> {
let tool_calls_json = message
.tool_calls
.as_ref()
.map(serde_json::to_string)
.transpose()?;
let tool_result_json = if message.role == "tool" {
Some(
serde_json::json!({
"tool_call_id": message.tool_call_id,
"name": message.name,
})
.to_string(),
)
} else {
None
};
Ok(StoredMessage {
id: None,
conversation_id: conversation_id.to_string(),
role: message.role.clone(),
content: message.text_content(),
attachments_json: None,
tool_calls_json,
tool_result_json,
created_at: Utc::now().to_rfc3339(),
})
}