use async_trait::async_trait;
use serde_json::Value as JsonValue;
use time::OffsetDateTime;
use toolkit_macros::domain_model;
use toolkit_odata::{ODataQuery, Page};
use uuid::Uuid;
use chat_engine_sdk::models::{
FileCitation, LifecycleState, LinkCitation, LinkReference, Message, MessagePartInput, Session,
SessionType,
};
use crate::domain::error::ChatEngineError;
use crate::domain::reaction::{MessageReaction, ReactionType};
#[domain_model]
#[derive(Debug, Clone)]
pub struct NewUserMessage {
pub session_id: Uuid,
pub tenant_id: Option<String>,
pub user_id: Option<String>,
pub parent_message_id: Option<Uuid>,
pub parts: Vec<MessagePartInput>,
pub file_ids: Option<Vec<Uuid>>,
pub metadata: Option<JsonValue>,
}
#[domain_model]
#[derive(Debug, Clone, Copy)]
pub struct InsertedPair {
pub user_message_id: Uuid,
pub assistant_message_id: Uuid,
pub user_variant_index: i32,
}
#[domain_model]
#[derive(Debug, Clone, Default)]
pub struct PartCitations {
pub file_citations: Vec<FileCitation>,
pub link_citations: Vec<LinkCitation>,
pub references: Vec<LinkReference>,
}
impl PartCitations {
#[must_use]
pub fn is_empty(&self) -> bool {
self.file_citations.is_empty()
&& self.link_citations.is_empty()
&& self.references.is_empty()
}
}
#[domain_model]
#[derive(Debug, Clone)]
pub enum FinalizeOutcome {
Complete {
text: String,
metadata: Option<JsonValue>,
citations: PartCitations,
extra_parts: Vec<MessagePartInput>,
},
Cancelled {
text: String,
},
Errored {
text: String,
error: String,
finish_reason: &'static str,
},
}
#[async_trait]
pub trait MessageRepo: Send + Sync {
async fn insert_user_and_assistant_stub(
&self,
req: NewUserMessage,
) -> Result<InsertedPair, ChatEngineError>;
async fn finalize_assistant(
&self,
session_id: Uuid,
assistant_message_id: Uuid,
outcome: FinalizeOutcome,
) -> Result<(), ChatEngineError>;
async fn fetch_active_history(
&self,
session_id: Uuid,
depth: Option<u32>,
) -> Result<Vec<Message>, ChatEngineError>;
async fn find_message_in_session(
&self,
session_id: Uuid,
message_id: Uuid,
) -> Result<Option<Message>, ChatEngineError>;
async fn find_message_by_id(
&self,
message_id: Uuid,
) -> Result<Option<Message>, ChatEngineError> {
let _ = message_id;
Err(ChatEngineError::internal(
"find_message_by_id not implemented for this repository",
))
}
async fn list_active_path(&self, session_id: Uuid) -> Result<Vec<Message>, ChatEngineError> {
self.fetch_active_history(session_id, None).await
}
async fn insert_assistant_variant_stub(
&self,
session_id: Uuid,
parent_message_id: Uuid,
tenant_id: Option<String>,
) -> Result<InsertedPair, ChatEngineError> {
let _ = (session_id, parent_message_id, tenant_id);
Err(ChatEngineError::internal(
"insert_assistant_variant_stub not implemented for this repository",
))
}
async fn insert_summary_message(
&self,
session_id: Uuid,
text: String,
metadata: Option<JsonValue>,
summarized_ids: Vec<Uuid>,
tenant_id: Option<String>,
) -> Result<Uuid, ChatEngineError> {
let _ = (session_id, text, metadata, summarized_ids, tenant_id);
Err(ChatEngineError::internal(
"insert_summary_message not implemented for this repository",
))
}
async fn list_non_root_messages_chrono(
&self,
session_id: Uuid,
) -> Result<Vec<Message>, ChatEngineError> {
let _ = session_id;
Ok(Vec::new())
}
async fn list_non_root_messages_older_than(
&self,
session_id: Uuid,
older_than: OffsetDateTime,
) -> Result<Vec<Message>, ChatEngineError> {
let _ = (session_id, older_than);
Ok(Vec::new())
}
async fn count_non_root_messages(&self, session_id: Uuid) -> Result<u64, ChatEngineError> {
let _ = session_id;
Ok(0)
}
async fn list_oldest_non_root_message_ids(
&self,
session_id: Uuid,
limit: u32,
) -> Result<Vec<Uuid>, ChatEngineError> {
let _ = (session_id, limit);
Ok(Vec::new())
}
async fn list_non_root_message_ids_older_than(
&self,
session_id: Uuid,
older_than: OffsetDateTime,
limit: u32,
) -> Result<Vec<Uuid>, ChatEngineError> {
let _ = (session_id, older_than, limit);
Ok(Vec::new())
}
async fn delete_message_subtree(
&self,
session_id: Uuid,
root_id: Uuid,
) -> Result<u64, ChatEngineError> {
let _ = (session_id, root_id);
Err(ChatEngineError::internal(
"delete_message_subtree not implemented for this repository",
))
}
}
#[domain_model]
#[derive(Debug, Clone)]
pub struct ReactionUpsertOutcome {
pub reaction: MessageReaction,
pub previous_reaction_type: Option<ReactionType>,
}
#[domain_model]
#[derive(Debug, Clone)]
pub struct ReactionDeleteOutcome {
pub applied: bool,
pub previous_reaction_type: Option<ReactionType>,
}
#[async_trait]
pub trait ReactionRepo: Send + Sync {
async fn get_by_pk(
&self,
message_id: Uuid,
user_id: &str,
) -> Result<Option<MessageReaction>, ChatEngineError>;
async fn upsert(
&self,
message_id: Uuid,
user_id: &str,
reaction_type: ReactionType,
) -> Result<ReactionUpsertOutcome, ChatEngineError>;
async fn delete(
&self,
message_id: Uuid,
user_id: &str,
) -> Result<ReactionDeleteOutcome, ChatEngineError>;
async fn list_by_message(
&self,
message_id: Uuid,
) -> Result<Vec<MessageReaction>, ChatEngineError>;
}
#[async_trait]
pub trait PluginConfigRepo: Send + Sync {
async fn find(
&self,
plugin_instance_id: &str,
session_type_id: Uuid,
) -> Result<Option<JsonValue>, ChatEngineError>;
async fn upsert(
&self,
plugin_instance_id: &str,
session_type_id: Uuid,
config: JsonValue,
) -> Result<(), ChatEngineError>;
async fn delete(
&self,
plugin_instance_id: &str,
session_type_id: Uuid,
) -> Result<(), ChatEngineError>;
}
pub const DEFAULT_SOFT_DELETE_RETENTION_DAYS: i64 = 30;
#[domain_model]
#[derive(Debug, Clone)]
pub struct NewSession {
pub session_id: Uuid,
pub tenant_id: String,
pub user_id: String,
pub client_id: Option<String>,
pub session_type_id: Option<Uuid>,
pub metadata: Option<JsonValue>,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
}
#[allow(clippy::large_enum_variant)]
#[domain_model]
#[derive(Debug, Clone)]
pub enum SessionScopeCheck {
Owned(Session),
WrongTenant,
WrongUser,
NotFound,
}
#[async_trait]
pub trait SessionRepo: Send + Sync {
async fn insert(&self, new: NewSession) -> Result<Session, ChatEngineError>;
async fn find_by_id(
&self,
tenant_id: &str,
user_id: &str,
session_id: Uuid,
) -> Result<Option<Session>, ChatEngineError>;
async fn list_paginated(
&self,
tenant_id: &str,
user_id: &str,
query: &ODataQuery,
) -> Result<Page<Session>, ChatEngineError>;
async fn update_metadata(
&self,
tenant_id: &str,
user_id: &str,
session_id: Uuid,
metadata: Option<JsonValue>,
) -> Result<Session, ChatEngineError>;
async fn update_capabilities(
&self,
tenant_id: &str,
user_id: &str,
session_id: Uuid,
capabilities: Option<JsonValue>,
) -> Result<Session, ChatEngineError>;
async fn update_lifecycle_state(
&self,
tenant_id: &str,
user_id: &str,
session_id: Uuid,
new_state: LifecycleState,
) -> Result<Session, ChatEngineError>;
async fn soft_delete(
&self,
tenant_id: &str,
user_id: &str,
session_id: Uuid,
retention_days: i64,
) -> Result<Session, ChatEngineError>;
async fn hard_delete(
&self,
tenant_id: &str,
user_id: &str,
session_id: Uuid,
) -> Result<bool, ChatEngineError>;
async fn scheduled_hard_delete_at(
&self,
tenant_id: &str,
user_id: &str,
session_id: Uuid,
) -> Result<Option<OffsetDateTime>, ChatEngineError> {
let _ = (tenant_id, user_id, session_id);
Ok(None)
}
async fn list_active_sessions_for_tenant(
&self,
tenant_id: &str,
after: Option<Uuid>,
limit: u32,
) -> Result<Vec<Session>, ChatEngineError> {
let _ = (tenant_id, after, limit);
Ok(Vec::new())
}
async fn list_tenants_with_active_sessions(&self) -> Result<Vec<String>, ChatEngineError> {
Ok(Vec::new())
}
async fn find_by_session_id_unscoped(
&self,
session_id: Uuid,
) -> Result<Option<Session>, ChatEngineError> {
let _ = session_id;
Ok(None)
}
async fn check_session_scope(
&self,
tenant_id: &str,
user_id: &str,
session_id: Uuid,
) -> Result<SessionScopeCheck, ChatEngineError> {
let Some(row) = self.find_by_session_id_unscoped(session_id).await? else {
return Ok(SessionScopeCheck::NotFound);
};
if row.tenant_id.as_str() != tenant_id {
return Ok(SessionScopeCheck::WrongTenant);
}
if row.user_id.as_str() != user_id {
return Ok(SessionScopeCheck::WrongUser);
}
Ok(SessionScopeCheck::Owned(row))
}
async fn find_by_share_token(
&self,
share_token: &str,
) -> Result<Option<Session>, ChatEngineError> {
let _ = share_token;
Ok(None)
}
async fn update_share_token(
&self,
tenant_id: &str,
user_id: &str,
session_id: Uuid,
share_token: Option<String>,
metadata: Option<JsonValue>,
) -> Result<Session, ChatEngineError> {
let _ = (tenant_id, user_id, session_id, share_token, metadata);
Err(ChatEngineError::internal(
"update_share_token not implemented for this repository",
))
}
}
#[domain_model]
#[derive(Debug, Clone)]
pub struct NewSessionType {
pub session_type_id: Uuid,
pub name: String,
pub plugin_instance_id: Option<String>,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
}
#[async_trait]
pub trait SessionTypeRepo: Send + Sync {
async fn insert(&self, new: NewSessionType) -> Result<SessionType, ChatEngineError>;
async fn find_by_id(
&self,
session_type_id: Uuid,
) -> Result<Option<SessionType>, ChatEngineError>;
async fn list(&self) -> Result<Vec<SessionType>, ChatEngineError>;
}
#[domain_model]
#[derive(Debug, Clone)]
pub struct BufferedEvent {
pub seq: u64,
pub event: JsonValue,
}
#[async_trait]
pub trait StreamEventBuffer: Send + Sync {
async fn append(
&self,
message_id: Uuid,
seq: u64,
event: JsonValue,
expires_at: OffsetDateTime,
) -> Result<(), ChatEngineError>;
async fn read_since(
&self,
message_id: Uuid,
after_seq: Option<u64>,
) -> Result<Vec<BufferedEvent>, ChatEngineError>;
async fn delete_expired(&self, now: OffsetDateTime) -> Result<u64, ChatEngineError>;
}