@kernlang/agon-dedup 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/classifier.py CHANGED
@@ -23,6 +23,7 @@ Exit codes:
23
23
  from __future__ import annotations
24
24
 
25
25
  import json
26
+ import os
26
27
  import sys
27
28
 
28
29
 
@@ -30,6 +31,20 @@ MIN_CONFIDENCE = 0.10
30
31
  MARGIN = 0.05 # top class must beat #2 by this much to commit; otherwise 'other'
31
32
  MODEL = "sentence-transformers/all-MiniLM-L6-v2"
32
33
 
34
+
35
+ def _cache_dir() -> str:
36
+ """Persistent model cache. fastembed's default is a tmpdir that the OS
37
+ prunes between runs — a half-pruned snapshot then fails with NO_SUCHFILE
38
+ on every call. Pin the cache under ~/.agon (or FASTEMBED_CACHE_PATH)."""
39
+ explicit = os.environ.get("FASTEMBED_CACHE_PATH", "").strip()
40
+ if explicit:
41
+ return explicit
42
+ agon_home = os.environ.get("AGON_HOME", "").strip() or os.path.join(
43
+ os.path.expanduser("~"), ".agon")
44
+ path = os.path.join(agon_home, "cache", "fastembed")
45
+ os.makedirs(path, exist_ok=True)
46
+ return path
47
+
33
48
  # Few-shot examples per label. We embed concrete task phrasings rather than
34
49
  # abstract definitions because all-MiniLM scores concrete-vs-concrete cosine
35
50
  # much higher than concrete-vs-abstract. Empirically this lifts true-positive
@@ -89,7 +104,7 @@ def _classify(text: str) -> dict:
89
104
  file=sys.stderr)
90
105
  sys.exit(2)
91
106
 
92
- embedder = TextEmbedding(MODEL)
107
+ embedder = TextEmbedding(MODEL, cache_dir=_cache_dir())
93
108
  label_keys = list(LABELS.keys())
94
109
  label_descriptions = [LABELS[k] for k in label_keys]
95
110
  all_texts = [text] + label_descriptions
package/embedder.py ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Embedding sidecar — raw sentence embeddings for the RAG retriever.
4
+
5
+ Same model + runtime as sidecar.py (MiniLM via fastembed/ONNX, ~30MB,
6
+ fully offline after the first model download), but instead of clustering
7
+ it returns the raw vectors so the TS side can build a persistent
8
+ cosine-similarity index.
9
+
10
+ Protocol:
11
+ stdin — JSONL, one per line: {"id": "<chunkId>", "text": "<chunk text>"}
12
+ stdout — single JSON:
13
+ {
14
+ "model": "sentence-transformers/all-MiniLM-L6-v2",
15
+ "dims": 384,
16
+ "vectors": [{"id": "<chunkId>", "vector": [..384 floats..]}, ...]
17
+ }
18
+ Vector order matches input order. Vectors are L2-normalized so cosine
19
+ similarity reduces to a dot product on the consumer side.
20
+
21
+ Exit codes:
22
+ 0 — success
23
+ 1 — bad input (malformed JSON, no items)
24
+ 2 — fastembed not installed (caller should fall back / surface a hint)
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import os
31
+ import sys
32
+
33
+
34
+ MODEL = "sentence-transformers/all-MiniLM-L6-v2"
35
+
36
+
37
+ def _cache_dir() -> str:
38
+ """Persistent model cache. fastembed's default is a tmpdir that the OS
39
+ prunes between runs — a half-pruned snapshot then fails with NO_SUCHFILE
40
+ on every call. Pin the cache under ~/.agon (or FASTEMBED_CACHE_PATH)."""
41
+ explicit = os.environ.get("FASTEMBED_CACHE_PATH", "").strip()
42
+ if explicit:
43
+ return explicit
44
+ agon_home = os.environ.get("AGON_HOME", "").strip() or os.path.join(
45
+ os.path.expanduser("~"), ".agon")
46
+ path = os.path.join(agon_home, "cache", "fastembed")
47
+ os.makedirs(path, exist_ok=True)
48
+ return path
49
+
50
+
51
+ def _read_input() -> list[dict[str, str]]:
52
+ items: list[dict[str, str]] = []
53
+ for line_num, raw_line in enumerate(sys.stdin, 1):
54
+ line = raw_line.strip()
55
+ if not line:
56
+ continue
57
+ try:
58
+ obj = json.loads(line)
59
+ except json.JSONDecodeError as err:
60
+ print(f"embedder-sidecar: line {line_num} is not valid JSON: {err}",
61
+ file=sys.stderr)
62
+ sys.exit(1)
63
+ if not isinstance(obj, dict) or "id" not in obj or "text" not in obj:
64
+ print(f"embedder-sidecar: line {line_num} missing 'id' or 'text'",
65
+ file=sys.stderr)
66
+ sys.exit(1)
67
+ items.append({"id": str(obj["id"]), "text": str(obj["text"])})
68
+ return items
69
+
70
+
71
+ def main() -> int:
72
+ items = _read_input()
73
+ if not items:
74
+ print("embedder-sidecar: no input items", file=sys.stderr)
75
+ return 1
76
+
77
+ try:
78
+ import numpy as np
79
+ from fastembed import TextEmbedding
80
+ except ImportError:
81
+ print("embedder-sidecar: fastembed not installed — install via "
82
+ "`pip install fastembed numpy` (see requirements.txt)",
83
+ file=sys.stderr)
84
+ return 2
85
+
86
+ embedder = TextEmbedding(MODEL, cache_dir=_cache_dir())
87
+ texts = [item["text"] for item in items]
88
+ embs = np.array(list(embedder.embed(texts)), dtype=np.float32)
89
+
90
+ # L2-normalize so the consumer can use a plain dot product as cosine.
91
+ norms = np.linalg.norm(embs, axis=1, keepdims=True)
92
+ norms[norms == 0] = 1.0
93
+ embs = embs / norms
94
+
95
+ vectors = [
96
+ {"id": item["id"], "vector": [round(float(x), 7) for x in emb]}
97
+ for item, emb in zip(items, embs)
98
+ ]
99
+ print(json.dumps({"model": MODEL, "dims": int(embs.shape[1]), "vectors": vectors}))
100
+ return 0
101
+
102
+
103
+ if __name__ == "__main__":
104
+ sys.exit(main())
package/history-search.py CHANGED
@@ -34,6 +34,7 @@ Exit codes:
34
34
  from __future__ import annotations
