conversations placeholder: show graph neighborhood to extractor

When building the {{conversations}} placeholder for the observation
agent, search for existing nodes relevant to each conversation
fragment and include them in the prompt. Uses seed matching + one-hop
graph expansion to find the neighborhood, so the extractor sees what
the graph already knows about these topics.

This helps prevent duplicate extractions, but the deeper bug is that
select_conversation_fragments doesn't track which conversations have
already been processed — that's next.
This commit is contained in:
ProofOfConcept 2026-03-12 17:55:47 -04:00 committed by Kent Overstreet
parent 10499a98ea
commit b3cf934c18
2 changed files with 60 additions and 1 deletions

View file

@ -163,7 +163,17 @@ fn resolve(
"conversations" => { "conversations" => {
let fragments = super::knowledge::select_conversation_fragments(count); let fragments = super::knowledge::select_conversation_fragments(count);
let text = fragments.iter() 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::<Vec<_>>() .collect::<Vec<_>>()
.join("\n\n---\n\n"); .join("\n\n---\n\n");
Some(Resolved { text, keys: vec![] }) Some(Resolved { text, keys: vec![] })

View file

@ -362,6 +362,55 @@ pub enum NamingResolution {
MergeInto(String), 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<String, f64> = 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. /// Find existing nodes that might conflict with a proposed new node.
/// Returns up to `limit` (key, content_preview) pairs. /// Returns up to `limit` (key, content_preview) pairs.
fn find_conflicts( fn find_conflicts(