organize: topic cluster diagnostic + agent with tool access
Add `poc-memory graph organize TERM` diagnostic that finds nodes
matching a search term, computes pairwise cosine similarity, reports
connectivity gaps, and optionally creates anchor nodes.
Add organize.agent definition that uses Bash(poc-memory:*) tool access
to explore clusters autonomously — query selects highest-degree
unvisited nodes, agent drives its own iteration via poc-memory CLI.
Add {{organize}} placeholder in defs.rs for inline cluster resolution.
Add `tools` field to AgentDef/AgentHeader so agents can declare
allowed tool patterns (passed as --allowedTools to claude CLI).
This commit is contained in:
parent
1da712874b
commit
76b8e69749
3 changed files with 316 additions and 0 deletions
|
|
@ -32,6 +32,7 @@ pub struct AgentDef {
|
|||
pub prompt: String,
|
||||
pub model: String,
|
||||
pub schedule: String,
|
||||
pub tools: Vec<String>,
|
||||
}
|
||||
|
||||
/// The JSON header portion (first line of the file).
|
||||
|
|
@ -44,6 +45,8 @@ struct AgentHeader {
|
|||
model: String,
|
||||
#[serde(default)]
|
||||
schedule: String,
|
||||
#[serde(default)]
|
||||
tools: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_model() -> String { "sonnet".into() }
|
||||
|
|
@ -60,6 +63,7 @@ fn parse_agent_file(content: &str) -> Option<AgentDef> {
|
|||
prompt: prompt.to_string(),
|
||||
model: header.model,
|
||||
schedule: header.schedule,
|
||||
tools: header.tools,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +164,76 @@ fn resolve(
|
|||
})
|
||||
}
|
||||
|
||||
"organize" => {
|
||||
// Run cluster diagnostic for the query term
|
||||
// The query field of the agent def holds the search term
|
||||
let term = if keys.is_empty() { "" } else { &keys[0] };
|
||||
if term.is_empty() {
|
||||
return Some(Resolved { text: "(no term provided)".into(), keys: vec![] });
|
||||
}
|
||||
let term_lower = term.to_lowercase();
|
||||
let skip_prefixes = ["journal#", "daily-", "weekly-", "monthly-", "_",
|
||||
"deep-index#", "facts-", "irc-history#"];
|
||||
|
||||
let mut cluster: Vec<(String, String)> = Vec::new();
|
||||
for (key, node) in &store.nodes {
|
||||
if node.deleted { continue; }
|
||||
if !key.to_lowercase().contains(&term_lower) { continue; }
|
||||
if skip_prefixes.iter().any(|p| key.starts_with(p)) { continue; }
|
||||
cluster.push((key.clone(), node.content.clone()));
|
||||
}
|
||||
cluster.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
// Similarity pairs
|
||||
let pairs = crate::similarity::pairwise_similar(&cluster, 0.4);
|
||||
|
||||
let mut text = format!("### Cluster: '{}' ({} nodes)\n\n", term, cluster.len());
|
||||
|
||||
// Similarity report
|
||||
if !pairs.is_empty() {
|
||||
text.push_str("#### Similarity scores\n\n");
|
||||
for (a, b, sim) in &pairs {
|
||||
text.push_str(&format!(" [{:.3}] {} ↔ {}\n", sim, a, b));
|
||||
}
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
// Connectivity
|
||||
let cluster_keys: std::collections::HashSet<&str> = cluster.iter()
|
||||
.map(|(k,_)| k.as_str()).collect();
|
||||
let mut best_hub: Option<(&str, usize)> = None;
|
||||
for key in &cluster_keys {
|
||||
let intra = graph.neighbor_keys(key).iter()
|
||||
.filter(|n| cluster_keys.contains(*n))
|
||||
.count();
|
||||
if best_hub.is_none() || intra > best_hub.unwrap().1 {
|
||||
best_hub = Some((key, intra));
|
||||
}
|
||||
}
|
||||
if let Some((hub, deg)) = best_hub {
|
||||
text.push_str(&format!("#### Hub: {} (intra-cluster degree {})\n\n", hub, deg));
|
||||
let hub_nbrs = graph.neighbor_keys(hub);
|
||||
for key in &cluster_keys {
|
||||
if *key == hub { continue; }
|
||||
if !hub_nbrs.contains(*key) {
|
||||
text.push_str(&format!(" NOT linked to hub: {}\n", key));
|
||||
}
|
||||
}
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
// Full node contents
|
||||
text.push_str("#### Node contents\n\n");
|
||||
let mut result_keys = Vec::new();
|
||||
for (key, content) in &cluster {
|
||||
let words = content.split_whitespace().count();
|
||||
text.push_str(&format!("##### {} ({} words)\n\n{}\n\n---\n\n", key, words, content));
|
||||
result_keys.push(key.clone());
|
||||
}
|
||||
|
||||
Some(Resolved { text, keys: result_keys })
|
||||
}
|
||||
|
||||
"conversations" => {
|
||||
let fragments = super::knowledge::select_conversation_fragments(count);
|
||||
let text = fragments.iter()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue