@kernlang/agon-dedup 0.2.0 → 0.2.3
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 +16 -1
- package/embedder.py +104 -0
- package/history-search.py +16 -1
- package/package.json +1 -1
- package/sidecar.py +16 -1
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.
|
|
3
|
+
"version": "0.2.3",
|
|
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
|
|