35
35
 
36
36
  import json
37
+ import os
37
38
  import sys
38
39
  from typing import Any
39
40
 
@@ -43,6 +44,20 @@ DEFAULT_TOP_K = 10
43
44
  MIN_SIMILARITY = 0.15 # below this, the match is effectively noise — drop it
44
45
 
45
46
 
47
+ def _cache_dir() -> str:
48
+ """Persistent model cache. fastembed's default is a tmpdir that the OS
49
+ prunes between runs — a half-pruned snapshot then fails with NO_SUCHFILE
50
+ on every call. Pin the cache under ~/.agon (or FASTEMBED_CACHE_PATH)."""
51
+ explicit = os.environ.get("FASTEMBED_CACHE_PATH", "").strip()
52
+ if explicit:
53
+ return explicit
54
+ agon_home = os.environ.get("AGON_HOME", "").strip() or os.path.join(
55
+ os.path.expanduser("~"), ".agon")
56
+ path = os.path.join(agon_home, "cache", "fastembed")
57
+ os.makedirs(path, exist_ok=True)
58
+ return path
59
+
60
+
46
61
  def _read_input() -> dict[str, Any]:
47
62
  raw = sys.stdin.read().strip()
48
63
  if not raw:
@@ -102,7 +117,7 @@ def _rank(payload: dict[str, Any]) -> list[dict[str, Any]]:
102
117
  items: list[dict[str, str]] = payload["items"]
103
118
  top_k: int = payload["top_k"]
104
119
 
105
- embedder = TextEmbedding(MODEL)
120
+ embedder = TextEmbedding(MODEL, cache_dir=_cache_dir())
106
121
  texts = [query] + [item["text"] for item in items]
107
122
  embs = np.array(list(embedder.embed(texts)))
108
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernlang/agon-dedup",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Python sidecars for Agon AI — semantic embeddings (fastembed/MiniLM) and tree-sitter syntax validation. Bridged from KERN via stdin/stdout JSON. Ships .py files that the @kernlang/agon-core bridges spawn at runtime.",
5
5
  "type": "module",
6
6
  "files": [
package/sidecar.py CHANGED
@@ -32,6 +32,7 @@ Exit codes:
32
32
  from __future__ import annotations
33
33
 
34
34
  import json
35
+ import os
35
36
  import sys
36
37
  from typing import Any
37
38
 
@@ -40,6 +41,20 @@ THRESHOLD = 0.55
40
41
  MODEL = "sentence-transformers/all-MiniLM-L6-v2"
41
42
 
42
43
 
44
+ def _cache_dir() -> str:
45
+ """Persistent model cache. fastembed's default is a tmpdir that the OS
46
+ prunes between runs — a half-pruned snapshot then fails with NO_SUCHFILE
47
+ on every call. Pin the cache under ~/.agon (or FASTEMBED_CACHE_PATH)."""
48
+ explicit = os.environ.get("FASTEMBED_CACHE_PATH", "").strip()
49
+ if explicit:
50
+ return explicit
51
+ agon_home = os.environ.get("AGON_HOME", "").strip() or os.path.join(
52
+ os.path.expanduser("~"), ".agon")
53
+ path = os.path.join(agon_home, "cache", "fastembed")
54
+ os.makedirs(path, exist_ok=True)
55
+ return path
56
+
57
+
43
58
  def _read_input() -> list[dict[str, str]]:
44
59
  items: list[dict[str, str]] = []
45
60
  for line_num, raw_line in enumerate(sys.stdin, 1):
@@ -77,7 +92,7 @@ def _cluster(items: list[dict[str, str]]) -> list[dict[str, Any]]:
77
92
  file=sys.stderr)
78
93
  sys.exit(2)
79
94
 
80
- embedder = TextEmbedding(MODEL)
95
+ embedder = TextEmbedding(MODEL, cache_dir=_cache_dir())
81
96
  texts = [item["text"] for item in items]
82
97
  embs = np.array(list(embedder.embed(texts)))
83
98