feat: per-user auto-memory with ResponseContext
Three memory channels: hidden tool (sol.memory.set/get in scripts), pre-response injection (relevant memories loaded into system prompt), and post-response extraction (ministral-3b extracts facts after each response). User isolation enforced at Rust level — user_id derived from Matrix sender, never from script arguments. New modules: context (ResponseContext), memory (schema, store, extractor). ResponseContext threaded through responder → tools → script runtime. OpenSearch index sol_user_memory created on startup alongside archive.
This commit is contained in:
@@ -59,6 +59,34 @@ impl Indexer {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_reaction(&self, target_event_id: &str, sender: &str, emoji: &str, timestamp: i64) {
|
||||
// Use a script to append to the reactions array (upsert-safe)
|
||||
let body = json!({
|
||||
"script": {
|
||||
"source": "if (ctx._source.reactions == null) { ctx._source.reactions = []; } ctx._source.reactions.add(params.reaction)",
|
||||
"params": {
|
||||
"reaction": {
|
||||
"sender": sender,
|
||||
"emoji": emoji,
|
||||
"timestamp": timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Err(e) = self
|
||||
.client
|
||||
.update(opensearch::UpdateParts::IndexId(
|
||||
&self.config.opensearch.index,
|
||||
target_event_id,
|
||||
))
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
warn!(target_event_id, sender, emoji, "Failed to add reaction: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_redaction(&self, event_id: &str) {
|
||||
let body = json!({
|
||||
"doc": {
|
||||
|
||||
@@ -24,6 +24,15 @@ pub struct ArchiveDocument {
|
||||
pub edited: bool,
|
||||
#[serde(default)]
|
||||
pub redacted: bool,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub reactions: Vec<Reaction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Reaction {
|
||||
pub sender: String,
|
||||
pub emoji: String,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
const INDEX_MAPPING: &str = r#"{
|
||||
@@ -45,7 +54,15 @@ const INDEX_MAPPING: &str = r#"{
|
||||
"media_urls": { "type": "keyword" },
|
||||
"event_type": { "type": "keyword" },
|
||||
"edited": { "type": "boolean" },
|
||||
"redacted": { "type": "boolean" }
|
||||
"redacted": { "type": "boolean" },
|
||||
"reactions": {
|
||||
"type": "nested",
|
||||
"properties": {
|
||||
"sender": { "type": "keyword" },
|
||||
"emoji": { "type": "keyword" },
|
||||
"timestamp": { "type": "date", "format": "epoch_millis" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
@@ -63,6 +80,25 @@ pub async fn create_index_if_not_exists(client: &OpenSearch, index: &str) -> any
|
||||
|
||||
if exists.status_code().is_success() {
|
||||
info!(index, "OpenSearch index already exists");
|
||||
// Ensure reactions field exists (added after initial schema)
|
||||
let reactions_mapping = serde_json::json!({
|
||||
"properties": {
|
||||
"reactions": {
|
||||
"type": "nested",
|
||||
"properties": {
|
||||
"sender": { "type": "keyword" },
|
||||
"emoji": { "type": "keyword" },
|
||||
"timestamp": { "type": "date", "format": "epoch_millis" }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let _ = client
|
||||
.indices()
|
||||
.put_mapping(opensearch::indices::IndicesPutMappingParts::Index(&[index]))
|
||||
.body(reactions_mapping)
|
||||
.send()
|
||||
.await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -102,6 +138,7 @@ mod tests {
|
||||
event_type: "m.room.message".to_string(),
|
||||
edited: false,
|
||||
redacted: false,
|
||||
reactions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user