graph_trace, graph_link_impact: convert to RPC tools
Agents can use these to understand graph structure: - trace: shows node and neighbors grouped by type - link_impact: analyzes what happens if a link is removed Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
f02a23468e
commit
11f2d5b169
2 changed files with 123 additions and 99 deletions
|
|
@ -124,6 +124,8 @@ async fn dispatch(
|
||||||
"graph_health" => graph_health().await,
|
"graph_health" => graph_health().await,
|
||||||
"graph_communities" => graph_communities(&args).await,
|
"graph_communities" => graph_communities(&args).await,
|
||||||
"graph_normalize_strengths" => graph_normalize_strengths(&args).await,
|
"graph_normalize_strengths" => graph_normalize_strengths(&args).await,
|
||||||
|
"graph_trace" => graph_trace(&args).await,
|
||||||
|
"graph_link_impact" => graph_link_impact(&args).await,
|
||||||
"journal_tail" => journal_tail(&args).await,
|
"journal_tail" => journal_tail(&args).await,
|
||||||
"journal_new" => journal_new(agent, &args).await,
|
"journal_new" => journal_new(agent, &args).await,
|
||||||
"journal_update" => journal_update(agent, &args).await,
|
"journal_update" => journal_update(agent, &args).await,
|
||||||
|
|
@ -686,3 +688,114 @@ async fn graph_normalize_strengths(args: &serde_json::Value) -> Result<String> {
|
||||||
|
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn graph_link_impact(args: &serde_json::Value) -> Result<String> {
|
||||||
|
let source = get_str(args, "source")?;
|
||||||
|
let target = get_str(args, "target")?;
|
||||||
|
|
||||||
|
let arc = cached_store().await?;
|
||||||
|
let store = arc.lock().await;
|
||||||
|
let source = store.resolve_key(source).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
|
let target = store.resolve_key(target).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
|
let g = store.build_graph();
|
||||||
|
let impact = g.link_impact(&source, &target);
|
||||||
|
|
||||||
|
use std::fmt::Write;
|
||||||
|
let mut out = String::new();
|
||||||
|
writeln!(out, "Link impact: {} → {}", source, target).ok();
|
||||||
|
writeln!(out, " Source degree: {} Target degree: {}", impact.source_deg, impact.target_deg).ok();
|
||||||
|
writeln!(out, " Hub link: {} Same community: {}", impact.is_hub_link, impact.same_community).ok();
|
||||||
|
writeln!(out, " ΔCC source: {:+.4} ΔCC target: {:+.4}", impact.delta_cc_source, impact.delta_cc_target).ok();
|
||||||
|
writeln!(out, " ΔGini: {:+.6}", impact.delta_gini).ok();
|
||||||
|
writeln!(out, " Assessment: {}", impact.assessment).ok();
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn graph_trace(args: &serde_json::Value) -> Result<String> {
|
||||||
|
let key = get_str(args, "key")?;
|
||||||
|
|
||||||
|
let arc = cached_store().await?;
|
||||||
|
let store = arc.lock().await;
|
||||||
|
let resolved = store.resolve_key(key).map_err(|e| anyhow::anyhow!("{}", e))?;
|
||||||
|
let g = store.build_graph();
|
||||||
|
|
||||||
|
let node = store.nodes.get(&resolved)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Node not found: {}", resolved))?;
|
||||||
|
|
||||||
|
use std::fmt::Write;
|
||||||
|
let mut out = String::new();
|
||||||
|
|
||||||
|
writeln!(out, "=== {} ===", resolved).ok();
|
||||||
|
writeln!(out, "Type: {:?} Weight: {:.2}", node.node_type, node.weight).ok();
|
||||||
|
if !node.source_ref.is_empty() {
|
||||||
|
writeln!(out, "Source: {}", node.source_ref).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let preview = crate::util::truncate(&node.content, 200, "...");
|
||||||
|
writeln!(out, "\n{}\n", preview).ok();
|
||||||
|
|
||||||
|
// Walk neighbors, grouped by node type
|
||||||
|
let neighbors = g.neighbors(&resolved);
|
||||||
|
let mut episodic_session = Vec::new();
|
||||||
|
let mut episodic_daily = Vec::new();
|
||||||
|
let mut episodic_weekly = Vec::new();
|
||||||
|
let mut semantic = Vec::new();
|
||||||
|
|
||||||
|
for (n, strength) in &neighbors {
|
||||||
|
if let Some(nnode) = store.nodes.get(n.as_str()) {
|
||||||
|
let entry = (n.as_str(), *strength, nnode);
|
||||||
|
match nnode.node_type {
|
||||||
|
crate::store::NodeType::EpisodicSession => episodic_session.push(entry),
|
||||||
|
crate::store::NodeType::EpisodicDaily => episodic_daily.push(entry),
|
||||||
|
crate::store::NodeType::EpisodicWeekly
|
||||||
|
| crate::store::NodeType::EpisodicMonthly => episodic_weekly.push(entry),
|
||||||
|
crate::store::NodeType::Semantic => semantic.push(entry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !episodic_weekly.is_empty() {
|
||||||
|
writeln!(out, "Weekly digests:").ok();
|
||||||
|
for (k, s, n) in &episodic_weekly {
|
||||||
|
let preview = crate::util::first_n_chars(n.content.lines().next().unwrap_or(""), 80);
|
||||||
|
writeln!(out, " [{:.2}] {} — {}", s, k, preview).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !episodic_daily.is_empty() {
|
||||||
|
writeln!(out, "Daily digests:").ok();
|
||||||
|
for (k, s, n) in &episodic_daily {
|
||||||
|
let preview = crate::util::first_n_chars(n.content.lines().next().unwrap_or(""), 80);
|
||||||
|
writeln!(out, " [{:.2}] {} — {}", s, k, preview).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !episodic_session.is_empty() {
|
||||||
|
writeln!(out, "Session entries:").ok();
|
||||||
|
for (k, s, n) in &episodic_session {
|
||||||
|
let preview = crate::util::first_n_chars(
|
||||||
|
n.content.lines()
|
||||||
|
.find(|l| !l.is_empty() && !l.starts_with("<!--"))
|
||||||
|
.unwrap_or(""),
|
||||||
|
80);
|
||||||
|
writeln!(out, " [{:.2}] {}", s, k).ok();
|
||||||
|
if !n.source_ref.is_empty() {
|
||||||
|
writeln!(out, " ↳ source: {}", n.source_ref).ok();
|
||||||
|
}
|
||||||
|
writeln!(out, " {}", preview).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !semantic.is_empty() {
|
||||||
|
writeln!(out, "Semantic links:").ok();
|
||||||
|
for (k, s, _) in &semantic {
|
||||||
|
writeln!(out, " [{:.2}] {}", s, k).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(out, "\nLinks: {} session, {} daily, {} weekly, {} semantic",
|
||||||
|
episodic_session.len(), episodic_daily.len(),
|
||||||
|
episodic_weekly.len(), semantic.len()).ok();
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
|
||||||
109
src/cli/graph.rs
109
src/cli/graph.rs
|
|
@ -70,19 +70,11 @@ pub fn cmd_link_set(source: &str, target: &str, strength: f32) -> Result<(), Str
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cmd_link_impact(source: &str, target: &str) -> Result<(), String> {
|
pub fn cmd_link_impact(source: &str, target: &str) -> Result<(), String> {
|
||||||
let store = store::Store::load()?;
|
let result = crate::mcp_server::memory_rpc(
|
||||||
let source = store.resolve_key(source)?;
|
"graph_link_impact",
|
||||||
let target = store.resolve_key(target)?;
|
serde_json::json!({"source": source, "target": target}),
|
||||||
let g = store.build_graph();
|
).map_err(|e| e.to_string())?;
|
||||||
|
print!("{}", result);
|
||||||
let impact = g.link_impact(&source, &target);
|
|
||||||
|
|
||||||
println!("Link impact: {} → {}", source, target);
|
|
||||||
println!(" Source degree: {} Target degree: {}", impact.source_deg, impact.target_deg);
|
|
||||||
println!(" Hub link: {} Same community: {}", impact.is_hub_link, impact.same_community);
|
|
||||||
println!(" ΔCC source: {:+.4} ΔCC target: {:+.4}", impact.delta_cc_source, impact.delta_cc_target);
|
|
||||||
println!(" ΔGini: {:+.6}", impact.delta_gini);
|
|
||||||
println!(" Assessment: {}", impact.assessment);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,92 +83,11 @@ pub fn cmd_trace(key: &[String]) -> Result<(), String> {
|
||||||
return Err("trace requires a key".into());
|
return Err("trace requires a key".into());
|
||||||
}
|
}
|
||||||
let key = key.join(" ");
|
let key = key.join(" ");
|
||||||
let store = store::Store::load()?;
|
let result = crate::mcp_server::memory_rpc(
|
||||||
let resolved = store.resolve_key(&key)?;
|
"graph_trace",
|
||||||
let g = store.build_graph();
|
serde_json::json!({"key": key}),
|
||||||
|
).map_err(|e| e.to_string())?;
|
||||||
let node = store.nodes.get(&resolved)
|
print!("{}", result);
|
||||||
.ok_or_else(|| format!("Node not found: {}", resolved))?;
|
|
||||||
|
|
||||||
// Display the node itself
|
|
||||||
println!("=== {} ===", resolved);
|
|
||||||
println!("Type: {:?} Weight: {:.2}",
|
|
||||||
node.node_type, node.weight);
|
|
||||||
if !node.source_ref.is_empty() {
|
|
||||||
println!("Source: {}", node.source_ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show content preview
|
|
||||||
let preview = crate::util::truncate(&node.content, 200, "...");
|
|
||||||
println!("\n{}\n", preview);
|
|
||||||
|
|
||||||
// Walk neighbors, grouped by node type
|
|
||||||
let neighbors = g.neighbors(&resolved);
|
|
||||||
let mut episodic_session = Vec::new();
|
|
||||||
let mut episodic_daily = Vec::new();
|
|
||||||
let mut episodic_weekly = Vec::new();
|
|
||||||
let mut semantic = Vec::new();
|
|
||||||
|
|
||||||
for (n, strength) in &neighbors {
|
|
||||||
if let Some(nnode) = store.nodes.get(n.as_str()) {
|
|
||||||
let entry = (n.as_str(), *strength, nnode);
|
|
||||||
match nnode.node_type {
|
|
||||||
store::NodeType::EpisodicSession =>
|
|
||||||
episodic_session.push(entry),
|
|
||||||
store::NodeType::EpisodicDaily =>
|
|
||||||
episodic_daily.push(entry),
|
|
||||||
store::NodeType::EpisodicWeekly
|
|
||||||
| store::NodeType::EpisodicMonthly =>
|
|
||||||
episodic_weekly.push(entry),
|
|
||||||
store::NodeType::Semantic =>
|
|
||||||
semantic.push(entry),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !episodic_weekly.is_empty() {
|
|
||||||
println!("Weekly digests:");
|
|
||||||
for (k, s, n) in &episodic_weekly {
|
|
||||||
let preview = crate::util::first_n_chars(n.content.lines().next().unwrap_or(""), 80);
|
|
||||||
println!(" [{:.2}] {} — {}", s, k, preview);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !episodic_daily.is_empty() {
|
|
||||||
println!("Daily digests:");
|
|
||||||
for (k, s, n) in &episodic_daily {
|
|
||||||
let preview = crate::util::first_n_chars(n.content.lines().next().unwrap_or(""), 80);
|
|
||||||
println!(" [{:.2}] {} — {}", s, k, preview);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !episodic_session.is_empty() {
|
|
||||||
println!("Session entries:");
|
|
||||||
for (k, s, n) in &episodic_session {
|
|
||||||
let preview = crate::util::first_n_chars(
|
|
||||||
n.content.lines()
|
|
||||||
.find(|l| !l.is_empty() && !l.starts_with("<!--"))
|
|
||||||
.unwrap_or(""),
|
|
||||||
80);
|
|
||||||
println!(" [{:.2}] {}", s, k);
|
|
||||||
if !n.source_ref.is_empty() {
|
|
||||||
println!(" ↳ source: {}", n.source_ref);
|
|
||||||
}
|
|
||||||
println!(" {}", preview);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !semantic.is_empty() {
|
|
||||||
println!("Semantic links:");
|
|
||||||
for (k, s, _) in &semantic {
|
|
||||||
println!(" [{:.2}] {}", s, k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\nLinks: {} session, {} daily, {} weekly, {} semantic",
|
|
||||||
episodic_session.len(), episodic_daily.len(),
|
|
||||||
episodic_weekly.len(), semantic.len());
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue