2026-07-19 17:05:27 +07:00
|
|
|
//! Conversation use-case implementations for the CMS.
|
2026-07-16 12:32:17 +07:00
|
|
|
//!
|
2026-07-19 17:05:27 +07:00
|
|
|
//! `ConversationServiceImpl` implements `ConversationService` (defined in
|
|
|
|
|
//! `domain::service`) and is generic over `R: ConversationRepository`
|
|
|
|
|
//! (defined in `domain::repository`), delegating all persistence to that
|
|
|
|
|
//! adapter. The repository is injected at composition root.
|
|
|
|
|
//!
|
|
|
|
|
//! ## Flow
|
|
|
|
|
//! Each method computes the session directory from the session ID, then
|
|
|
|
|
//! delegates the actual I/O to the injected `repo`. Error context is
|
|
|
|
|
//! added at this layer to identify which session caused the failure.
|
|
|
|
|
|
|
|
|
|
use std::path::PathBuf;
|
2026-07-16 12:32:17 +07:00
|
|
|
|
|
|
|
|
use anyhow::{Context, Result};
|
2026-07-19 17:05:27 +07:00
|
|
|
use tracing;
|
2026-07-16 12:32:17 +07:00
|
|
|
|
|
|
|
|
use crate::domain::conversation::{ChatMessage, Conversation};
|
|
|
|
|
use crate::domain::repository::ConversationRepository;
|
|
|
|
|
use crate::domain::service::ConversationService;
|
|
|
|
|
|
2026-07-19 17:05:27 +07:00
|
|
|
/// Service implementation for conversation CRUD operations.
|
|
|
|
|
///
|
|
|
|
|
/// Generic over `R: ConversationRepository` so the persistence layer
|
|
|
|
|
/// can be swapped without changing business logic.
|
|
|
|
|
///
|
|
|
|
|
/// ## Fields
|
|
|
|
|
/// - `repo` — injected conversation repository implementation
|
|
|
|
|
/// - `sessions_dir` — base path under which session directories live
|
2026-07-16 12:32:17 +07:00
|
|
|
pub struct ConversationServiceImpl<R> {
|
|
|
|
|
pub repo: R,
|
2026-07-19 17:05:27 +07:00
|
|
|
/// Base directory containing session subdirectories.
|
|
|
|
|
pub sessions_dir: PathBuf,
|
2026-07-16 12:32:17 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<R: ConversationRepository> ConversationServiceImpl<R> {
|
|
|
|
|
/// Create a new service with the given repository and sessions directory.
|
2026-07-19 17:05:27 +07:00
|
|
|
///
|
|
|
|
|
/// ## Parameters
|
|
|
|
|
/// - `repo` — the repository adapter to delegate persistence to
|
|
|
|
|
/// - `sessions_dir` — base path for session directories (converted via `Into`)
|
|
|
|
|
pub fn new(repo: R, sessions_dir: impl Into<PathBuf>) -> Self {
|
|
|
|
|
tracing::debug!("creating ConversationServiceImpl");
|
2026-07-16 12:32:17 +07:00
|
|
|
Self {
|
|
|
|
|
repo,
|
|
|
|
|
sessions_dir: sessions_dir.into(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Compute the session directory for a given session id.
|
2026-07-19 17:05:27 +07:00
|
|
|
///
|
|
|
|
|
/// Returns `{sessions_dir}/{session_id}`.
|
|
|
|
|
fn session_dir(&self, session_id: &str) -> PathBuf {
|
2026-07-16 12:32:17 +07:00
|
|
|
self.sessions_dir.join(session_id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<R: ConversationRepository> ConversationService for ConversationServiceImpl<R> {
|
2026-07-19 17:05:27 +07:00
|
|
|
/// Load a conversation from disk for the given session.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: resolve session dir → delegate to repo.load() → wrap error with context.
|
2026-07-16 12:32:17 +07:00
|
|
|
fn load_conversation(&self, session_id: &str) -> Result<Conversation> {
|
2026-07-19 17:05:27 +07:00
|
|
|
tracing::debug!("loading conversation for session {session_id}");
|
2026-07-16 12:32:17 +07:00
|
|
|
let dir = self.session_dir(session_id);
|
|
|
|
|
self.repo
|
|
|
|
|
.load(&dir)
|
|
|
|
|
.with_context(|| format!("failed to load conversation for session '{session_id}'"))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 17:05:27 +07:00
|
|
|
/// Persist a conversation to disk.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: resolve session dir from conv.session_id → delegate to repo.save() → wrap error.
|
2026-07-16 12:32:17 +07:00
|
|
|
fn save_conversation(&self, conv: &Conversation) -> Result<()> {
|
2026-07-19 17:05:27 +07:00
|
|
|
tracing::debug!("saving conversation for session {}", conv.session_id);
|
2026-07-16 12:32:17 +07:00
|
|
|
let dir = self.session_dir(&conv.session_id);
|
2026-07-17 06:44:31 +07:00
|
|
|
self.repo.save(&dir, conv).with_context(|| {
|
|
|
|
|
format!(
|
|
|
|
|
"failed to save conversation for session '{}'",
|
|
|
|
|
conv.session_id
|
|
|
|
|
)
|
|
|
|
|
})
|
2026-07-16 12:32:17 +07:00
|
|
|
}
|
|
|
|
|
|
2026-07-19 17:05:27 +07:00
|
|
|
/// Add a message to a conversation and persist immediately.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: push message to in-memory conversation → resolve session dir → delegate save.
|
|
|
|
|
///
|
|
|
|
|
/// ## Note
|
|
|
|
|
/// This is a write-through operation: the message is appended to the
|
|
|
|
|
/// in-memory `Conversation` and then the full conversation is persisted.
|
2026-07-16 12:32:17 +07:00
|
|
|
fn add_message(&self, conv: &mut Conversation, msg: ChatMessage) -> Result<()> {
|
2026-07-19 17:05:27 +07:00
|
|
|
tracing::debug!("adding message to session {}", conv.session_id);
|
|
|
|
|
conv.push(msg); // append message to in-memory conversation
|
2026-07-16 12:32:17 +07:00
|
|
|
let dir = self.session_dir(&conv.session_id);
|
2026-07-17 06:44:31 +07:00
|
|
|
self.repo.save(&dir, conv).with_context(|| {
|
|
|
|
|
format!(
|
|
|
|
|
"failed to persist conversation after adding message for session '{}'",
|
|
|
|
|
conv.session_id
|
|
|
|
|
)
|
|
|
|
|
})
|
2026-07-16 12:32:17 +07:00
|
|
|
}
|
|
|
|
|
}
|