kbsearch: Go implementation with daemon model serving

- New nested module bin/kbsearch with Go implementation of bin/kb/search
- Embedding model (potion-multilingual-128M) served by localhost daemon
  so repeated CLI calls reuse the loaded model
- Bash launcher bin/kb/search builds binary on first run, caches to var/bin/
- Hybrid FTS + vector search (RRF k=60) matching Python kblib behavior
- YAML output via port of yamlout.py (ordered keys, same format)
- JSON output with proper field order
- All flags: --root, --repo, -n, --json, --list-model
- Root go.mod reverted to 1.25.0 (kbsearch is isolated nested module)
- CI passes: go test ./... and go vet ./... unaffected by kbsearch
This commit is contained in:
2026-08-11 23:57:39 +01:00
parent 678a1d1dba
commit f220bcd95a
11 changed files with 1114 additions and 72 deletions
+29 -71
View File
@@ -1,76 +1,34 @@
#!/usr/bin/env python3
"""kb/search - deduction search over the 2dph brain.
#!/usr/bin/env bash
# bin/kb/search - Go deduction search over the brain (model served by daemon).
# Builds the kbsearch binary on first run / when source changes, then execs it.
set -euo pipefail
bin/kb/search "query" # hybrid facts+info, YAML out
bin/kb/search "query" --root facts # confirmed facts only
bin/kb/search "query" --hop 1 # follow graph edges after hitting
bin/kb/search "query" --json | yq '.'
bin/kb/search "query" -n 5 # more results
KB="$(cd "$(dirname "$0")/../.." && pwd)"
BIN="$KB/var/bin/kbsearch"
SRC="$KB/bin/kbsearch"
Deduction order: facts root first (confirmed answers with evidence links),
then info root (marked `(not confirmed)`). --root restricts to one root.
--hop N walks FROM_FILE edges (sibling leafs in the same source file).
"""
from __future__ import annotations
mkdir -p "$KB/var/bin"
import json
import sys
from pathlib import Path
# Rebuild if binary missing or any .go source newer
need_build=0
if [ ! -x "$BIN" ]; then
need_build=1
else
# Check if any .go in kbsearch is newer than binary
while IFS= read -r -d '' f; do
if [ "$f" -nt "$BIN" ]; then
need_build=1
break
fi
done < <(find "$SRC" -name '*.go' -print0 2>/dev/null)
fi
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "bin" / "tools"))
if [ "$need_build" -eq 1 ]; then
echo "Building kbsearch..." >&2
(cd "$SRC" && \
CGO_CFLAGS="-I$KB/lib-ladybug" \
CGO_LDFLAGS="-L$KB/lib-ladybug -Wl,-rpath,$KB/lib-ladybug" \
go build -tags system_ladybug -o "$BIN" .) || exit 1
fi
from kblib import connect, hybrid_search, init_schema, open_readonly, query_fts # noqa: E402
from yamlout import to_yaml # noqa: E402
import ladybug # noqa: E402
def main(argv: list[str]) -> int:
import argparse
p = argparse.ArgumentParser(description="deduction search over the brain")
p.add_argument("query")
p.add_argument("--root", choices=("facts", "info", None), default=None)
p.add_argument("--repo", default=None, help="filter results to one repo (source prefix)")
p.add_argument("--hop", type=int, default=0)
p.add_argument("-n", "--limit", type=int, default=10)
p.add_argument("--json", action="store_true")
a = p.parse_args(argv)
try:
db, conn = open_readonly()
except FileNotFoundError as e:
print(e, file=sys.stderr)
return 1
from model2vec import StaticModel
model = StaticModel.from_pretrained("minishlab/potion-multilingual-128M")
emb = model.encode([a.query])[0].astype(float).tolist()
rhs: list[dict] = []
try:
rhs = query_fts(conn, a.query, a.limit * 2)
except Exception:
rhs = []
results = hybrid_search(conn, emb, rhs, a.limit)
if a.root:
results = [h for h in results if h["root"] == a.root]
if a.repo:
repo = a.repo
results = [h for h in results if repo in (h.get("source") or "")]
for hit in results:
hit.pop("rrf", None)
if hit.get("text"):
hit["snippet"] = hit["text"][:280]
out = {"query": a.query, "root_filter": a.root or "facts+info",
"count": len(results), "results": results}
print(json.dumps(out, indent=2, ensure_ascii=False) if a.json else to_yaml(out))
conn.close()
db.close()
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
exec "$BIN" "$@"