File size: 6,172 Bytes
06c3755
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
#!/usr/bin/env python3
"""
agent-shell RAG entry point (v0.10.2).

Invoked by Node via child_process.spawn. Two subcommands:

  embed.py embed        # batch-embed texts, upsert into the L3 collection
  embed.py query        # semantic query against the L3 collection

Protocol (line-delimited JSON over stdio):
  - Node writes ONE JSON request object to stdin, then closes stdin.
  - This script writes ONE JSON response object to stdout, then exits.
  - Diagnostic logs go to stderr (Node must not parse stderr).

Request shape (embed):
  {
    "command": "embed",
    "storePath": "<optional, defaults to ~/.databus/state/vectors/chroma>",
    "collection": "<optional, defaults to m3_summaries>",
    "items": [
      { "id": "<string>", "text": "<string>", "stamp": "<string>" },
      ...
    ]
  }

Response shape (embed):
  { "ok": true, "count": <int>, "dim": <int> }
  { "ok": false, "error": "<string>" }

Request shape (query):
  {
    "command": "query",
    "storePath": "<optional>",
    "collection": "<optional>",
    "queryText": "<string>",
    "limit": <optional int, defaults to 5>
  }

Response shape (query):
  {
    "ok": true,
    "results": [
      { "id": "<string>", "stamp": "<string>", "distance": <float>, "document": "<string>" },
      ...
    ]
  }
  { "ok": false, "error": "<string>" }

Model: ONNXMiniLM_L6_V2 (all-MiniLM-L6-v2, 384-dim, cosine).
The model is cached at ~/.cache/chroma/onnx_models/all-MiniLM-L6-v2/ (chromadb default).
"""

import sys
import os
import json
from pathlib import Path


def _get_embedding_function():
    from chromadb.utils.embedding_functions.onnx_mini_lm_l6_v2 import ONNXMiniLM_L6_V2
    return ONNXMiniLM_L6_V2()


def _get_client(store_path: str):
    import chromadb
    resolved = Path(store_path).expanduser()
    resolved.mkdir(parents=True, exist_ok=True)
    return chromadb.PersistentClient(path=str(resolved))


def _get_collection(client, name: str, embedding_function):
    return client.get_or_create_collection(
        name=name,
        embedding_function=embedding_function,
        metadata={"hnsw:space": "cosine"},
    )


def _default_store_path() -> str:
    return str(Path.home() / ".databus" / "state" / "vectors" / "chroma")


def cmd_embed(req: dict) -> dict:
    store_path = req.get("storePath") or _default_store_path()
    collection_name = req.get("collection") or "m3_summaries"
    items = req.get("items") or []

    if not isinstance(items, list) or len(items) == 0:
        return {"ok": False, "error": "items must be a non-empty array"}

    ids = []
    texts = []
    metadatas = []
    for i, item in enumerate(items):
        if not isinstance(item, dict) or "text" not in item:
            return {"ok": False, "error": f"items[{i}] missing 'text'"}
        item_id = item.get("id") or f"item-{i}"
        stamp = item.get("stamp") or ""
        ids.append(str(item_id))
        texts.append(str(item["text"]))
        metadatas.append({"stamp": str(stamp)})

    ef = _get_embedding_function()
    client = _get_client(store_path)
    collection = _get_collection(client, collection_name, ef)

    collection.upsert(ids=ids, documents=texts, metadatas=metadatas)

    sample_vector = ef([texts[0]])[0]
    dim = len(sample_vector)

    return {"ok": True, "count": len(ids), "dim": dim}


def cmd_query(req: dict) -> dict:
    store_path = req.get("storePath") or _default_store_path()
    collection_name = req.get("collection") or "m3_summaries"
    query_text = req.get("queryText")
    limit = req.get("limit") or 5

    if not isinstance(query_text, str) or len(query_text) == 0:
        return {"ok": False, "error": "queryText must be a non-empty string"}

    ef = _get_embedding_function()
    client = _get_client(store_path)
    collection = _get_collection(client, collection_name, ef)

    result = collection.query(
        query_texts=[query_text],
        n_results=int(limit),
        include=["documents", "metadatas", "distances"],
    )

    out = []
    ids_batch = result.get("ids", [[]])[0]
    docs_batch = result.get("documents", [[]])[0]
    metas_batch = result.get("metadatas", [[]])[0]
    dists_batch = result.get("distances", [[]])[0]

    for idx in range(len(ids_batch)):
        out.append({
            "id": ids_batch[idx],
            "stamp": (metas_batch[idx] or {}).get("stamp", ""),
            "distance": float(dists_batch[idx]) if idx < len(dists_batch) else None,
            "document": docs_batch[idx] if idx < len(docs_batch) else "",
        })

    return {"ok": True, "results": out}


def _send(resp: dict) -> None:
    # 显式 UTF-8:Windows 中文系统下 sys.stdout 默认 cp936(GBK),print 会把
    # 中文摘要/错误信息按 GBK 编码发到管道,Node 端按 UTF-8 toString() 得到乱码
    # (甚至半个代理项)。RAG 全文都是 CJK,这一处不对整个 Python 侧输出全废。
    sys.stdout.buffer.write((json.dumps(resp, ensure_ascii=False) + "\n").encode("utf-8"))


def main():
    # 显式 UTF-8 读 stdin:sys.stdin 默认按系统 locale(GBK)解码,shell 管道给
    # 的是 UTF-8 字节——GBK 误读中文会产生孤立代理项(如 \udc86),chromadb 的
    # tokenizer 直接抛 "TextInputSequence must be str in upsert"。这是 RAG 对中文
    # 静默全废的根因(2026-09-16 定位)。
    raw = sys.stdin.buffer.read().decode("utf-8")
    if not raw.strip():
        _send({"ok": False, "error": "empty stdin"})
        sys.exit(0)

    try:
        req = json.loads(raw)
    except json.JSONDecodeError as e:
        _send({"ok": False, "error": f"invalid JSON on stdin: {e}"})
        sys.exit(0)

    command = req.get("command")
    if command == "embed":
        resp = cmd_embed(req)
    elif command == "query":
        resp = cmd_query(req)
    else:
        resp = {"ok": False, "error": f"unknown command: {command!r} (expected 'embed' or 'query')"}

    _send(resp)


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        sys.stderr.write(f"[embed.py] unhandled: {type(e).__name__}: {e}\n")
        _send({"ok": False, "error": f"{type(e).__name__}: {e}"})
        sys.exit(0)