#!/usr/bin/env python3 """kb/add - incremental leaf write (no rebuild). bin/kb/add --text T --root facts|info --source S bin/kb/add --json # stdin: one object or {"leafs":[...]} bin/kb/add --db PATH --json Writes facts+info in one Ladybug transaction. Does not delete kb.lbug. Embedding is used when provided; otherwise model2vec encodes the text. """ from __future__ import annotations import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "bin" / "tools")) from kblib import ( # noqa: E402 EMBED_DIM, add_leafs, connect, ensure_indexes, init_schema, ) def _as_leafs(payload: object) -> list[dict]: if isinstance(payload, list): return [dict(x) for x in payload] if isinstance(payload, dict): if "leafs" in payload: return [dict(x) for x in payload["leafs"]] return [dict(payload)] raise ValueError("json must be an object, a list, or {leafs:[...]}") def _embed_missing(leafs: list[dict]) -> None: missing = [lf for lf in leafs if not lf.get("embedding")] if not missing: return from model2vec import StaticModel model = StaticModel.from_pretrained("minishlab/potion-multilingual-128M") for lf in missing: text = str(lf.get("text") or "") vec = model.encode([text])[0].astype(float).tolist() if len(vec) != EMBED_DIM: vec = (vec + [0.0] * EMBED_DIM)[:EMBED_DIM] lf["embedding"] = vec def main(argv: list[str]) -> int: import argparse p = argparse.ArgumentParser(description="add leafs without rebuilding the brain") p.add_argument("--db", default="", help="path to kb.lbug (default var/kb.lbug)") p.add_argument("--json", action="store_true", help="read leaf JSON from stdin") p.add_argument("--text", default="", help="leaf text") p.add_argument("--root", default="info", choices=("facts", "info")) p.add_argument("--source", default="") p.add_argument("--confidence", default="confirmed") p.add_argument("--source-rev", default="working-tree") p.add_argument("--how", default="brain/add") p.add_argument("--loc", default="") p.add_argument("--type", default="reference", dest="type_") args = p.parse_args(argv) if args.json: raw = sys.stdin.read() if not raw.strip(): print("kb/add: empty stdin", file=sys.stderr) return 2 leafs = _as_leafs(json.loads(raw)) else: if not args.text or not args.source: print("kb/add: --text and --source are required (or --json)", file=sys.stderr) return 2 leafs = [{ "text": args.text, "root": args.root, "source": args.source, "confidence": args.confidence, "source_rev": args.source_rev, "how": args.how, "loc": args.loc or args.source, "type": args.type_, }] for lf in leafs: if not lf.get("text") or not lf.get("source"): print("kb/add: each leaf needs text and source", file=sys.stderr) return 2 _embed_missing(leafs) from kblib import DB_PATH, VAR dbpath = Path(args.db) if args.db else DB_PATH dbpath.parent.mkdir(parents=True, exist_ok=True) VAR.mkdir(exist_ok=True) db, conn = connect(dbpath, read_only=False) init_schema(conn) ids = add_leafs(conn, leafs) ensure_indexes(conn) conn.close() db.close() print(json.dumps({"mode": "add", "ids": ids, "db": str(dbpath)})) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))