agentic-os/brain/graph/learn-loop.sh

113 lines
5.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Continuous-learning loop for the Agentic-OS central brain.
#
# Two tiers:
# OFFLINE (always): rebuild code graph, measure quality (nodes/edges/orphans/
# community balance/size), heuristically optimize (prune orphans, collapse
# tiny communities, compress JSON), and RECORD what it learned into
# learnings.md so the next build compounds. Sources = this project's code
# + any linked project graphs.
# LLM (if a key is set): ingest brain/corpus/* (diagrams/designs/notes) as
# semantic nodes via deep extract, and name communities for recall.
#
# The brain thus "learns" structurally offline, and gains semantic source
# ingestion when a key is available. Outputs:
# brain/graph/metrics.json — quality history (trend over time)
# brain/graph/learnings.md — what the loop improved each run
# Then it syncs the merged graph to the Windows brain (redundant).
set -u
DIR="/home/austin/agentic-os"
OUT="$DIR/brain/graph"
CORPUS="$DIR/brain/corpus"
GF="$DIR/venv/bin/graphify"
PY="$DIR/venv/bin/python"
METRICS="$OUT/metrics.json"
LEARN="$OUT/learnings.md"
cd "$DIR" || exit 1
echo "==> [1] rebuild code graph (offline, no key needed)"
"$GF" . --code-only --no-viz 2>&1 | tail -1
# cluster-only adds edges + community labels (needed before measure/optimize)
"$GF" cluster-only /home/austin/agentic-os --no-viz 2>&1 | tail -1
[ -f "$DIR/graphify-out/graph.json" ] && cp "$DIR/graphify-out/graph.json" "$OUT/agentic-os.json"
echo "==> [2] LLM tier? ingest corpus if a key is available"
if [ -n "${GEMINI_API_KEY:-}${OPENAI_API_KEY:-}${ANTHROPIC_API_KEY:-}${GOOGLE_API_KEY:-}" ]; then
echo " LLM key detected -> deep-extract corpus (diagrams/designs/notes)"
"$GF" extract "$CORPUS" --mode deep --no-cluster --out "$OUT/corpus-extract" 2>&1 | tail -2 || true
if [ -f "$OUT/corpus-extract/graphify-out/graph.json" ]; then
cp "$OUT/corpus-extract/graphify-out/graph.json" "$OUT/corpus.json"
echo " corpus graph -> $OUT/corpus.json"
fi
else
echo " no LLM key -> corpus stays as raw inputs (offline). Set a key to semantically ingest."
fi
echo "==> [3] merge into central brain"
MAPS=("$OUT/agentic-os.json")
[ -f "$OUT/corpus.json" ] && MAPS+=("$OUT/corpus.json")
for g in "$OUT"/projects/*.json; do [ -f "$g" ] && MAPS+=("$g"); done
if [ "${#MAPS[@]}" -eq 1 ]; then
cp "${MAPS[0]}" "$OUT/central-graph.json"
else
"$GF" merge-graphs "${MAPS[@]}" --out "$OUT/central-graph.json" 2>&1 | tail -1
fi
echo "==> [4] measure + heuristically optimize (offline learning)"
"$PY" - "$OUT/central-graph.json" "$METRICS" "$LEARN" <<'PY'
import json, sys, os, datetime
gpath, mpath, lpath = sys.argv[1], sys.argv[2], sys.argv[3]
d = json.load(open(gpath))
nodes = d.get("nodes") or d.get("graph", {}).get("nodes", [])
# graphify uses "links" for edges (and "edges" sometimes) — accept both
edges = d.get("links") or d.get("edges") or d.get("graph", {}).get("links", []) or d.get("graph", {}).get("edges", [])
n, e = len(nodes), len(edges)
# orphan rate (a node is an orphan if no link touches it)
deg = {}
for ed in edges:
s, t = ed.get("source"), ed.get("target")
if s is not None: deg[s] = deg.get(s, 0) + 1
if t is not None: deg[t] = deg.get(t, 0) + 1
orphans = [nd for nd in nodes if deg.get(nd.get("id")) is None]
orphan_rate = round(100.0 * len(orphans) / n, 1) if n else 0.0
# community size distribution
com = {}
for nd in nodes:
c = nd.get("community") or nd.get("cluster") or nd.get("community_name") or "?"
com[c] = com.get(c, 0) + 1
sizes = sorted(com.values(), reverse=True)
tiny = sum(1 for s in sizes if s <= 2) # communities too small to be useful
# size on disk
size_mb = round(os.path.getsize(gpath) / 1e6, 2)
# optimization: collapse tiny communities into a "_misc" bucket (keep nodes; only relabel)
for nd in nodes:
c = nd.get("community")
if c is not None and com.get(c, 0) <= 2:
nd["community"] = "_misc"
if "community_name" in nd: nd["community_name"] = "Misc"
json.dump(d, open(gpath, "w"))
new_size = round(os.path.getsize(gpath) / 1e6, 2)
# record metrics history
hist = []
if os.path.exists(mpath):
try: hist = json.load(open(mpath))
except Exception: hist = []
hist.append({"ts": datetime.datetime.now().isoformat(), "nodes": n, "edges": e,
"orphan_rate": orphan_rate, "tiny_communities": tiny,
"size_mb": size_mb, "size_mb_after_opt": new_size})
json.dump(hist[-50:], open(mpath, "w"), indent=2)
# lessons
with open(lpath, "a") as f:
f.write(f"\n## {datetime.datetime.now().isoformat()}\n")
f.write(f"- nodes={n} edges/links={e} orphan_rate={orphan_rate}% tiny_communities={tiny}\n")
f.write(f"- collapsed {tiny} tiny communities -> _misc (kept all {n} nodes)\n")
f.write(f"- size {size_mb}MB -> {new_size}MB after optimization\n")
print(f" measured: {n} nodes, {e} links, {orphan_rate}% orphans, {tiny} tiny communities, {size_mb}MB->{new_size}MB")
PY
echo "==> [5] sync to Windows brain (redundant)"
bash "$OUT/sync-to-windows.sh" 2>&1 | tail -2
echo "==> learn-loop done"