use std::sync::Arc;
use rig::completion::Message;
use rig::memory::{DemotionHook, MemoryError};
use rig::wasm_compat::WasmBoxedFuture;
use crate::dedup::DedupSet;
use crate::frame_writer::{commit_if_each_turn, write_frame};
use crate::hook::{MemoryConfig, WritePolicy, render_message_text};
use crate::store::MemvidStore;
pub struct MemvidDemotionHook {
store: MemvidStore,
config: MemoryConfig,
dedup: Arc<DedupSet>,
}
impl Clone for MemvidDemotionHook {
fn clone(&self) -> Self {
Self {
store: self.store.clone(),
config: self.config.clone(),
dedup: Arc::clone(&self.dedup),
}
}
}
impl std::fmt::Debug for MemvidDemotionHook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemvidDemotionHook")
.field("config", &self.config)
.field("dedup", &self.dedup)
.finish_non_exhaustive()
}
}
impl MemvidDemotionHook {
pub fn new(store: MemvidStore, config: MemoryConfig) -> Self {
Self {
store,
config,
dedup: Arc::new(DedupSet::new()),
}
}
pub fn with_defaults(store: MemvidStore) -> Self {
Self::new(store, MemoryConfig::default())
}
pub fn store(&self) -> &MemvidStore {
&self.store
}
pub fn config(&self) -> &MemoryConfig {
&self.config
}
pub fn dedup_snapshot(&self) -> Result<Vec<String>, MemoryError> {
self.dedup
.snapshot()
.map_err(|err| MemoryError::backend(Box::new(err)))
}
pub fn load_dedup_snapshot(&self, hexes: &[String]) -> Result<(), MemoryError> {
self.dedup
.extend_from_snapshot(hexes)
.map_err(|err| MemoryError::backend(Box::new(err)))
}
fn render(&self, msg: &Message) -> Option<String> {
match &self.config.policy {
WritePolicy::Disabled => None,
WritePolicy::Raw => render_message_text(msg),
WritePolicy::Custom(f) => f(msg),
}
}
async fn write(&self, conversation_id: &str, msg: &Message) -> Result<(), MemoryError> {
let Some(text) = self.render(msg) else {
return Ok(());
};
write_frame(
&self.store,
&self.config,
&self.dedup,
crate::metadata::FrameKind::DemotedMessage,
conversation_id,
role_label(msg),
&text,
)
.await
.map(|_| ())
}
}
impl DemotionHook for MemvidDemotionHook {
fn on_demote<'a>(
&'a self,
conversation_id: &'a str,
messages: Vec<Message>,
) -> WasmBoxedFuture<'a, Result<(), MemoryError>> {
Box::pin(async move {
if matches!(self.config.policy, WritePolicy::Disabled) || messages.is_empty() {
return Ok(());
}
for msg in &messages {
self.write(conversation_id, msg).await?;
}
commit_if_each_turn(&self.store, &self.config).await?;
#[cfg(feature = "observe")]
rig_tap::emit_kind(
conversation_id,
rig_tap::EventKind::MemoryDemoted {
demoted_count: messages.len(),
tags: self.config.default_tags.clone(),
},
);
Ok(())
})
}
}
fn role_label(msg: &Message) -> &'static str {
match msg {
Message::System { .. } => "system",
Message::User { .. } => "user",
Message::Assistant { .. } => "assistant",
}
}