diff --git a/poc-memory/src/agents/defs.rs b/poc-memory/src/agents/defs.rs index 8cf53fc..8fc4691 100644 --- a/poc-memory/src/agents/defs.rs +++ b/poc-memory/src/agents/defs.rs @@ -163,7 +163,17 @@ fn resolve( "conversations" => { let fragments = super::knowledge::select_conversation_fragments(count); let text = fragments.iter() - .map(|(id, text)| format!("### Session {}\n\n{}", id, text)) + .map(|(id, text)| { + let existing = super::knowledge::find_existing_observations(store, text, 10); + let mut section = format!("### Session {}\n\n{}", id, text); + if !existing.is_empty() { + section.push_str("\n\n#### Already extracted from this or similar conversations\n\n"); + for (key, preview) in &existing { + section.push_str(&format!("- **`{}`**: {}\n", key, preview)); + } + } + section + }) .collect::>() .join("\n\n---\n\n"); Some(Resolved { text, keys: vec![] }) diff --git a/poc-memory/src/agents/knowledge.rs b/poc-memory/src/agents/knowledge.rs index f5f5012..8f69132 100644 --- a/poc-memory/src/agents/knowledge.rs +++ b/poc-memory/src/agents/knowledge.rs @@ -362,6 +362,55 @@ pub enum NamingResolution { MergeInto(String), } +/// Find existing observation-authored nodes relevant to a conversation fragment. +/// Used to show the observation agent what's already been extracted, +/// preventing duplicate extractions across consolidation runs. +pub fn find_existing_observations( + store: &Store, + conversation_text: &str, + limit: usize, +) -> Vec<(String, String)> { + use std::collections::{BTreeMap, HashSet}; + + let graph = store.build_graph(); + + let content_terms = crate::search::extract_query_terms(conversation_text, 15); + let mut terms: BTreeMap = BTreeMap::new(); + for term in content_terms.split_whitespace() { + terms.entry(term.to_string()).or_insert(1.0); + } + if terms.is_empty() { + return Vec::new(); + } + + let (seeds, _) = crate::search::match_seeds_opts(&terms, store, true, false); + + // Collect seeds + their graph neighbors (one hop) + let mut seen = HashSet::new(); + let mut result = Vec::new(); + + for (key, _) in &seeds { + // Add the seed itself + if seen.insert(key.clone()) { + if let Some(node) = store.nodes.get(key.as_str()) { + result.push((key.clone(), node.content.clone())); + } + } + // Add its neighbors + for (neighbor, _) in graph.neighbors(key) { + if seen.insert(neighbor.clone()) { + if let Some(node) = store.nodes.get(neighbor.as_str()) { + result.push((neighbor.clone(), node.content.clone())); + } + } + } + if result.len() >= limit { break; } + } + + result.truncate(limit); + result +} + /// Find existing nodes that might conflict with a proposed new node. /// Returns up to `limit` (key, content_preview) pairs. fn find_conflicts(