use crate::ai_studio::chat_orchestrator::{ChatContext, ChatMessage};
use crate::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs;
use tokio::sync::RwLock;
use uuid::Uuid;
pub struct ConversationStore {
cache: Arc<RwLock<HashMap<String, Conversation>>>,
storage_path: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conversation {
pub id: String,
pub workspace_id: Option<String>,
pub messages: Vec<ChatMessage>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub metadata: HashMap<String, serde_json::Value>,
}
impl Conversation {
pub fn new(workspace_id: Option<String>) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
workspace_id,
messages: Vec::new(),
created_at: now,
updated_at: now,
metadata: HashMap::new(),
}
}
pub fn add_message(&mut self, message: ChatMessage) {
self.messages.push(message);
self.updated_at = Utc::now();
}
pub fn to_context(&self) -> ChatContext {
ChatContext {
history: self.messages.clone(),
workspace_id: self.workspace_id.clone(),
}
}
}
impl ConversationStore {
pub fn new() -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
storage_path: None,
}
}
pub fn with_persistence<P: AsRef<Path>>(storage_path: P) -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
storage_path: Some(storage_path.as_ref().to_path_buf()),
}
}
pub async fn initialize(&self) -> Result<()> {
if let Some(ref path) = self.storage_path {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await.map_err(|e| {
crate::Error::io_with_context("create storage directory", e.to_string())
})?;
}
if path.exists() {
let content = fs::read_to_string(path).await.map_err(|e| {
crate::Error::io_with_context("read conversation store", e.to_string())
})?;
let conversations: Vec<Conversation> =
serde_json::from_str(&content).map_err(|e| {
crate::Error::io_with_context("parse conversation store", e.to_string())
})?;
let mut cache = self.cache.write().await;
for conv in conversations {
cache.insert(conv.id.clone(), conv);
}
}
}
Ok(())
}
async fn persist(&self) -> Result<()> {
if let Some(ref path) = self.storage_path {
let cache = self.cache.read().await;
let conversations: Vec<&Conversation> = cache.values().collect();
let content = serde_json::to_string_pretty(&conversations).map_err(|e| {
crate::Error::io_with_context("serialize conversations", e.to_string())
})?;
fs::write(path, content).await.map_err(|e| {
crate::Error::io_with_context("write conversation store", e.to_string())
})?;
}
Ok(())
}
pub async fn create_conversation(&self, workspace_id: Option<String>) -> Result<String> {
let conversation = Conversation::new(workspace_id);
let id = conversation.id.clone();
{
let mut cache = self.cache.write().await;
cache.insert(id.clone(), conversation);
}
self.persist().await?;
Ok(id)
}
pub async fn get_conversation(&self, id: &str) -> Result<Option<Conversation>> {
let cache = self.cache.read().await;
Ok(cache.get(id).cloned())
}
pub async fn add_message(&self, conversation_id: &str, message: ChatMessage) -> Result<()> {
let mut cache = self.cache.write().await;
if let Some(conversation) = cache.get_mut(conversation_id) {
conversation.add_message(message);
self.persist().await?;
Ok(())
} else {
Err(crate::Error::not_found("Conversation", conversation_id))
}
}
pub async fn get_context(&self, conversation_id: &str) -> Result<Option<ChatContext>> {
let conversation = self.get_conversation(conversation_id).await?;
Ok(conversation.map(|c| c.to_context()))
}
pub async fn list_conversations(
&self,
workspace_id: Option<&str>,
) -> Result<Vec<Conversation>> {
let cache = self.cache.read().await;
let conversations: Vec<Conversation> = cache
.values()
.filter(|conv| {
if let Some(wid) = workspace_id {
conv.workspace_id.as_deref() == Some(wid)
} else {
true
}
})
.cloned()
.collect();
Ok(conversations)
}
pub async fn delete_conversation(&self, conversation_id: &str) -> Result<()> {
let mut cache = self.cache.write().await;
cache.remove(conversation_id);
self.persist().await?;
Ok(())
}
pub async fn clear_old_conversations(&self, days: u64) -> Result<usize> {
let cutoff = Utc::now() - chrono::Duration::days(days as i64);
let mut cache = self.cache.write().await;
let mut removed = 0;
cache.retain(|_, conv| {
if conv.updated_at < cutoff {
removed += 1;
false
} else {
true
}
});
if removed > 0 {
self.persist().await?;
}
Ok(removed)
}
}
impl Default for ConversationStore {
fn default() -> Self {
Self::new()
}
}
static CONVERSATION_STORE: once_cell::sync::Lazy<Arc<ConversationStore>> =
once_cell::sync::Lazy::new(|| {
let storage_path = dirs::home_dir()
.map(|home| home.join(".mockforge").join("conversations.json"))
.or_else(|| Some(PathBuf::from(".mockforge/conversations.json")));
if let Some(path) = storage_path {
Arc::new(ConversationStore::with_persistence(path))
} else {
Arc::new(ConversationStore::new())
}
});
pub fn get_conversation_store() -> Arc<ConversationStore> {
CONVERSATION_STORE.clone()
}
pub async fn initialize_conversation_store() -> Result<()> {
CONVERSATION_STORE.initialize().await
}