From 6af3f0bfa2d265e68faa9174f6e69ae939757c6d Mon Sep 17 00:00:00 2001 From: quangdang46 Date: Wed, 3 Jun 2026 14:10:56 +0700 Subject: [PATCH] feat(palace): add tag/untag/link/list_tags to MemoryProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jcode's MemoryManager exposes 4 graph-mutation methods that today have no equivalent on the MemoryProvider trait: - tag(&id, tag) — MemoryManager::tag_memory - untag(&id, tag) — MemoryManager::untag_memory - link(&from, &to, w) — MemoryManager::link_memories (with weight) - list_tags() — closest jcode equiv: graph_stats.1 Add them as default-implemented methods on MemoryProvider. The defaults use the metadata path (so this PR does NOT depend on the KG being wired into Palace), mirroring the values in shapes that match the eventual KG triples: tag → metadata["tags"] (Vec) + metadata["tag:"] = true untag → metadata["tags"] (minus removed) + remove metadata["tag:"] link → metadata["links"] = Vec<{target, weight}> list → aggregate metadata["tags"] across get_drawers(None, None) Implementations that have a wired KG (mp-020 sub-task) should override and use KnowledgeGraph::add_triple / query_relationship directly: tag → add_triple { subject: drawer_id, predicate: "has_tag", object: tag, current: true } link → add_triple { subject: from, predicate: "relates_to", object: to, confidence: Some(weight) } list → query_relationship(predicate="has_tag") then group-by Includes a cherry-pick of PR #1 (the boost/decay/reinforce/ supersede/set_metadata additions) so the trait has all 9 mutation methods together. Both can be reviewed independently because the cherry-pick commit is its own clean diff in the series. This is PR 2/8 in the jcode → mempalace Mode C library migration series. Co-Authored-By: Claude Opus 4.8 --- crates/core/src/palace.rs | 114 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/crates/core/src/palace.rs b/crates/core/src/palace.rs index 229808f..b1562e4 100644 --- a/crates/core/src/palace.rs +++ b/crates/core/src/palace.rs @@ -947,6 +947,120 @@ pub trait MemoryProvider: Send + Sync + 'static { Ok((memories, tags, edges, clusters)) } + // ----------------------------------------------------------------------- + // Tag and link methods (mp-migration 2/8) + // + // jcode's `MemoryManager::tag_memory` / `link_memories` are the + // canonical graph-mutation entry points. mempalace stores these + // as KG triples (predicate = "has_tag" / "relates_to") so the + // same data backs both `mempalace_kg_query` and jcode's adapter. + // + // The default implementations here use the metadata path (so + // they don't depend on the KG being wired into Palace) and + // mirror the values in shapes that match the eventual KG + // triples: + // tag → metadata["tags"] (Vec) + // + metadata["tag:"] = true (cheap lookup) + // link → metadata["links"] (Vec<{target, weight}>) + // Implementations that have a wired KG should override and use + // KnowledgeGraph::add_triple directly. + // ----------------------------------------------------------------------- + + /// Add a tag to a drawer. jcode's `MemoryManager::tag_memory`. + async fn tag(&self, id: &DrawerId, tag: &str) -> anyhow::Result<()> + where + Self: Sized, + { + default_mutate_drawer(self, id, |d| { + let mut tags: Vec = d + .metadata + .get("tags") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_default(); + if !tags.iter().any(|t| t == tag) { + tags.push(tag.to_string()); + } + d.metadata + .insert("tags".to_string(), serde_json::json!(tags)); + d.metadata + .insert(format!("tag:{}", tag), serde_json::json!(true)); + }) + .await + } + + /// Remove a tag from a drawer. jcode's + /// `MemoryManager::untag_memory`. + async fn untag(&self, id: &DrawerId, tag: &str) -> anyhow::Result<()> + where + Self: Sized, + { + default_mutate_drawer(self, id, |d| { + let mut tags: Vec = d + .metadata + .get("tags") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_default(); + tags.retain(|t| t != tag); + d.metadata + .insert("tags".to_string(), serde_json::json!(tags)); + d.metadata.remove(&format!("tag:{}", tag)); + }) + .await + } + + /// Link two drawers with a weighted edge. jcode's + /// `MemoryManager::link_memories`. + async fn link(&self, from_id: &DrawerId, to_id: &DrawerId, weight: f32) -> anyhow::Result<()> + where + Self: Sized, + { + default_mutate_drawer(self, from_id, |d| { + let mut links: Vec = d + .metadata + .get("links") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .unwrap_or_default(); + links.retain(|l| l.get("target").and_then(|v| v.as_str()) != Some(to_id.0.as_str())); + links.push(serde_json::json!({ + "target": to_id.0, + "weight": weight, + })); + d.metadata + .insert("links".to_string(), serde_json::json!(links)); + }) + .await + } + + /// List all tags used in the palace, with usage counts. + /// jcode's closest equivalent is `graph_stats.1` (the second + /// element of the 4-tuple). + /// + /// Returns `Vec<(tag, count)>` sorted by count desc, then tag + /// asc (deterministic). The default implementation aggregates + /// from `get_drawers`; implementations with a wired KG should + /// override and use `kg.query_relationship(predicate="has_tag")` + /// for an O(1) path. + async fn list_tags(&self) -> anyhow::Result> + where + Self: Sized, + { + use std::collections::HashMap; + let drawers = self.get_drawers(None, None).await?; + let mut counts: HashMap = HashMap::new(); + for d in &drawers { + if let Some(arr) = d.metadata.get("tags") { + if let Ok(tags) = serde_json::from_value::>(arr.clone()) { + for t in tags { + *counts.entry(t).or_insert(0) += 1; + } + } + } + } + let mut out: Vec<(String, usize)> = counts.into_iter().collect(); + out.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0))); + Ok(out) + } + /// Stable identifier for this provider — used in audit logs and /// agent memory traces. Convention: `"mempalace:"`. fn fingerprint(&self) -> &str;