use crate::{Error, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
#[cfg(feature = "sqlite")]
use super::sqlite_storage::SqliteStorage;
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub struct SessionId(String);
impl SessionId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for SessionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Session {
pub id: SessionId,
pub system_prompt: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
#[serde(default = "chrono::Utc::now")]
pub created_at: chrono::DateTime<chrono::Utc>,
#[serde(default = "chrono::Utc::now")]
pub updated_at: chrono::DateTime<chrono::Utc>,
}
impl Session {
pub fn new(id: SessionId) -> Self {
let now = chrono::Utc::now();
Self {
id,
system_prompt: None,
metadata: HashMap::new(),
created_at: now,
updated_at: now,
}
}
#[must_use]
pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
pub fn id(&self) -> &SessionId {
&self.id
}
}
#[async_trait]
pub trait SessionStorage: Send + Sync + std::fmt::Debug {
async fn save(&self, session: &Session) -> Result<()>;
async fn load(&self, id: &SessionId) -> Result<Option<Session>>;
async fn delete(&self, id: &SessionId) -> Result<()>;
async fn list_ids(&self) -> Result<Vec<SessionId>>;
async fn clear(&self) -> Result<()>;
}
#[derive(Debug, Clone)]
pub enum StorageBackend {
Memory,
File(PathBuf),
#[cfg(feature = "sqlite")]
Sqlite(PathBuf),
}
impl Default for StorageBackend {
fn default() -> Self {
StorageBackend::Memory
}
}
#[derive(Debug, Clone)]
pub struct MemoryStorage {
sessions: Arc<RwLock<HashMap<SessionId, Session>>>,
}
impl MemoryStorage {
pub fn new() -> Self {
Self {
sessions: Arc::new(RwLock::new(HashMap::new())),
}
}
}
#[async_trait]
impl SessionStorage for MemoryStorage {
async fn save(&self, session: &Session) -> Result<()> {
let mut sessions = self.sessions.write().await;
sessions.insert(session.id.clone(), session.clone());
Ok(())
}
async fn load(&self, id: &SessionId) -> Result<Option<Session>> {
let sessions = self.sessions.read().await;
Ok(sessions.get(id).cloned())
}
async fn delete(&self, id: &SessionId) -> Result<()> {
let mut sessions = self.sessions.write().await;
sessions.remove(id);
Ok(())
}
async fn list_ids(&self) -> Result<Vec<SessionId>> {
let sessions = self.sessions.read().await;
Ok(sessions.keys().cloned().collect())
}
async fn clear(&self) -> Result<()> {
let mut sessions = self.sessions.write().await;
sessions.clear();
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct FileStorage {
base_path: PathBuf,
}
impl FileStorage {
pub fn new(base_path: PathBuf) -> Self {
Self { base_path }
}
fn session_file_path(&self, id: &SessionId) -> PathBuf {
self.base_path.join(format!("{}.json", id.as_str()))
}
async fn ensure_directory(&self) -> Result<()> {
tokio::fs::create_dir_all(&self.base_path)
.await
.map_err(|e| Error::Io(e))?;
Ok(())
}
}
#[async_trait]
impl SessionStorage for FileStorage {
async fn save(&self, session: &Session) -> Result<()> {
self.ensure_directory().await?;
let mut updated_session = session.clone();
updated_session.updated_at = chrono::Utc::now();
let json = serde_json::to_string_pretty(&updated_session)?;
let path = self.session_file_path(&session.id);
tokio::fs::write(&path, json)
.await
.map_err(|e| Error::Io(e))?;
Ok(())
}
async fn load(&self, id: &SessionId) -> Result<Option<Session>> {
let path = self.session_file_path(id);
match tokio::fs::read_to_string(&path).await {
Ok(content) => {
let session: Session = serde_json::from_str(&content)?;
Ok(Some(session))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(Error::Io(e)),
}
}
async fn delete(&self, id: &SessionId) -> Result<()> {
let path = self.session_file_path(id);
match tokio::fs::remove_file(&path).await {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(Error::Io(e)),
}
}
async fn list_ids(&self) -> Result<Vec<SessionId>> {
self.ensure_directory().await?;
let mut ids = Vec::new();
let mut entries = tokio::fs::read_dir(&self.base_path)
.await
.map_err(|e| Error::Io(e))?;
while let Some(entry) = entries.next_entry().await.map_err(|e| Error::Io(e))? {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
ids.push(SessionId::new(stem));
}
}
}
Ok(ids)
}
async fn clear(&self) -> Result<()> {
if self.base_path.exists() {
let mut entries = tokio::fs::read_dir(&self.base_path)
.await
.map_err(|e| Error::Io(e))?;
while let Some(entry) = entries.next_entry().await.map_err(|e| Error::Io(e))? {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
tokio::fs::remove_file(&path)
.await
.map_err(|e| Error::Io(e))?;
}
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SessionManager {
storage: Arc<Box<dyn SessionStorage>>,
}
impl SessionManager {
pub fn new() -> Self {
Self::with_storage(StorageBackend::Memory)
}
pub fn with_storage(backend: StorageBackend) -> Self {
let storage: Box<dyn SessionStorage> = match backend {
StorageBackend::Memory => Box::new(MemoryStorage::new()),
StorageBackend::File(path) => Box::new(FileStorage::new(path)),
#[cfg(feature = "sqlite")]
StorageBackend::Sqlite(_) => {
panic!("SQLite storage requires async initialization. Use SessionManager::with_storage_async instead.");
}
};
Self {
storage: Arc::new(storage),
}
}
pub async fn with_storage_async(backend: StorageBackend) -> Result<Self> {
let storage: Box<dyn SessionStorage> = match backend {
StorageBackend::Memory => Box::new(MemoryStorage::new()),
StorageBackend::File(path) => Box::new(FileStorage::new(path)),
#[cfg(feature = "sqlite")]
StorageBackend::Sqlite(path) => Box::new(SqliteStorage::new(path).await?),
};
Ok(Self {
storage: Arc::new(storage),
})
}
pub fn builder() -> SessionBuilder {
SessionBuilder::new()
}
pub fn create_session(&self) -> SessionBuilder {
SessionBuilder::with_manager(self.clone())
}
pub async fn get(&self, id: &SessionId) -> Result<Option<Session>> {
self.storage.load(id).await
}
pub async fn resume(&self, id: &SessionId) -> Result<Session> {
self.storage
.load(id)
.await?
.ok_or_else(|| Error::SessionNotFound(id.to_string()))
}
pub async fn list(&self) -> Result<Vec<SessionId>> {
self.storage.list_ids().await
}
pub async fn delete(&self, id: &SessionId) -> Result<()> {
self.storage.delete(id).await
}
pub async fn clear(&self) -> Result<()> {
self.storage.clear().await
}
async fn store(&self, session: Session) -> Result<()> {
self.storage.save(&session).await
}
}
impl Default for SessionManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct SessionBuilder {
session: Session,
manager: Option<SessionManager>,
}
impl Default for SessionBuilder {
fn default() -> Self {
Self::new()
}
}
impl SessionBuilder {
pub fn new() -> Self {
let id = SessionId::new(uuid::Uuid::new_v4().to_string());
Self {
session: Session::new(id),
manager: None,
}
}
pub fn with_id(id: impl Into<String>) -> Self {
Self {
session: Session::new(SessionId::new(id)),
manager: None,
}
}
fn with_manager(manager: SessionManager) -> Self {
let id = SessionId::new(uuid::Uuid::new_v4().to_string());
Self {
session: Session::new(id),
manager: Some(manager),
}
}
#[must_use]
pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.session.system_prompt = Some(prompt.into());
self
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.session.metadata.insert(key.into(), value);
self
}
pub async fn build(self) -> Result<Session> {
if let Some(manager) = self.manager {
manager.store(self.session.clone()).await?;
}
Ok(self.session)
}
}