dsh-data-domain-knowledge 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +360 -0
- package/cordis.patch.yml +410 -0
- package/eval/README.md +76 -0
- package/eval/evaluate-retrieval.mjs +121 -0
- package/eval/queries.example.jsonl +2 -0
- package/lib/client.js +111415 -0
- package/lib/types/config-schema.d.ts +35 -0
- package/lib/types/config-schema.d.ts.map +1 -0
- package/lib/types/config-schema.js +34 -0
- package/lib/types/config-schema.js.map +1 -0
- package/lib/types/convert/core-loader.d.ts +32 -0
- package/lib/types/convert/core-loader.d.ts.map +1 -0
- package/lib/types/convert/core-loader.js +60 -0
- package/lib/types/convert/core-loader.js.map +1 -0
- package/lib/types/convert/host.d.ts +88 -0
- package/lib/types/convert/host.d.ts.map +1 -0
- package/lib/types/convert/host.js +479 -0
- package/lib/types/convert/host.js.map +1 -0
- package/lib/types/convert/ingest.d.ts +125 -0
- package/lib/types/convert/ingest.d.ts.map +1 -0
- package/lib/types/convert/ingest.js +228 -0
- package/lib/types/convert/ingest.js.map +1 -0
- package/lib/types/convert/settings-schema.d.ts +18 -0
- package/lib/types/convert/settings-schema.d.ts.map +1 -0
- package/lib/types/convert/settings-schema.js +26 -0
- package/lib/types/convert/settings-schema.js.map +1 -0
- package/lib/types/convert/types.d.ts +46 -0
- package/lib/types/convert/types.d.ts.map +1 -0
- package/lib/types/convert/types.js +41 -0
- package/lib/types/convert/types.js.map +1 -0
- package/lib/types/diagnosis/cards.d.ts +71 -0
- package/lib/types/diagnosis/cards.d.ts.map +1 -0
- package/lib/types/diagnosis/cards.js +482 -0
- package/lib/types/diagnosis/cards.js.map +1 -0
- package/lib/types/diagnosis/problem-classes.d.ts +51 -0
- package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
- package/lib/types/diagnosis/problem-classes.js +66 -0
- package/lib/types/diagnosis/problem-classes.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +65 -0
- package/lib/types/diagnosis/schema.d.ts.map +1 -0
- package/lib/types/diagnosis/schema.js +208 -0
- package/lib/types/diagnosis/schema.js.map +1 -0
- package/lib/types/diagnosis/seed-source.d.ts +20 -0
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
- package/lib/types/diagnosis/seed-source.js +96 -0
- package/lib/types/diagnosis/seed-source.js.map +1 -0
- package/lib/types/diagnosis/seed.d.ts +50 -0
- package/lib/types/diagnosis/seed.d.ts.map +1 -0
- package/lib/types/diagnosis/seed.js +242 -0
- package/lib/types/diagnosis/seed.js.map +1 -0
- package/lib/types/diagnosis/store.d.ts +81 -0
- package/lib/types/diagnosis/store.d.ts.map +1 -0
- package/lib/types/diagnosis/store.js +409 -0
- package/lib/types/diagnosis/store.js.map +1 -0
- package/lib/types/diagnosis/types.d.ts +124 -0
- package/lib/types/diagnosis/types.d.ts.map +1 -0
- package/lib/types/diagnosis/types.js +13 -0
- package/lib/types/diagnosis/types.js.map +1 -0
- package/lib/types/directory-picker.d.ts +94 -0
- package/lib/types/directory-picker.d.ts.map +1 -0
- package/lib/types/directory-picker.js +110 -0
- package/lib/types/directory-picker.js.map +1 -0
- package/lib/types/extract-model.d.ts +76 -0
- package/lib/types/extract-model.d.ts.map +1 -0
- package/lib/types/extract-model.js +193 -0
- package/lib/types/extract-model.js.map +1 -0
- package/lib/types/extract.d.ts +121 -0
- package/lib/types/extract.d.ts.map +1 -0
- package/lib/types/extract.js +549 -0
- package/lib/types/extract.js.map +1 -0
- package/lib/types/fts.d.ts +96 -0
- package/lib/types/fts.d.ts.map +1 -0
- package/lib/types/fts.js +184 -0
- package/lib/types/fts.js.map +1 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +1112 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/intent/engine.d.ts +108 -0
- package/lib/types/intent/engine.d.ts.map +1 -0
- package/lib/types/intent/engine.js +501 -0
- package/lib/types/intent/engine.js.map +1 -0
- package/lib/types/intent/host.d.ts +88 -0
- package/lib/types/intent/host.d.ts.map +1 -0
- package/lib/types/intent/host.js +389 -0
- package/lib/types/intent/host.js.map +1 -0
- package/lib/types/intent/preset-tools.d.ts +37 -0
- package/lib/types/intent/preset-tools.d.ts.map +1 -0
- package/lib/types/intent/preset-tools.js +45 -0
- package/lib/types/intent/preset-tools.js.map +1 -0
- package/lib/types/intent/settings-schema.d.ts +59 -0
- package/lib/types/intent/settings-schema.d.ts.map +1 -0
- package/lib/types/intent/settings-schema.js +57 -0
- package/lib/types/intent/settings-schema.js.map +1 -0
- package/lib/types/intent/store.d.ts +75 -0
- package/lib/types/intent/store.d.ts.map +1 -0
- package/lib/types/intent/store.js +139 -0
- package/lib/types/intent/store.js.map +1 -0
- package/lib/types/intent/tools.d.ts +63 -0
- package/lib/types/intent/tools.d.ts.map +1 -0
- package/lib/types/intent/tools.js +108 -0
- package/lib/types/intent/tools.js.map +1 -0
- package/lib/types/intent/types.d.ts +189 -0
- package/lib/types/intent/types.d.ts.map +1 -0
- package/lib/types/intent/types.js +62 -0
- package/lib/types/intent/types.js.map +1 -0
- package/lib/types/knowledge-bases.d.ts +266 -0
- package/lib/types/knowledge-bases.d.ts.map +1 -0
- package/lib/types/knowledge-bases.js +1107 -0
- package/lib/types/knowledge-bases.js.map +1 -0
- package/lib/types/layout.d.ts +262 -0
- package/lib/types/layout.d.ts.map +1 -0
- package/lib/types/layout.js +312 -0
- package/lib/types/layout.js.map +1 -0
- package/lib/types/migrate.d.ts +110 -0
- package/lib/types/migrate.d.ts.map +1 -0
- package/lib/types/migrate.js +280 -0
- package/lib/types/migrate.js.map +1 -0
- package/lib/types/recall-rank.d.ts +158 -0
- package/lib/types/recall-rank.d.ts.map +1 -0
- package/lib/types/recall-rank.js +261 -0
- package/lib/types/recall-rank.js.map +1 -0
- package/lib/types/runtime.d.ts +568 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/runtime.js +3045 -0
- package/lib/types/runtime.js.map +1 -0
- package/lib/types/settings-schema.d.ts +117 -0
- package/lib/types/settings-schema.d.ts.map +1 -0
- package/lib/types/settings-schema.js +92 -0
- package/lib/types/settings-schema.js.map +1 -0
- package/lib/types/settings-volatile.d.ts +102 -0
- package/lib/types/settings-volatile.d.ts.map +1 -0
- package/lib/types/settings-volatile.js +114 -0
- package/lib/types/settings-volatile.js.map +1 -0
- package/lib/types/types.d.ts +799 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +387 -0
- package/lib/types/types.js.map +1 -0
- package/lib/types/wiki-types.d.ts +68 -0
- package/lib/types/wiki-types.d.ts.map +1 -0
- package/lib/types/wiki-types.js +322 -0
- package/lib/types/wiki-types.js.map +1 -0
- package/package.json +101 -0
- package/preset/tita-data-agent/agent.cordis.yml +376 -0
- package/preset/tita-data-agent/preset.yml +3 -0
- package/python/requirements.txt +8 -0
- package/scripts/doctor.mjs +70 -0
- package/scripts/e2e-pipeline.mjs +355 -0
- package/scripts/install-agent-preset.mjs +92 -0
- package/scripts/setup-embedding.sh +34 -0
- package/scripts/sync-agent-preset.mjs +188 -0
- package/src/client/AgentPresetCard.tsx +247 -0
- package/src/client/DirectoryPicker.tsx +357 -0
- package/src/client/InfoHint.tsx +65 -0
- package/src/client/IntentLogicTab.tsx +185 -0
- package/src/client/IntentTab.tsx +715 -0
- package/src/client/KnowledgeBaseTab.tsx +1454 -0
- package/src/client/KnowledgeGraphView.tsx +306 -0
- package/src/client/KnowledgePanel.tsx +381 -0
- package/src/client/KnowledgeStepRail.tsx +60 -0
- package/src/client/NodeSettings.tsx +392 -0
- package/src/client/PreviewSections.tsx +116 -0
- package/src/client/RecallPanel.tsx +399 -0
- package/src/client/TypeDefinitionDrawer.tsx +215 -0
- package/src/client/WikiFlowCard.tsx +226 -0
- package/src/client/WikiTypeEditor.tsx +332 -0
- package/src/client/agent-preset.ts +121 -0
- package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
- package/src/client/arch/NodeLogicPanel.tsx +133 -0
- package/src/client/arch/ProblemDetailPanel.tsx +88 -0
- package/src/client/arch/model.ts +691 -0
- package/src/client/architecture.css +191 -0
- package/src/client/architecture.tsx +530 -0
- package/src/client/attribution/AttributionGraph.tsx +104 -0
- package/src/client/attribution/AttributionTab.tsx +332 -0
- package/src/client/attribution/CaseView.tsx +102 -0
- package/src/client/attribution/GraphStage.tsx +87 -0
- package/src/client/attribution/RunView.tsx +184 -0
- package/src/client/attribution/StageChrome.tsx +403 -0
- package/src/client/attribution/ViewSwitch.tsx +107 -0
- package/src/client/attribution/graph-options.ts +381 -0
- package/src/client/attribution/model.ts +462 -0
- package/src/client/config-scope.ts +119 -0
- package/src/client/convert/ConvertPanel.tsx +594 -0
- package/src/client/convert/styles.css +138 -0
- package/src/client/convert/styles.ts +49 -0
- package/src/client/format.ts +39 -0
- package/src/client/graph/layout.ts +181 -0
- package/src/client/graph.css +113 -0
- package/src/client/index.ts +98 -0
- package/src/client/problem-domains.ts +114 -0
- package/src/client/styles.css +940 -0
- package/src/client/styles.ts +11 -0
- package/src/client/useFitScale.ts +68 -0
- package/src/config-schema.ts +45 -0
- package/src/convert/core-loader.ts +68 -0
- package/src/convert/host.ts +579 -0
- package/src/convert/ingest.ts +367 -0
- package/src/convert/settings-schema.ts +27 -0
- package/src/convert/types.ts +65 -0
- package/src/diagnosis/cards.ts +498 -0
- package/src/diagnosis/problem-classes.ts +117 -0
- package/src/diagnosis/schema.ts +260 -0
- package/src/diagnosis/seed-source.ts +107 -0
- package/src/diagnosis/seed.ts +291 -0
- package/src/diagnosis/store.ts +432 -0
- package/src/diagnosis/types.ts +152 -0
- package/src/directory-picker.ts +164 -0
- package/src/extract-model.ts +227 -0
- package/src/extract.ts +632 -0
- package/src/fts.ts +203 -0
- package/src/index.ts +1091 -0
- package/src/intent/README.md +63 -0
- package/src/intent/engine.ts +527 -0
- package/src/intent/host.ts +462 -0
- package/src/intent/preset-tools.ts +48 -0
- package/src/intent/settings-schema.ts +63 -0
- package/src/intent/store.ts +143 -0
- package/src/intent/tools.ts +139 -0
- package/src/intent/types.ts +245 -0
- package/src/knowledge-bases.ts +1160 -0
- package/src/layout.ts +350 -0
- package/src/migrate.ts +262 -0
- package/src/python/embedding_worker.py +471 -0
- package/src/recall-rank.ts +299 -0
- package/src/runtime.ts +3069 -0
- package/src/settings-schema.ts +94 -0
- package/src/settings-volatile.ts +122 -0
- package/src/types.ts +1140 -0
- package/src/wiki-types.ts +357 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Small JSONL bridge for the local WeMM SentenceTransformers backend.
|
|
3
|
+
|
|
4
|
+
The Host deliberately talks to this process over stdin/stdout instead of
|
|
5
|
+
importing Python from Node. That keeps the DSH plugin installable without a
|
|
6
|
+
Python dependency while still making the real multimodal model a first-class
|
|
7
|
+
local backend on macOS (MPS) and Linux (CPU/CUDA as selected by PyTorch).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
from contextlib import redirect_stdout
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
MODEL_CACHE: dict[tuple[str, str], tuple[Any, str]] = {}
|
|
22
|
+
# (model, device, health) — the health dict is filled once, at load, by
|
|
23
|
+
# `reranker_health` and reused for every request in this process.
|
|
24
|
+
RERANK_CACHE: dict[tuple[str, str], tuple[Any, str, dict[str, Any]]] = {}
|
|
25
|
+
|
|
26
|
+
# A real XLM-R vocabulary is ~250k entries. A *degenerate* one still loads and
|
|
27
|
+
# still returns numbers: with `sentencepiece.bpe.model` / `tokenizer.json` missing
|
|
28
|
+
# from the local snapshot, `XLMRobertaTokenizer` came up with `vocab_size = 5`
|
|
29
|
+
# (the five special tokens only) and tokenized every input to `<s> <unk>… </s>`.
|
|
30
|
+
# Measured: three texts (relevant / unrelated-topic / pure noise) scored
|
|
31
|
+
# 0.999, 0.998, 0.9994 — i.e. the "reranker" was ranking `<unk>` soup, and stage 4
|
|
32
|
+
# reported `status: ready` with `dropped: 0` the whole time. Silent, so it has to
|
|
33
|
+
# be a hard error at load rather than something a caller might notice.
|
|
34
|
+
RERANK_MIN_VOCAB = 1000
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def emit(value: dict[str, Any]) -> None:
|
|
38
|
+
sys.stdout.write(json.dumps(value, ensure_ascii=False, separators=(",", ":")) + "\n")
|
|
39
|
+
sys.stdout.flush()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def choose_device(requested: str) -> str:
|
|
43
|
+
if requested in {"mps", "cpu"}:
|
|
44
|
+
return requested
|
|
45
|
+
try:
|
|
46
|
+
import torch
|
|
47
|
+
|
|
48
|
+
if getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available():
|
|
49
|
+
return "mps"
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
return "cpu"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_local_first(build):
|
|
56
|
+
"""Build a HuggingFace-backed model **from the local cache first**.
|
|
57
|
+
|
|
58
|
+
A machine that already has the weights must not need huggingface.co to answer
|
|
59
|
+
a query. `SentenceTransformer(...)` / `CrossEncoder(...)` resolve a bare repo id
|
|
60
|
+
by contacting the Hub, and with no egress that call does not fail fast — it
|
|
61
|
+
*hangs* until the plugin's timeout. Measured on this machine: the reranker
|
|
62
|
+
weights were fully present in `~/.cache/huggingface/hub` (1.0G), yet the first
|
|
63
|
+
rerank after startup spent the whole 60s timeout trying to reach the Hub and
|
|
64
|
+
came back as `EMBEDDING_TIMEOUT` — the wrong subsystem *and* the wrong cause.
|
|
65
|
+
`local_files_only=True` turns a cached model into an instant offline load; the
|
|
66
|
+
networked attempt stays for the genuinely-uncached first run.
|
|
67
|
+
|
|
68
|
+
Note what this flag does **not** prove: the first attempt below succeeds on a
|
|
69
|
+
*partially* downloaded snapshot too. That is exactly how the missing reranker
|
|
70
|
+
tokenizer went unnoticed — `local_files_only=True` loaded in 0.1s and returned
|
|
71
|
+
a tokenizer with an empty vocabulary. "Loads offline" is not "is usable";
|
|
72
|
+
`reranker_health` is what checks the second half.
|
|
73
|
+
|
|
74
|
+
`build(local_only)` receives the flag because the argument has to reach the
|
|
75
|
+
constructor, and only the caller knows its exact signature.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
return build(True)
|
|
79
|
+
except Exception:
|
|
80
|
+
# Uncached cache, or a cache that cannot satisfy the load: let the normal
|
|
81
|
+
# path run so its own error — which names the real problem — surfaces.
|
|
82
|
+
return build(False)
|
|
83
|
+
|
|
84
|
+
def load_model(model_name: str, requested_device: str):
|
|
85
|
+
import torch
|
|
86
|
+
from sentence_transformers import SentenceTransformer
|
|
87
|
+
|
|
88
|
+
device = choose_device(requested_device)
|
|
89
|
+
# Do not force torch_dtype here. The quantized checkpoint owns its weight
|
|
90
|
+
# loading policy; forcing a dtype breaks some custom WeMM loaders.
|
|
91
|
+
if device == "mps":
|
|
92
|
+
torch.set_float32_matmul_precision("high")
|
|
93
|
+
def load(path: str, target: str, local_only: bool = False):
|
|
94
|
+
kwargs = {"local_files_only": True} if local_only else {}
|
|
95
|
+
return SentenceTransformer(path, trust_remote_code=True, device=target, **kwargs)
|
|
96
|
+
|
|
97
|
+
def move_float8_storage_to_mps(model: Any) -> None:
|
|
98
|
+
# MPS does not expose Float8_e4m3fn tensors. The quantized checkpoint
|
|
99
|
+
# uses FP8 buffers for a subset of weights, while its forward methods
|
|
100
|
+
# already dequantize them to float32/bfloat16. Converting those buffers
|
|
101
|
+
# before the device move preserves the computation and makes the
|
|
102
|
+
# checkpoint usable on Apple Silicon; the W4A8 uint8 buffers stay packed.
|
|
103
|
+
float8 = getattr(torch, "float8_e4m3fn", None)
|
|
104
|
+
if float8 is None:
|
|
105
|
+
raise RuntimeError("MPS_FLOAT8_UNAVAILABLE")
|
|
106
|
+
for module in model.modules():
|
|
107
|
+
for name, buffer in list(module._buffers.items()):
|
|
108
|
+
if buffer is not None and buffer.dtype == float8:
|
|
109
|
+
module._buffers[name] = buffer.to(dtype=torch.bfloat16)
|
|
110
|
+
|
|
111
|
+
# The released W4A8/FP8 checkpoint intentionally omits lm_head.weight, but
|
|
112
|
+
# its config still says tie_word_embeddings=true. Transformers then tries
|
|
113
|
+
# to assign the FP8 input buffer to an nn.Parameter and aborts. Keep the
|
|
114
|
+
# downloaded model immutable and load through a tiny symlinked directory
|
|
115
|
+
# with only this inference-irrelevant config flag corrected.
|
|
116
|
+
patched_dir = None
|
|
117
|
+
load_path = model_name
|
|
118
|
+
if os.path.isdir(model_name):
|
|
119
|
+
config_path = os.path.join(model_name, "config.json")
|
|
120
|
+
try:
|
|
121
|
+
with open(config_path, "r", encoding="utf-8") as handle:
|
|
122
|
+
config = json.load(handle)
|
|
123
|
+
if config.get("tie_word_embeddings") is True:
|
|
124
|
+
patched_dir = tempfile.mkdtemp(prefix="tita-wemm-")
|
|
125
|
+
for name in os.listdir(model_name):
|
|
126
|
+
source = os.path.join(model_name, name)
|
|
127
|
+
target = os.path.join(patched_dir, name)
|
|
128
|
+
if name == "config.json":
|
|
129
|
+
config["tie_word_embeddings"] = False
|
|
130
|
+
with open(target, "w", encoding="utf-8") as handle:
|
|
131
|
+
json.dump(config, handle)
|
|
132
|
+
else:
|
|
133
|
+
os.symlink(source, target)
|
|
134
|
+
load_path = patched_dir
|
|
135
|
+
except Exception:
|
|
136
|
+
if patched_dir is not None:
|
|
137
|
+
shutil.rmtree(patched_dir, ignore_errors=True)
|
|
138
|
+
patched_dir = None
|
|
139
|
+
|
|
140
|
+
def attempt(local_only: bool):
|
|
141
|
+
try:
|
|
142
|
+
if device == "mps":
|
|
143
|
+
model = load(load_path, "cpu", local_only)
|
|
144
|
+
move_float8_storage_to_mps(model)
|
|
145
|
+
model.to("mps")
|
|
146
|
+
return model, device
|
|
147
|
+
return load(load_path, device, local_only), device
|
|
148
|
+
except Exception:
|
|
149
|
+
# MPS support for custom/quantized checkpoints can vary by torch
|
|
150
|
+
# build. `auto` remains useful where CPU is the safe fallback.
|
|
151
|
+
if requested_device == "auto" and device == "mps":
|
|
152
|
+
return load(load_path, "cpu", local_only), "cpu"
|
|
153
|
+
raise
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
return load_local_first(attempt)
|
|
157
|
+
finally:
|
|
158
|
+
if patched_dir is not None:
|
|
159
|
+
shutil.rmtree(patched_dir, ignore_errors=True)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def make_input(item: Any) -> Any:
|
|
163
|
+
if isinstance(item, str):
|
|
164
|
+
return item
|
|
165
|
+
if not isinstance(item, dict):
|
|
166
|
+
raise ValueError("embedding input must be a string or object")
|
|
167
|
+
text = str(item.get("text") or "")
|
|
168
|
+
image = item.get("image")
|
|
169
|
+
if image:
|
|
170
|
+
# WeMM uses a shared text/image space. Keeping the markdown image and
|
|
171
|
+
# its surrounding text in one input is what makes image-in-markdown
|
|
172
|
+
# searchable by a natural-language query.
|
|
173
|
+
value = {"image": os.fspath(image), "text": text or "Represent this image for retrieval."}
|
|
174
|
+
return value
|
|
175
|
+
return text
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def cached_model(model_name: str, requested_device: str):
|
|
179
|
+
key = (model_name, requested_device)
|
|
180
|
+
cached = MODEL_CACHE.get(key)
|
|
181
|
+
if cached is not None:
|
|
182
|
+
return cached
|
|
183
|
+
# Some model-loading libraries log informational messages to stdout. The
|
|
184
|
+
# stdout channel is the JSONL protocol, so keep those logs on stderr.
|
|
185
|
+
with redirect_stdout(sys.stderr):
|
|
186
|
+
loaded = load_model(model_name, requested_device)
|
|
187
|
+
MODEL_CACHE[key] = loaded
|
|
188
|
+
return loaded
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def has_official_path(model_wrapper: Any) -> bool:
|
|
192
|
+
"""Whether this checkpoint exposes WeMM's own processor + embedding head.
|
|
193
|
+
|
|
194
|
+
WeMM's card is explicit that the official path is the processor + `model.embedding`
|
|
195
|
+
pair, and here it is not a preference. `SentenceTransformer.encode` on this
|
|
196
|
+
checkpoint returns **the same vector for every input** (measured: four unrelated
|
|
197
|
+
texts — Chinese prose, English SQL — pairwise cosine 1.000000, at both 2048 and
|
|
198
|
+
1024 dimensions, on CPU as well as MPS). The official path on the same texts gives
|
|
199
|
+
0.208 / 0.361 / 0.461. So the wrapper path is not a fallback for this checkpoint,
|
|
200
|
+
it is a trap; the Host's encoder health gate is the second line of defence.
|
|
201
|
+
"""
|
|
202
|
+
# `SentenceTransformer` subclasses `torch.nn.Sequential`, so `isinstance(..., (list, tuple))`
|
|
203
|
+
# is False even though `model_wrapper[0]` is exactly the transformer module. Indexing is
|
|
204
|
+
# the capability test that actually holds.
|
|
205
|
+
try:
|
|
206
|
+
transformer = model_wrapper[0]
|
|
207
|
+
except Exception:
|
|
208
|
+
transformer = None
|
|
209
|
+
processor = getattr(transformer, "processor", None)
|
|
210
|
+
inner_model = getattr(transformer, "model", None)
|
|
211
|
+
return processor is not None and inner_model is not None and hasattr(inner_model, "embedding")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _render_text(processor: Any, text: str) -> str:
|
|
215
|
+
return processor.apply_chat_template(
|
|
216
|
+
[{"role": "user", "content": [{"type": "text", "text": text}]}],
|
|
217
|
+
tokenize=False,
|
|
218
|
+
add_generation_prompt=False,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def encode_official_text(model_wrapper: Any, texts: list[str], device: str):
|
|
223
|
+
"""Encode text-only inputs through the checkpoint's own processor path.
|
|
224
|
+
|
|
225
|
+
Batched on purpose: the processor accepts a list of rendered conversations and
|
|
226
|
+
pads them, and the result matches the per-item path to cos 0.9999 (measured) at a
|
|
227
|
+
fifth of the time. Per-item would be correct but needlessly slow — a 44-note store
|
|
228
|
+
re-encodes on every `rebuild all`.
|
|
229
|
+
"""
|
|
230
|
+
import torch
|
|
231
|
+
|
|
232
|
+
transformer = model_wrapper[0]
|
|
233
|
+
processor = transformer.processor
|
|
234
|
+
inner_model = transformer.model
|
|
235
|
+
rendered = [_render_text(processor, text) for text in texts]
|
|
236
|
+
inputs = processor(text=rendered, return_tensors="pt", padding=True)
|
|
237
|
+
inputs = {key: (value.to(device) if isinstance(value, torch.Tensor) else value) for key, value in inputs.items()}
|
|
238
|
+
with torch.inference_mode():
|
|
239
|
+
embedding = inner_model.embedding(**inputs)
|
|
240
|
+
return embedding.detach().to("cpu").float().numpy()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def encode_multimodal(model_wrapper: Any, items: list[dict[str, Any]], device: str):
|
|
244
|
+
"""Encode image-bearing inputs through the official WeMM processor path.
|
|
245
|
+
|
|
246
|
+
SentenceTransformers 6.0's generic `apply_chat_template(tokenize=True)` path can
|
|
247
|
+
produce image features without the matching `<|image_pad|>` token for this
|
|
248
|
+
checkpoint, so the explicit processor path is used for Markdown images.
|
|
249
|
+
"""
|
|
250
|
+
import torch
|
|
251
|
+
from qwen_vl_utils import process_vision_info
|
|
252
|
+
|
|
253
|
+
transformer = model_wrapper[0]
|
|
254
|
+
processor = transformer.processor
|
|
255
|
+
inner_model = transformer.model
|
|
256
|
+
outputs = []
|
|
257
|
+
for item in items:
|
|
258
|
+
messages = [{"role": "user", "content": [
|
|
259
|
+
{"type": "image", "image": os.fspath(item["image"])},
|
|
260
|
+
{"type": "text", "text": str(item.get("text") or "Represent this image for retrieval.")},
|
|
261
|
+
]}]
|
|
262
|
+
rendered = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=False)
|
|
263
|
+
images, videos, video_kwargs = process_vision_info(
|
|
264
|
+
messages,
|
|
265
|
+
image_patch_size=getattr(transformer, "image_patch_size", 16),
|
|
266
|
+
return_video_kwargs=True,
|
|
267
|
+
return_video_metadata=True,
|
|
268
|
+
)
|
|
269
|
+
kwargs = {"text": [rendered], "images": images, "videos": videos, "return_tensors": "pt", "padding": True, **video_kwargs}
|
|
270
|
+
inputs = processor(**kwargs)
|
|
271
|
+
inputs = {key: value.to(device) if isinstance(value, torch.Tensor) else value for key, value in inputs.items()}
|
|
272
|
+
with torch.inference_mode():
|
|
273
|
+
embedding = inner_model.embedding(**inputs)
|
|
274
|
+
outputs.append(embedding.detach().to("cpu").float().numpy()[0])
|
|
275
|
+
return outputs
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def encode(request: dict[str, Any]) -> dict[str, Any]:
|
|
279
|
+
model_name = str(request.get("model") or "ewin-reg/WeMM-Embedding-2B-Quantized")
|
|
280
|
+
requested_device = str(request.get("device") or "auto")
|
|
281
|
+
dimension = int(request.get("dimension") or 1024)
|
|
282
|
+
batch_size = max(1, int(request.get("batchSize") or 8))
|
|
283
|
+
raw_inputs = request.get("inputs")
|
|
284
|
+
if not isinstance(raw_inputs, list) or not raw_inputs:
|
|
285
|
+
raise ValueError("inputs must be a non-empty array")
|
|
286
|
+
if dimension < 1:
|
|
287
|
+
raise ValueError("dimension must be positive")
|
|
288
|
+
|
|
289
|
+
model, device = cached_model(model_name, requested_device)
|
|
290
|
+
import numpy as np
|
|
291
|
+
|
|
292
|
+
vectors: list[list[float]] = []
|
|
293
|
+
# A single model process handles all batches, which is important for large
|
|
294
|
+
# knowledge stores: loading a 1.8GB checkpoint once is acceptable; loading it once
|
|
295
|
+
# per document is not.
|
|
296
|
+
official = has_official_path(model)
|
|
297
|
+
for start in range(0, len(raw_inputs), batch_size):
|
|
298
|
+
raw_batch = raw_inputs[start : start + batch_size]
|
|
299
|
+
batch = [make_input(item) for item in raw_batch]
|
|
300
|
+
has_images = any(isinstance(item, dict) and item.get("image") for item in raw_batch)
|
|
301
|
+
if official and not has_images:
|
|
302
|
+
# The common case: a whole batch of text through the official path.
|
|
303
|
+
encoded = encode_official_text(model, [str(item) for item in batch], device)
|
|
304
|
+
elif official:
|
|
305
|
+
# A batch may mix ordinary text with an image-bearing unit. The low-level
|
|
306
|
+
# multimodal processor requires an image for every item, so keep the model
|
|
307
|
+
# loaded once but route each item through the path it needs.
|
|
308
|
+
rows = []
|
|
309
|
+
for raw_item, text_item in zip(raw_batch, batch):
|
|
310
|
+
if isinstance(raw_item, dict) and raw_item.get("image"):
|
|
311
|
+
item_encoded = np.asarray(encode_multimodal(model, [raw_item], device), dtype=np.float32)
|
|
312
|
+
else:
|
|
313
|
+
item_encoded = np.asarray(encode_official_text(model, [str(text_item)], device), dtype=np.float32)
|
|
314
|
+
if item_encoded.ndim == 1:
|
|
315
|
+
item_encoded = item_encoded.reshape(1, -1)
|
|
316
|
+
rows.extend(item_encoded.tolist())
|
|
317
|
+
encoded = rows
|
|
318
|
+
else:
|
|
319
|
+
# A generic SentenceTransformers checkpoint: there is no WeMM head to use,
|
|
320
|
+
# so the wrapper's own encode is the only option. `has_official_path`
|
|
321
|
+
# documents why that path cannot be trusted for WeMM itself.
|
|
322
|
+
encoded = model.encode(
|
|
323
|
+
batch,
|
|
324
|
+
batch_size=batch_size,
|
|
325
|
+
normalize_embeddings=True,
|
|
326
|
+
truncate_dim=dimension,
|
|
327
|
+
convert_to_numpy=True,
|
|
328
|
+
show_progress_bar=False,
|
|
329
|
+
)
|
|
330
|
+
array = np.asarray(encoded, dtype=np.float32)
|
|
331
|
+
if array.ndim == 1:
|
|
332
|
+
array = array.reshape(1, -1)
|
|
333
|
+
# The explicit multimodal WeMM path returns the native 2048-d vector; the
|
|
334
|
+
# generic SentenceTransformers path can apply MRL truncation for us. Keep both
|
|
335
|
+
# paths identical by applying the requested dimension here too.
|
|
336
|
+
if array.ndim == 2 and array.shape[1] > dimension:
|
|
337
|
+
array = array[:, :dimension]
|
|
338
|
+
if array.ndim != 2 or array.shape[1] != dimension:
|
|
339
|
+
raise RuntimeError(f"embedding shape mismatch: expected (*, {dimension}), got {tuple(array.shape)}")
|
|
340
|
+
# The model promises normalized vectors, but re-normalize after MRL truncation
|
|
341
|
+
# so cosine / int8 scoring stays mathematically consistent.
|
|
342
|
+
norms = np.linalg.norm(array, axis=1, keepdims=True)
|
|
343
|
+
array = array / np.maximum(norms, 1e-12)
|
|
344
|
+
vectors.extend(array.tolist())
|
|
345
|
+
return {"ok": True, "device": device, "dimension": dimension, "vectors": vectors}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def reranker_health(model) -> dict[str, Any]:
|
|
349
|
+
"""Refuse a cross-encoder whose tokenizer has no vocabulary.
|
|
350
|
+
|
|
351
|
+
Runs once per loaded model. Both checks are cheap and catch the same failure
|
|
352
|
+
mode from two directions: the vocabulary size (a *present* tokenizer) and the
|
|
353
|
+
probe sentence (a tokenizer that is present but resolves nothing).
|
|
354
|
+
"""
|
|
355
|
+
tokenizer = getattr(model, "tokenizer", None)
|
|
356
|
+
vocab_size = int(getattr(tokenizer, "vocab_size", 0) or 0)
|
|
357
|
+
if vocab_size < RERANK_MIN_VOCAB:
|
|
358
|
+
raise RuntimeError(
|
|
359
|
+
f"RERANK_TOKENIZER_DEGENERATE: vocab_size={vocab_size} (< {RERANK_MIN_VOCAB}); "
|
|
360
|
+
"本地模型快照缺少分词器词表(bge-reranker-base 需要 tokenizer.json 或 "
|
|
361
|
+
"sentencepiece.bpe.model,见 docs/0024-本地模型运行时.md)"
|
|
362
|
+
)
|
|
363
|
+
unknown = getattr(tokenizer, "unk_token_id", None)
|
|
364
|
+
special = set(getattr(tokenizer, "all_special_ids", None) or [])
|
|
365
|
+
probe = tokenizer("订单 指标 排查 口径 DAU", add_special_tokens=False)["input_ids"]
|
|
366
|
+
# 特判 `add_special_tokens` 的两种行为:只排除 <unk> 是不够的,`<s>` / `</s>`
|
|
367
|
+
# 也不是「真实词表条目」,否则空词表能靠这两个 id 蒙过检查。
|
|
368
|
+
if not [token for token in probe if token != unknown and token not in special]:
|
|
369
|
+
raise RuntimeError("RERANK_TOKENIZER_DEGENERATE: 探针句全部落到 <unk>,词表里没有真实条目")
|
|
370
|
+
return {"vocabSize": vocab_size}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def predict_raw(model, pairs):
|
|
374
|
+
"""Cross-encoder 分数,**在原始 logit 刻度上**。
|
|
375
|
+
|
|
376
|
+
`CrossEncoder.predict` 对 `num_labels == 1` 的模型默认套 sigmoid,返回值落在
|
|
377
|
+
(0, 1);而 `rerankThreshold` 的默认值 0.2 是 WeKnora 在 **logit** 刻度上定的。
|
|
378
|
+
两个刻度混用会让阈值形同虚设:实测 sigmoid 输出恒 > 0.2,于是 `dropped` 永远是 0,
|
|
379
|
+
「重排阈值」这个绝对闸门从来没生效过;Host 再 sigmoid 一次做展示,又把无关文档
|
|
380
|
+
(logit −10.2 → 0.0000454)显示成 0.5。
|
|
381
|
+
|
|
382
|
+
展示用的那一次 sigmoid 由 Host 做(`recall-rank.ts`),这里只给 logit ——
|
|
383
|
+
阈值与排序都留在一个可以跨查询比较的刻度上。
|
|
384
|
+
"""
|
|
385
|
+
try:
|
|
386
|
+
import torch
|
|
387
|
+
except Exception as exc: # pragma: no cover - torch is required to have loaded the model
|
|
388
|
+
raise RuntimeError(f"RERANK_TORCH_MISSING: {exc}") from exc
|
|
389
|
+
try:
|
|
390
|
+
return model.predict(pairs, show_progress_bar=False, activation_fn=torch.nn.Identity())
|
|
391
|
+
except TypeError as exc:
|
|
392
|
+
raise RuntimeError(
|
|
393
|
+
"RERANK_ACTIVATION_UNSUPPORTED: 需要 sentence-transformers 支持 predict(activation_fn=...) "
|
|
394
|
+
"才能拿到原始 logit(阈值刻度)"
|
|
395
|
+
) from exc
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def load_reranker(model_name: str, requested_device: str):
|
|
399
|
+
"""Load a BERT cross-encoder for stage 4 rerank.
|
|
400
|
+
|
|
401
|
+
Separate from the embedding model: the reranker scores (query, note) pairs
|
|
402
|
+
instead of producing vectors, and lives in its own Host-side worker process
|
|
403
|
+
so a missing reranker model cannot disable embeddings.
|
|
404
|
+
"""
|
|
405
|
+
from sentence_transformers import CrossEncoder
|
|
406
|
+
|
|
407
|
+
device = choose_device(requested_device)
|
|
408
|
+
|
|
409
|
+
def attempt(local_only: bool):
|
|
410
|
+
kwargs = {"local_files_only": True} if local_only else {}
|
|
411
|
+
try:
|
|
412
|
+
return CrossEncoder(model_name, device=device, trust_remote_code=True, **kwargs), device
|
|
413
|
+
except TypeError:
|
|
414
|
+
# Older sentence-transformers releases do not expose trust_remote_code.
|
|
415
|
+
return CrossEncoder(model_name, device=device, **kwargs), device
|
|
416
|
+
|
|
417
|
+
return load_local_first(attempt)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def cached_reranker(model_name: str, requested_device: str):
|
|
421
|
+
key = (model_name, requested_device)
|
|
422
|
+
cached = RERANK_CACHE.get(key)
|
|
423
|
+
if cached is not None:
|
|
424
|
+
return cached
|
|
425
|
+
with redirect_stdout(sys.stderr):
|
|
426
|
+
model, device = load_reranker(model_name, requested_device)
|
|
427
|
+
health = reranker_health(model)
|
|
428
|
+
RERANK_CACHE[key] = (model, device, health)
|
|
429
|
+
return RERANK_CACHE[key]
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def rerank(request: dict[str, Any]) -> dict[str, Any]:
|
|
433
|
+
model_name = str(request.get("model") or "BAAI/bge-reranker-base")
|
|
434
|
+
requested_device = str(request.get("device") or "auto")
|
|
435
|
+
query = str(request.get("query") or "")
|
|
436
|
+
documents = request.get("documents")
|
|
437
|
+
if not query:
|
|
438
|
+
raise ValueError("rerank requires a non-empty query")
|
|
439
|
+
if not isinstance(documents, list) or not documents:
|
|
440
|
+
raise ValueError("rerank requires a non-empty documents array")
|
|
441
|
+
model, device, health = cached_reranker(model_name, requested_device)
|
|
442
|
+
pairs = [(query, str(document)) for document in documents]
|
|
443
|
+
with redirect_stdout(sys.stderr):
|
|
444
|
+
scores = predict_raw(model, pairs)
|
|
445
|
+
return {
|
|
446
|
+
"ok": True,
|
|
447
|
+
"device": device,
|
|
448
|
+
"vocabSize": health.get("vocabSize"),
|
|
449
|
+
"scores": [float(score) for score in scores],
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def main() -> None:
|
|
454
|
+
for line in sys.stdin:
|
|
455
|
+
if not line.strip():
|
|
456
|
+
continue
|
|
457
|
+
try:
|
|
458
|
+
request = json.loads(line)
|
|
459
|
+
# Stage 4 rerank and stage 2/3 embedding share one worker script so
|
|
460
|
+
# the plugin keeps a single local Python runtime; the `task` field
|
|
461
|
+
# selects the path and both models are cached per process.
|
|
462
|
+
if str(request.get("task") or "") == "rerank":
|
|
463
|
+
emit(rerank(request))
|
|
464
|
+
else:
|
|
465
|
+
emit(encode(request))
|
|
466
|
+
except Exception as exc: # return machine-readable errors to the Host
|
|
467
|
+
emit({"ok": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
if __name__ == "__main__":
|
|
471
|
+
main()
|