Add calibrate agent, link-set command, and dominating-set query stage
calibrate.agent: Haiku-based agent that reads a node and all its neighbors, then assigns appropriate link strengths relative to each other. Designed for high-volume runs across the whole graph. graph link-set: Set strength of an existing link (0.0-1.0). dominating-set query stage: Greedy 3-covering dominating set — finds the minimum set of nodes such that every node in the input is within 1 hop of at least 3 selected nodes. Use with calibrate agent to ensure every link gets assessed from multiple perspectives. Usage: poc-memory query "content ~ 'bcachefs' | dominating-set"
This commit is contained in:
parent
7fc1270d6f
commit
19e181665d
5 changed files with 166 additions and 1 deletions
56
poc-memory/agents/calibrate.agent
Normal file
56
poc-memory/agents/calibrate.agent
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
{"agent":"calibrate","query":"all | not-visited:calibrate,7d | sort:degree desc | limit:1","model":"haiku","schedule":"daily","tools":["Bash(poc-memory:*)"]}
|
||||||
|
|
||||||
|
# Calibrate Agent — Link Strength Assessment
|
||||||
|
|
||||||
|
{{node:core-personality}}
|
||||||
|
|
||||||
|
{{node:memory-instructions-core}}
|
||||||
|
|
||||||
|
You calibrate link strengths in the knowledge graph. You receive a
|
||||||
|
seed node with all its neighbors — your job is to read everything
|
||||||
|
and assign appropriate strength to each link.
|
||||||
|
|
||||||
|
## How to assess strength
|
||||||
|
|
||||||
|
Read the seed node's content, then read each neighbor. For each link,
|
||||||
|
judge how strongly related they actually are:
|
||||||
|
|
||||||
|
- **0.8–1.0** — core relationship. One defines or is essential to the other.
|
||||||
|
Parent-child, same concept different depth, direct dependency.
|
||||||
|
- **0.5–0.7** — strong relationship. Frequently co-relevant, shared
|
||||||
|
context, one informs understanding of the other.
|
||||||
|
- **0.2–0.4** — moderate relationship. Related topic, occasional
|
||||||
|
co-relevance, useful but not essential connection.
|
||||||
|
- **0.05–0.15** — weak relationship. Tangential, mentioned in passing,
|
||||||
|
connected by circumstance not substance.
|
||||||
|
|
||||||
|
## How to work
|
||||||
|
|
||||||
|
For the seed node, read it and all its neighbors. Then for each
|
||||||
|
neighbor, set the link strength:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poc-memory graph link-set SEED_KEY NEIGHBOR_KEY STRENGTH
|
||||||
|
```
|
||||||
|
|
||||||
|
Think about the strengths *relative to each other*. If node A has
|
||||||
|
10 neighbors, they can't all be 0.8 — rank them and spread the
|
||||||
|
strengths accordingly.
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- **Read before judging.** Don't guess from key names alone.
|
||||||
|
- **Calibrate relatively.** The strongest link from this node should
|
||||||
|
be stronger than the weakest. Use the full range.
|
||||||
|
- **Journal→topic links are usually weak (0.1–0.3).** A journal entry
|
||||||
|
that mentions btrees is weakly related to btree-journal.
|
||||||
|
- **Topic→subtopic links are strong (0.6–0.9).** btree-journal and
|
||||||
|
btree-journal-txn-restart are tightly related.
|
||||||
|
- **Hub→leaf links vary.** bcachefs→kernel-patterns is moderate (0.4),
|
||||||
|
bcachefs→some-random-journal is weak (0.1).
|
||||||
|
- **Don't remove links.** Only adjust strength. If a link shouldn't
|
||||||
|
exist at all, set it to 0.05.
|
||||||
|
|
||||||
|
## Seed node
|
||||||
|
|
||||||
|
{{organize}}
|
||||||
|
|
@ -178,6 +178,35 @@ pub fn cmd_link_add(source: &str, target: &str, reason: &[String]) -> Result<(),
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cmd_link_set(source: &str, target: &str, strength: f32) -> Result<(), String> {
|
||||||
|
super::check_dry_run();
|
||||||
|
let mut store = store::Store::load()?;
|
||||||
|
let source = store.resolve_key(source)?;
|
||||||
|
let target = store.resolve_key(target)?;
|
||||||
|
let strength = strength.clamp(0.01, 1.0);
|
||||||
|
|
||||||
|
let mut found = false;
|
||||||
|
for rel in &mut store.relations {
|
||||||
|
if rel.deleted { continue; }
|
||||||
|
if (rel.source_key == source && rel.target_key == target)
|
||||||
|
|| (rel.source_key == target && rel.target_key == source)
|
||||||
|
{
|
||||||
|
let old = rel.strength;
|
||||||
|
rel.strength = strength;
|
||||||
|
println!("Set: {} ↔ {} strength {:.2} → {:.2}", source, target, old, strength);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return Err(format!("No link found between {} and {}", source, target));
|
||||||
|
}
|
||||||
|
|
||||||
|
store.save()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
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 store = store::Store::load()?;
|
||||||
let source = store.resolve_key(source)?;
|
let source = store.resolve_key(source)?;
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,16 @@ enum GraphCmd {
|
||||||
/// Optional reason
|
/// Optional reason
|
||||||
reason: Vec<String>,
|
reason: Vec<String>,
|
||||||
},
|
},
|
||||||
|
/// Set strength of an existing link
|
||||||
|
#[command(name = "link-set")]
|
||||||
|
LinkSet {
|
||||||
|
/// Source node key
|
||||||
|
source: String,
|
||||||
|
/// Target node key
|
||||||
|
target: String,
|
||||||
|
/// Strength (0.0–1.0)
|
||||||
|
strength: f32,
|
||||||
|
},
|
||||||
/// Simulate adding an edge, report topology impact
|
/// Simulate adding an edge, report topology impact
|
||||||
#[command(name = "link-impact")]
|
#[command(name = "link-impact")]
|
||||||
LinkImpact {
|
LinkImpact {
|
||||||
|
|
@ -775,6 +785,8 @@ fn main() {
|
||||||
GraphCmd::Link { key } => cli::graph::cmd_link(&key),
|
GraphCmd::Link { key } => cli::graph::cmd_link(&key),
|
||||||
GraphCmd::LinkAdd { source, target, reason }
|
GraphCmd::LinkAdd { source, target, reason }
|
||||||
=> cli::graph::cmd_link_add(&source, &target, &reason),
|
=> cli::graph::cmd_link_add(&source, &target, &reason),
|
||||||
|
GraphCmd::LinkSet { source, target, strength }
|
||||||
|
=> cli::graph::cmd_link_set(&source, &target, strength),
|
||||||
GraphCmd::LinkImpact { source, target }
|
GraphCmd::LinkImpact { source, target }
|
||||||
=> cli::graph::cmd_link_impact(&source, &target),
|
=> cli::graph::cmd_link_impact(&source, &target),
|
||||||
GraphCmd::LinkAudit { apply } => cli::graph::cmd_link_audit(apply),
|
GraphCmd::LinkAudit { apply } => cli::graph::cmd_link_audit(apply),
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,7 @@ pub enum Filter {
|
||||||
pub enum Transform {
|
pub enum Transform {
|
||||||
Sort(SortField),
|
Sort(SortField),
|
||||||
Limit(usize),
|
Limit(usize),
|
||||||
|
DominatingSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
@ -257,6 +258,11 @@ impl Stage {
|
||||||
return Ok(Stage::Generator(Generator::All));
|
return Ok(Stage::Generator(Generator::All));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transform: "dominating-set"
|
||||||
|
if s == "dominating-set" {
|
||||||
|
return Ok(Stage::Transform(Transform::DominatingSet));
|
||||||
|
}
|
||||||
|
|
||||||
// Try algorithm parse first (bare words, no colon)
|
// Try algorithm parse first (bare words, no colon)
|
||||||
if !s.contains(':') {
|
if !s.contains(':') {
|
||||||
if let Ok(algo) = AlgoStage::parse(s) {
|
if let Ok(algo) = AlgoStage::parse(s) {
|
||||||
|
|
@ -348,6 +354,7 @@ impl fmt::Display for Stage {
|
||||||
Stage::Filter(filt) => write!(f, "{}", filt),
|
Stage::Filter(filt) => write!(f, "{}", filt),
|
||||||
Stage::Transform(Transform::Sort(field)) => write!(f, "sort:{:?}", field),
|
Stage::Transform(Transform::Sort(field)) => write!(f, "sort:{:?}", field),
|
||||||
Stage::Transform(Transform::Limit(n)) => write!(f, "limit:{}", n),
|
Stage::Transform(Transform::Limit(n)) => write!(f, "limit:{}", n),
|
||||||
|
Stage::Transform(Transform::DominatingSet) => write!(f, "dominating-set"),
|
||||||
Stage::Algorithm(a) => write!(f, "{}", a.algo),
|
Stage::Algorithm(a) => write!(f, "{}", a.algo),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -508,7 +515,7 @@ fn eval_filter(filt: &Filter, key: &str, store: &Store, now: i64) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_transform(
|
pub fn run_transform(
|
||||||
xform: &Transform,
|
xform: &Transform,
|
||||||
mut items: Vec<(String, f64)>,
|
mut items: Vec<(String, f64)>,
|
||||||
store: &Store,
|
store: &Store,
|
||||||
|
|
@ -564,6 +571,56 @@ fn run_transform(
|
||||||
items.truncate(*n);
|
items.truncate(*n);
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
Transform::DominatingSet => {
|
||||||
|
// Greedy 3-covering dominating set: pick the node that covers
|
||||||
|
// the most under-covered neighbors, repeat until every node
|
||||||
|
// has been covered 3 times (by 3 different selected seeds).
|
||||||
|
use std::collections::HashMap as HMap;
|
||||||
|
let input_keys: std::collections::HashSet<String> = items.iter().map(|(k, _)| k.clone()).collect();
|
||||||
|
let mut cover_count: HMap<String, usize> = items.iter().map(|(k, _)| (k.clone(), 0)).collect();
|
||||||
|
let mut selected: Vec<(String, f64)> = Vec::new();
|
||||||
|
let mut selected_set: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||||
|
const REQUIRED_COVERAGE: usize = 3;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Find the unselected node that covers the most under-covered nodes
|
||||||
|
let best = items.iter()
|
||||||
|
.filter(|(k, _)| !selected_set.contains(k.as_str()))
|
||||||
|
.map(|(k, _)| {
|
||||||
|
let mut value = 0usize;
|
||||||
|
// Count self if under-covered
|
||||||
|
if cover_count.get(k).copied().unwrap_or(0) < REQUIRED_COVERAGE {
|
||||||
|
value += 1;
|
||||||
|
}
|
||||||
|
for (nbr, _) in graph.neighbors(k) {
|
||||||
|
if input_keys.contains(nbr.as_str()) {
|
||||||
|
if cover_count.get(nbr.as_str()).copied().unwrap_or(0) < REQUIRED_COVERAGE {
|
||||||
|
value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(k.clone(), value)
|
||||||
|
})
|
||||||
|
.max_by_key(|(_, v)| *v);
|
||||||
|
|
||||||
|
let Some((key, value)) = best else { break };
|
||||||
|
if value == 0 { break; } // everything covered 3x
|
||||||
|
|
||||||
|
// Mark coverage
|
||||||
|
*cover_count.entry(key.clone()).or_default() += 1;
|
||||||
|
for (nbr, _) in graph.neighbors(&key) {
|
||||||
|
if let Some(c) = cover_count.get_mut(nbr.as_str()) {
|
||||||
|
*c += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let score = items.iter().find(|(k, _)| k == &key).map(|(_, s)| *s).unwrap_or(1.0);
|
||||||
|
selected.push((key.clone(), score));
|
||||||
|
selected_set.insert(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
selected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ pub enum Stage {
|
||||||
Select(Vec<String>),
|
Select(Vec<String>),
|
||||||
Count,
|
Count,
|
||||||
Connectivity,
|
Connectivity,
|
||||||
|
DominatingSet,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -90,6 +91,7 @@ peg::parser! {
|
||||||
/ "select" _ f:field_list() { Stage::Select(f) }
|
/ "select" _ f:field_list() { Stage::Select(f) }
|
||||||
/ "count" { Stage::Count }
|
/ "count" { Stage::Count }
|
||||||
/ "connectivity" { Stage::Connectivity }
|
/ "connectivity" { Stage::Connectivity }
|
||||||
|
/ "dominating-set" { Stage::DominatingSet }
|
||||||
|
|
||||||
rule asc_desc() -> bool
|
rule asc_desc() -> bool
|
||||||
= "asc" { true }
|
= "asc" { true }
|
||||||
|
|
@ -425,6 +427,15 @@ fn execute_parsed(
|
||||||
}
|
}
|
||||||
Stage::Connectivity => {} // handled in output
|
Stage::Connectivity => {} // handled in output
|
||||||
Stage::Select(_) | Stage::Count => {} // handled in output
|
Stage::Select(_) | Stage::Count => {} // handled in output
|
||||||
|
Stage::DominatingSet => {
|
||||||
|
let mut items: Vec<(String, f64)> = results.iter()
|
||||||
|
.map(|r| (r.key.clone(), graph.degree(&r.key) as f64))
|
||||||
|
.collect();
|
||||||
|
let xform = super::engine::Transform::DominatingSet;
|
||||||
|
items = super::engine::run_transform(&xform, items, store, &graph);
|
||||||
|
let keep: std::collections::HashSet<String> = items.into_iter().map(|(k, _)| k).collect();
|
||||||
|
results.retain(|r| keep.contains(&r.key));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue