113 lines
5.0 KiB
Bash
Executable File
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"
|