dsh-biocore 0.1.0 → 0.1.2

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.
Files changed (250) hide show
  1. package/README.md +2 -2
  2. package/lib/py/bridge.py +15 -7
  3. package/lib/py-bridge.js +4 -3
  4. package/package.json +3 -2
  5. package/python/biocore_memory/__init__.py +21 -0
  6. package/python/biocore_memory/adapters/__init__.py +2 -0
  7. package/python/biocore_memory/adapters/astrbot.py +24 -0
  8. package/python/biocore_memory/adapters/dsh_cli.py +335 -0
  9. package/python/biocore_memory/co_recall.py +324 -0
  10. package/python/biocore_memory/consolidate/__init__.py +11 -0
  11. package/python/biocore_memory/consolidate/gated.py +104 -0
  12. package/python/biocore_memory/consolidate/neighbors.py +68 -0
  13. package/python/biocore_memory/consolidate/reflection.py +45 -0
  14. package/python/biocore_memory/consolidate/tree.py +95 -0
  15. package/python/biocore_memory/consolidate/writeback.py +181 -0
  16. package/python/biocore_memory/core/__init__.py +4 -0
  17. package/python/biocore_memory/core/config.py +127 -0
  18. package/python/biocore_memory/core/permissions.py +69 -0
  19. package/python/biocore_memory/core/types.py +116 -0
  20. package/python/biocore_memory/core/util.py +26 -0
  21. package/python/biocore_memory/embedding_local.py +447 -0
  22. package/python/biocore_memory/embedding_mode.py +156 -0
  23. package/python/biocore_memory/engine.py +2476 -0
  24. package/python/biocore_memory/engine_helpers.py +266 -0
  25. package/python/biocore_memory/event/__init__.py +3 -0
  26. package/python/biocore_memory/event/_types.py +16 -0
  27. package/python/biocore_memory/event/consistency.py +117 -0
  28. package/python/biocore_memory/event/extract.py +48 -0
  29. package/python/biocore_memory/event/media.py +111 -0
  30. package/python/biocore_memory/event/notes.py +85 -0
  31. package/python/biocore_memory/event/notes_link.py +87 -0
  32. package/python/biocore_memory/event/vector.py +69 -0
  33. package/python/biocore_memory/evict/__init__.py +3 -0
  34. package/python/biocore_memory/evict/adaptive.py +73 -0
  35. package/python/biocore_memory/evict/consequence.py +58 -0
  36. package/python/biocore_memory/evict/heat.py +54 -0
  37. package/python/biocore_memory/evict/inhibition.py +105 -0
  38. package/python/biocore_memory/evict/policy.py +188 -0
  39. package/python/biocore_memory/evict/score.py +23 -0
  40. package/python/biocore_memory/identity/__init__.py +6 -0
  41. package/python/biocore_memory/identity/api.py +99 -0
  42. package/python/biocore_memory/identity/history.py +9 -0
  43. package/python/biocore_memory/identity/ledger.py +191 -0
  44. package/python/biocore_memory/identity/narrative.py +358 -0
  45. package/python/biocore_memory/identity/snapshot.py +13 -0
  46. package/python/biocore_memory/identity/writeback.py +61 -0
  47. package/python/biocore_memory/index/__init__.py +3 -0
  48. package/python/biocore_memory/index/cue_index.py +80 -0
  49. package/python/biocore_memory/index/entities.py +85 -0
  50. package/python/biocore_memory/index/entity_boost.py +104 -0
  51. package/python/biocore_memory/index/graph.py +172 -0
  52. package/python/biocore_memory/recall/__init__.py +5 -0
  53. package/python/biocore_memory/recall/api.py +65 -0
  54. package/python/biocore_memory/recall/bm25.py +118 -0
  55. package/python/biocore_memory/recall/channels.py +98 -0
  56. package/python/biocore_memory/recall/complexity.py +62 -0
  57. package/python/biocore_memory/recall/constraints.py +75 -0
  58. package/python/biocore_memory/recall/embedding.py +745 -0
  59. package/python/biocore_memory/recall/expand.py +26 -0
  60. package/python/biocore_memory/recall/filters.py +40 -0
  61. package/python/biocore_memory/recall/fusion.py +18 -0
  62. package/python/biocore_memory/recall/mmr.py +70 -0
  63. package/python/biocore_memory/recall/ppr.py +85 -0
  64. package/python/biocore_memory/recall/retrieval_practice.py +102 -0
  65. package/python/biocore_memory/relay/__init__.py +2 -0
  66. package/python/biocore_memory/relay/card.py +51 -0
  67. package/python/biocore_memory/slots/__init__.py +18 -0
  68. package/python/biocore_memory/slots/admission.py +64 -0
  69. package/python/biocore_memory/slots/boundary.py +128 -0
  70. package/python/biocore_memory/slots/evict_bridge.py +106 -0
  71. package/python/biocore_memory/slots/registry.py +135 -0
  72. package/python/biocore_memory/slots/switch.py +42 -0
  73. package/python/biocore_memory/store/__init__.py +4 -0
  74. package/python/biocore_memory/store/audit.py +74 -0
  75. package/python/biocore_memory/store/compress.py +81 -0
  76. package/python/biocore_memory/store/dedupe.py +178 -0
  77. package/python/biocore_memory/store/json_store.py +154 -0
  78. package/python/biocore_memory/store/media.py +145 -0
  79. package/python/biocore_memory/store/provenance.py +73 -0
  80. package/python/biocore_memory/store/rate_limit.py +93 -0
  81. package/python/biocore_memory/store/security.py +107 -0
  82. package/python/biocore_memory/store/source_consistency.py +123 -0
  83. package/python/biocore_memory/store/tiering.py +61 -0
  84. package/python/biocore_memory/store/tiering_exec.py +50 -0
  85. package/python/biocore_memory/store/tombstone.py +23 -0
  86. package/python/biocore_memory/store/versions.py +69 -0
  87. package/python/biocore_memory/thread/__init__.py +2 -0
  88. package/python/biocore_memory/thread/card.py +58 -0
  89. package/python/biocore_model/__init__.py +37 -0
  90. package/python/biocore_model/affective_labor.py +162 -0
  91. package/python/biocore_model/appraisal.py +257 -0
  92. package/python/biocore_model/appraisal_action.py +78 -0
  93. package/python/biocore_model/archive_queue.py +60 -0
  94. package/python/biocore_model/attachment_behavior.py +130 -0
  95. package/python/biocore_model/attachment_regulation.py +75 -0
  96. package/python/biocore_model/attachment_state.py +41 -0
  97. package/python/biocore_model/attribution.py +73 -0
  98. package/python/biocore_model/attributor.py +162 -0
  99. package/python/biocore_model/autonomic.py +91 -0
  100. package/python/biocore_model/autonomy_affect.py +51 -0
  101. package/python/biocore_model/autonomy_check.py +67 -0
  102. package/python/biocore_model/autonomy_loop.py +122 -0
  103. package/python/biocore_model/autonomy_mode.py +114 -0
  104. package/python/biocore_model/autonomy_model.py +733 -0
  105. package/python/biocore_model/autonomy_params.py +131 -0
  106. package/python/biocore_model/autonomy_reason.py +73 -0
  107. package/python/biocore_model/autonomy_tick.py +227 -0
  108. package/python/biocore_model/bio_appraisal.py +49 -0
  109. package/python/biocore_model/bio_autonomy_state.py +171 -0
  110. package/python/biocore_model/bio_config.py +221 -0
  111. package/python/biocore_model/bio_const.py +128 -0
  112. package/python/biocore_model/bio_daily.py +267 -0
  113. package/python/biocore_model/bio_emotion.py +538 -0
  114. package/python/biocore_model/bio_emotion2.py +214 -0
  115. package/python/biocore_model/bio_goal.py +220 -0
  116. package/python/biocore_model/bio_identity.py +82 -0
  117. package/python/biocore_model/bio_noise.py +119 -0
  118. package/python/biocore_model/bio_physiology2.py +114 -0
  119. package/python/biocore_model/bio_snapshot.py +324 -0
  120. package/python/biocore_model/bio_state.py +272 -0
  121. package/python/biocore_model/bio_tick.py +310 -0
  122. package/python/biocore_model/bio_util.py +82 -0
  123. package/python/biocore_model/blocks.py +264 -0
  124. package/python/biocore_model/boundary.py +251 -0
  125. package/python/biocore_model/boundary_gate.py +188 -0
  126. package/python/biocore_model/commitment_extractor.py +517 -0
  127. package/python/biocore_model/compassion_fatigue.py +84 -0
  128. package/python/biocore_model/connection_bids.py +120 -0
  129. package/python/biocore_model/core_bridge.py +177 -0
  130. package/python/biocore_model/core_client.py +127 -0
  131. package/python/biocore_model/core_commands.py +112 -0
  132. package/python/biocore_model/core_mode.py +104 -0
  133. package/python/biocore_model/core_mode_ui.mjs +104 -0
  134. package/python/biocore_model/criticality.py +81 -0
  135. package/python/biocore_model/defense_mechanisms.py +272 -0
  136. package/python/biocore_model/delta_budget.py +99 -0
  137. package/python/biocore_model/diag_trace.py +274 -0
  138. package/python/biocore_model/dissonance.py +134 -0
  139. package/python/biocore_model/emotion_geometry.py +108 -0
  140. package/python/biocore_model/emotion_probe.py +209 -0
  141. package/python/biocore_model/entity_graph.py +931 -0
  142. package/python/biocore_model/entity_resolve.py +260 -0
  143. package/python/biocore_model/envy.py +193 -0
  144. package/python/biocore_model/exit_or_silence.py +230 -0
  145. package/python/biocore_model/expr_guard.py +24 -0
  146. package/python/biocore_model/expr_intent.py +77 -0
  147. package/python/biocore_model/expr_state.py +204 -0
  148. package/python/biocore_model/expr_template.py +105 -0
  149. package/python/biocore_model/expr_tone.py +81 -0
  150. package/python/biocore_model/extreme_scenarios.py +205 -0
  151. package/python/biocore_model/feedback_flow.py +227 -0
  152. package/python/biocore_model/feedback_guard.py +129 -0
  153. package/python/biocore_model/feedback_llm.py +163 -0
  154. package/python/biocore_model/feedback_memory.py +96 -0
  155. package/python/biocore_model/feedback_user.py +93 -0
  156. package/python/biocore_model/frustration_curve.py +110 -0
  157. package/python/biocore_model/granularity.py +283 -0
  158. package/python/biocore_model/graph_extract.py +248 -0
  159. package/python/biocore_model/graph_recall.py +221 -0
  160. package/python/biocore_model/intent_render.py +91 -0
  161. package/python/biocore_model/intero_core.py +235 -0
  162. package/python/biocore_model/intero_style.py +116 -0
  163. package/python/biocore_model/layered_memory.py +297 -0
  164. package/python/biocore_model/learning_probe.py +120 -0
  165. package/python/biocore_model/memory_dual.py +224 -0
  166. package/python/biocore_model/memory_forgetting.py +125 -0
  167. package/python/biocore_model/memory_store.py +1399 -0
  168. package/python/biocore_model/metacog.py +153 -0
  169. package/python/biocore_model/migrate.py +733 -0
  170. package/python/biocore_model/migrate_store.py +337 -0
  171. package/python/biocore_model/mind_wander.py +111 -0
  172. package/python/biocore_model/mood.py +48 -0
  173. package/python/biocore_model/narrative.py +172 -0
  174. package/python/biocore_model/needs_esteem.py +116 -0
  175. package/python/biocore_model/needs_events.py +137 -0
  176. package/python/biocore_model/needs_sleep.py +72 -0
  177. package/python/biocore_model/noise_bank.py +95 -0
  178. package/python/biocore_model/persona_flow.mjs +25 -0
  179. package/python/biocore_model/pipeline.py +240 -0
  180. package/python/biocore_model/pipeline_api.py +200 -0
  181. package/python/biocore_model/plugin_config.py +83 -0
  182. package/python/biocore_model/plugin_routes.py +1435 -0
  183. package/python/biocore_model/policy_learning.py +132 -0
  184. package/python/biocore_model/possible_selves.py +110 -0
  185. package/python/biocore_model/predictor.py +178 -0
  186. package/python/biocore_model/preset.py +205 -0
  187. package/python/biocore_model/proactive_context.py +153 -0
  188. package/python/biocore_model/prompt_rules.py +90 -0
  189. package/python/biocore_model/prospective.py +183 -0
  190. package/python/biocore_model/provider_fallback.py +97 -0
  191. package/python/biocore_model/reconstruction.py +132 -0
  192. package/python/biocore_model/regulation_loop.py +179 -0
  193. package/python/biocore_model/regulation_strategies.py +294 -0
  194. package/python/biocore_model/relation_stage.py +130 -0
  195. package/python/biocore_model/relationship_stage.py +95 -0
  196. package/python/biocore_model/render.py +901 -0
  197. package/python/biocore_model/render_guard.mjs +34 -0
  198. package/python/biocore_model/repair_after_boundary.py +350 -0
  199. package/python/biocore_model/reply_limit.py +183 -0
  200. package/python/biocore_model/reply_split.py +203 -0
  201. package/python/biocore_model/rollback.py +143 -0
  202. package/python/biocore_model/runtime.py +479 -0
  203. package/python/biocore_model/sanitize.py +200 -0
  204. package/python/biocore_model/scheduler.py +123 -0
  205. package/python/biocore_model/schema.py +120 -0
  206. package/python/biocore_model/secrecy.py +145 -0
  207. package/python/biocore_model/self_esteem.py +50 -0
  208. package/python/biocore_model/self_intent.py +437 -0
  209. package/python/biocore_model/self_interest.py +263 -0
  210. package/python/biocore_model/self_verification.py +178 -0
  211. package/python/biocore_model/services.py +78 -0
  212. package/python/biocore_model/situation.py +254 -0
  213. package/python/biocore_model/sleep_circadian.py +116 -0
  214. package/python/biocore_model/social_pain.py +69 -0
  215. package/python/biocore_model/social_repair.py +111 -0
  216. package/python/biocore_model/social_theory.py +131 -0
  217. package/python/biocore_model/source_monitoring.py +292 -0
  218. package/python/biocore_model/stages.py +487 -0
  219. package/python/biocore_model/state_tag.py +231 -0
  220. package/python/biocore_model/switch_groups.mjs +201 -0
  221. package/python/biocore_model/tabs_state.mjs +37 -0
  222. package/python/biocore_model/template_expr.py +252 -0
  223. package/python/biocore_model/tools/caps_check.mjs +23 -0
  224. package/python/biocore_model/tools/init_symbols_check.mjs +11 -0
  225. package/python/biocore_model/tools/missing_ids.mjs +16 -0
  226. package/python/biocore_model/tools/scope_check.mjs +19 -0
  227. package/python/biocore_model/topic_control.py +268 -0
  228. package/python/biocore_model/turn_memory.py +152 -0
  229. package/python/biocore_model/ui_compat.mjs +48 -0
  230. package/python/biocore_model/ui_settings.py +266 -0
  231. package/python/biocore_model/ui_settings.py.bak_authority +108 -0
  232. package/python/biocore_model/value_order.py +186 -0
  233. package/python/biocore_model/view_mode.mjs +8 -0
  234. package/python/biocore_model/voice_bank.py +177 -0
  235. package/python/biocore_model/waiting_sense.py +229 -0
  236. package/python/biocore_model/x_directives.py +142 -0
  237. package/python/biocore_runtime/__init__.py +7 -0
  238. package/python/biocore_runtime/pipeline.py +1915 -0
  239. package/python/dsh_adapter/README.md +96 -0
  240. package/python/dsh_adapter/adapter.py +1077 -0
  241. package/python/dsh_adapter/cli.py +107 -0
  242. package/python/dsh_adapter/data/demo_state.json +164 -0
  243. package/python/dsh_adapter/data/verify_state.json +164 -0
  244. package/python/dsh_adapter/tests/__init__.py +1 -0
  245. package/python/dsh_adapter/tests/test_adapter.py +494 -0
  246. package/ui/panel-layout.js +3 -3
  247. package/ui/panel.css +53 -6
  248. package/ui/panel.html +33 -32
  249. package/ui/panel.js +9 -1
  250. package/lib/py/__pycache__/bridge.cpython-310.pyc +0 -0
@@ -0,0 +1,447 @@
1
+ # -*- coding: utf-8 -*-
2
+ """CPU-only local int8 ONNX embedder (the fallback path for remote embeddings).
3
+
4
+ The optional component (task-190) is a bge-small-zh-v1.5 int8 ONNX export:
5
+
6
+ models/bge-small-zh-int8/model.onnx about 23 MB
7
+ inputs input_ids, attention_mask int64, dynamic batch / sequence
8
+ output embedding CLS-pooled + L2-normalized, dim 512
9
+
10
+ Hard rules:
11
+ - CPU only: onnxruntime with CPUExecutionProvider; no torch, no CUDA packages,
12
+ no GPU requirement; intra-op threads configurable (default 2).
13
+ - Lazy import: importing this module never imports onnxruntime.
14
+ - Clear failures: LocalEmbeddingUnavailable when runtime / model / vocab is
15
+ missing, LocalEmbeddingError for bad input or output dimensions. Callers
16
+ (the provider) catch these and stay on the remote path; the process never
17
+ dies because the optional component is absent.
18
+ - Tokenizer: a small stdlib BERT WordPiece reader over vocab.txt, so the
19
+ component needs onnxruntime only (no transformers / tokenizers package).
20
+
21
+ The model directory is an optional, removable component: delete it and the
22
+ provider simply keeps using the remote API.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import os
28
+ import threading
29
+ import unicodedata
30
+ from pathlib import Path
31
+
32
+ DEFAULT_DIM = 512
33
+ DEFAULT_MAX_LENGTH = 512
34
+ DEFAULT_THREADS = 2
35
+ MAX_THREADS = 8
36
+ MAX_WORD_CHARS = 100
37
+ SPECIAL_TOKENS = ("[PAD]", "[UNK]", "[CLS]", "[SEP]")
38
+ ENV_TOKENIZER = "BIOCORE_EMBEDDING_LOCAL_TOKENIZER"
39
+
40
+ # Lazy optional deps: imported on first real use, cached for the process, and
41
+ # abstracted behind helpers so tests can inject fakes without touching
42
+ # sys.modules (replacing sys.modules entries can force numpy to be loaded
43
+ # twice in one process, which C extensions reject).
44
+ _LAZY_MODULES = {}
45
+
46
+
47
+ def _lazy_import(name):
48
+ module = _LAZY_MODULES.get(name)
49
+ if module is None:
50
+ module = __import__(name)
51
+ _LAZY_MODULES[name] = module
52
+ return module
53
+
54
+
55
+ def _numpy():
56
+ return _lazy_import("numpy")
57
+
58
+
59
+ def _onnxruntime():
60
+ return _lazy_import("onnxruntime")
61
+
62
+
63
+ class LocalEmbeddingUnavailable(RuntimeError):
64
+ """The optional local component cannot be used (missing runtime/model)."""
65
+
66
+
67
+ class LocalEmbeddingError(RuntimeError):
68
+ """The local component was used with bad input or produced bad output."""
69
+
70
+
71
+ def _positive_int(value, default):
72
+ try:
73
+ number = int(value)
74
+ except Exception:
75
+ return int(default)
76
+ return number if number > 0 else int(default)
77
+
78
+
79
+ def _chinese(char):
80
+ code = ord(char)
81
+ return (0x4E00 <= code <= 0x9FFF or 0x3400 <= code <= 0x4DBF
82
+ or 0x20000 <= code <= 0x2A6DF or 0x2A700 <= code <= 0x2B73F
83
+ or 0x2B740 <= code <= 0x2B81F or 0x2B820 <= code <= 0x2CEAF
84
+ or 0xF900 <= code <= 0xFAFF or 0x2F800 <= code <= 0x2FA1F)
85
+
86
+
87
+ def _control(char):
88
+ category = unicodedata.category(char)
89
+ return category in ("Cc", "Cf") and char not in ("\t", "\n", "\r")
90
+
91
+
92
+ def _whitespace(char):
93
+ if char in ("\t", "\n", "\r"):
94
+ return True
95
+ return unicodedata.category(char) in ("Zs", "Zl", "Zp")
96
+
97
+
98
+ def _punctuation(char):
99
+ code = ord(char)
100
+ if (33 <= code <= 47 or 58 <= code <= 64 or 91 <= code <= 96
101
+ or 123 <= code <= 126):
102
+ return True
103
+ return unicodedata.category(char).startswith("P")
104
+
105
+
106
+ def _clean_text(text, do_lower_case=True):
107
+ out = []
108
+ for char in str(text or ""):
109
+ code = ord(char)
110
+ if code == 0 or code == 0xFFFD:
111
+ continue
112
+ if _control(char):
113
+ continue
114
+ if _whitespace(char):
115
+ out.append(" ")
116
+ continue
117
+ out.append(char.lower() if do_lower_case else char)
118
+ return "".join(out)
119
+
120
+
121
+ def _basic_tokens(text, do_lower_case=True):
122
+ """BERT BasicTokenizer: lowercase, CJK split, whitespace, punctuation."""
123
+ cleaned = _clean_text(text, do_lower_case)
124
+ padded = []
125
+ for char in cleaned:
126
+ if _chinese(char):
127
+ padded.append(" ")
128
+ padded.append(char)
129
+ padded.append(" ")
130
+ else:
131
+ padded.append(char)
132
+ tokens = []
133
+ for word in "".join(padded).split():
134
+ current = []
135
+ for char in word:
136
+ if _punctuation(char):
137
+ if current:
138
+ tokens.append("".join(current))
139
+ current = []
140
+ tokens.append(char)
141
+ else:
142
+ current.append(char)
143
+ if current:
144
+ tokens.append("".join(current))
145
+ return tokens
146
+
147
+
148
+ def _wordpiece(token, vocab):
149
+ if len(token) > MAX_WORD_CHARS:
150
+ return [token]
151
+ chars = list(token)
152
+ pieces = []
153
+ start = 0
154
+ while start < len(chars):
155
+ end = len(chars)
156
+ found = None
157
+ while start < end:
158
+ piece = "".join(chars[start:end])
159
+ if start > 0:
160
+ piece = "##" + piece
161
+ if piece in vocab:
162
+ found = piece
163
+ break
164
+ end -= 1
165
+ if found is None:
166
+ return [token]
167
+ pieces.append(found)
168
+ start = end
169
+ return pieces
170
+
171
+
172
+ class _VocabTokenizer:
173
+ """Minimal BERT WordPiece tokenizer over tokenizer.json / vocab.txt.
174
+
175
+ tokenizer.json (the fast-tokenizer export) is authoritative and is tried
176
+ first; vocab.txt is only a fallback for components that ship just the
177
+ classic file. Stdlib only.
178
+ """
179
+
180
+ def __init__(self, vocab_path, do_lower_case=True):
181
+ path = Path(vocab_path)
182
+ try:
183
+ if path.suffix.lower() == ".json":
184
+ payload = json.loads(
185
+ path.read_text(encoding="utf-8", errors="replace"))
186
+ vocab = payload.get("model", {}).get("vocab")
187
+ if not isinstance(vocab, dict):
188
+ raise ValueError("tokenizer.json has no model.vocab")
189
+ self.vocab = {}
190
+ for token, index in vocab.items():
191
+ try:
192
+ self.vocab[str(token)] = int(index)
193
+ except (TypeError, ValueError):
194
+ continue
195
+ else:
196
+ lines = path.read_text(
197
+ encoding="utf-8", errors="replace").splitlines()
198
+ self.vocab = {}
199
+ for index, line in enumerate(lines):
200
+ token = line[:-1] if line.endswith("\r") else line
201
+ if token and token not in self.vocab:
202
+ self.vocab[token] = index
203
+ except OSError as exc:
204
+ raise LocalEmbeddingUnavailable(
205
+ "cannot read tokenizer vocab: %s (%s)" % (path, exc)) from exc
206
+ except ValueError as exc:
207
+ raise LocalEmbeddingUnavailable(
208
+ "invalid tokenizer vocab %s (%s)" % (path, exc)) from exc
209
+ missing = [token for token in SPECIAL_TOKENS if token not in self.vocab]
210
+ if missing:
211
+ raise LocalEmbeddingUnavailable(
212
+ "tokenizer vocab is missing special tokens: %s"
213
+ % ", ".join(missing))
214
+ self.do_lower_case = bool(do_lower_case)
215
+ self.pad_id = self.vocab["[PAD]"]
216
+ self.unk_id = self.vocab["[UNK]"]
217
+ self.cls_id = self.vocab["[CLS]"]
218
+ self.sep_id = self.vocab["[SEP]"]
219
+
220
+ def encode(self, text, max_length=DEFAULT_MAX_LENGTH):
221
+ limit = max(8, _positive_int(max_length, DEFAULT_MAX_LENGTH))
222
+ pieces = []
223
+ for token in _basic_tokens(text, self.do_lower_case):
224
+ pieces.extend(_wordpiece(token, self.vocab))
225
+ if len(pieces) >= limit - 2:
226
+ break
227
+ pieces = pieces[:limit - 2]
228
+ ids = [self.cls_id]
229
+ ids.extend(self.vocab.get(piece, self.unk_id) for piece in pieces)
230
+ ids.append(self.sep_id)
231
+ return ids, [1] * len(ids)
232
+
233
+ def encode_batch(self, texts, max_length=DEFAULT_MAX_LENGTH):
234
+ pairs = [self.encode(text, max_length) for text in texts]
235
+ width = max([len(ids) for ids, _ in pairs] or [2])
236
+ ids_rows = []
237
+ mask_rows = []
238
+ for ids, mask in pairs:
239
+ padding = width - len(ids)
240
+ ids_rows.append(ids + [self.pad_id] * padding)
241
+ mask_rows.append(mask + [0] * padding)
242
+ return ids_rows, mask_rows
243
+
244
+
245
+ def _as_texts(texts):
246
+ if texts is None:
247
+ return []
248
+ if isinstance(texts, str):
249
+ texts = [texts]
250
+ if not isinstance(texts, (list, tuple)):
251
+ raise LocalEmbeddingError(
252
+ "texts must be a string or a list of strings")
253
+ rows = []
254
+ for item in texts:
255
+ if item is None:
256
+ rows.append("")
257
+ elif isinstance(item, (dict, list, tuple, set)):
258
+ raise LocalEmbeddingError("text items must be strings")
259
+ else:
260
+ rows.append(str(item))
261
+ return rows
262
+
263
+
264
+ def _normalize(values):
265
+ norm = 0.0
266
+ for value in values:
267
+ norm += value * value
268
+ norm = norm ** 0.5
269
+ if norm <= 1e-12:
270
+ return [0.0] * len(values)
271
+ return [value / norm for value in values]
272
+
273
+
274
+ class LocalEmbedder:
275
+ """Lazy CPU-only int8 ONNX embedder. One instance may be shared."""
276
+
277
+ def __init__(self, model_path, dim=DEFAULT_DIM, tokenizer_path=None,
278
+ max_length=DEFAULT_MAX_LENGTH, threads=DEFAULT_THREADS):
279
+ self.model_path = str(model_path or "").strip()
280
+ self.dim = _positive_int(dim, DEFAULT_DIM)
281
+ self.max_length = _positive_int(max_length, DEFAULT_MAX_LENGTH)
282
+ self.threads = max(1, min(MAX_THREADS,
283
+ _positive_int(threads, DEFAULT_THREADS)))
284
+ self.tokenizer_path = str(tokenizer_path or "").strip()
285
+ self._session = None
286
+ self._tokenizer = None
287
+ self._output_name = "embedding"
288
+ self._load_error = None
289
+ self._lock = threading.Lock()
290
+
291
+ # -- status -------------------------------------------------------
292
+ def status(self):
293
+ """ok | missing | failed (health string for /health)."""
294
+ if self._session is not None:
295
+ return "ok"
296
+ if self._load_error is not None:
297
+ return "failed"
298
+ if not self.model_path:
299
+ return "missing"
300
+ try:
301
+ self._model_file()
302
+ except LocalEmbeddingUnavailable:
303
+ return "failed"
304
+ return "missing"
305
+
306
+ def health(self):
307
+ return {"local_embedding": self.status(),
308
+ "model_path": self.model_path,
309
+ "dim": self.dim,
310
+ "threads": self.threads,
311
+ "load_error": str(self._load_error or "")[:200]}
312
+
313
+ # -- load ---------------------------------------------------------
314
+ def _model_file(self):
315
+ if not self.model_path:
316
+ raise LocalEmbeddingUnavailable(
317
+ "embedding_local_model_path is empty")
318
+ path = Path(self.model_path)
319
+ if path.is_dir():
320
+ path = path / "model.onnx"
321
+ if not path.is_file():
322
+ raise LocalEmbeddingUnavailable(
323
+ "local embedding model not found: %s" % path)
324
+ return path
325
+
326
+ def _vocab_file(self, model_file):
327
+ candidates = []
328
+
329
+ def add(path):
330
+ path = Path(path)
331
+ if path.suffix.lower() in (".json", ".txt"):
332
+ candidates.append(path)
333
+ return
334
+ candidates.append(path / "tokenizer.json")
335
+ candidates.append(path / "vocab.txt")
336
+
337
+ if self.tokenizer_path:
338
+ add(self.tokenizer_path)
339
+ env_path = os.environ.get(ENV_TOKENIZER, "").strip()
340
+ if env_path:
341
+ add(env_path)
342
+ add(model_file.parent)
343
+ add(model_file.parent / "tokenizer")
344
+ parent = model_file.parent.parent
345
+ if str(parent) != ".":
346
+ add(parent / "bge-small-zh")
347
+ add(parent / "bge-small-zh" / "tokenizer")
348
+ for candidate in candidates:
349
+ if candidate.is_file():
350
+ return candidate
351
+ raise LocalEmbeddingUnavailable(
352
+ "tokenizer (tokenizer.json or vocab.txt) not found for the local "
353
+ "embedding model; set embedding_local_tokenizer_path")
354
+
355
+ def load(self):
356
+ """Load onnxruntime + tokenizer once; raises LocalEmbeddingUnavailable."""
357
+ with self._lock:
358
+ if self._session is not None:
359
+ return self
360
+ if self._load_error is not None:
361
+ raise self._load_error
362
+ model_file = self._model_file()
363
+ vocab_file = self._vocab_file(model_file)
364
+ tokenizer = _VocabTokenizer(vocab_file)
365
+ try:
366
+ ort = _onnxruntime()
367
+ except Exception as exc:
368
+ error = LocalEmbeddingUnavailable(
369
+ "onnxruntime is not installed; local embedding fallback "
370
+ "is unavailable (%s)" % exc)
371
+ self._load_error = error
372
+ raise error
373
+ try:
374
+ options = ort.SessionOptions()
375
+ options.intra_op_num_threads = self.threads
376
+ options.inter_op_num_threads = 1
377
+ options.graph_optimization_level = (
378
+ ort.GraphOptimizationLevel.ORT_ENABLE_ALL)
379
+ options.log_severity_level = 3
380
+ session = ort.InferenceSession(
381
+ str(model_file), sess_options=options,
382
+ providers=["CPUExecutionProvider"])
383
+ except Exception as exc:
384
+ error = LocalEmbeddingUnavailable(
385
+ "failed to load local embedding model %s (%s)"
386
+ % (model_file, exc))
387
+ self._load_error = error
388
+ raise error
389
+ try:
390
+ names = [item.name for item in session.get_outputs()]
391
+ except Exception:
392
+ names = []
393
+ if "embedding" in names or not names:
394
+ self._output_name = "embedding"
395
+ else:
396
+ self._output_name = names[0]
397
+ self._session = session
398
+ self._tokenizer = tokenizer
399
+ return self
400
+
401
+ # -- inference ----------------------------------------------------
402
+ def encode(self, texts):
403
+ """Encode a string or a list of strings into L2-normalized vectors."""
404
+ rows = _as_texts(texts)
405
+ if not rows:
406
+ return []
407
+ self.load()
408
+ ids, mask = self._tokenizer.encode_batch(rows, self.max_length)
409
+ try:
410
+ np = _numpy()
411
+ except Exception as exc:
412
+ raise LocalEmbeddingError(
413
+ "numpy is required for onnxruntime inputs (%s)" % exc)
414
+ feeds = {"input_ids": np.asarray(ids, dtype=np.int64),
415
+ "attention_mask": np.asarray(mask, dtype=np.int64)}
416
+ try:
417
+ raw = self._session.run([self._output_name], feeds)[0]
418
+ except Exception as exc:
419
+ raise LocalEmbeddingError(
420
+ "local embedding inference failed (%s)" % exc)
421
+ output_rows = raw.tolist() if hasattr(raw, "tolist") else list(raw)
422
+ vectors = []
423
+ for row in output_rows:
424
+ try:
425
+ values = [float(item) for item in row]
426
+ except Exception as exc:
427
+ raise LocalEmbeddingError(
428
+ "local embedding produced a non-numeric row (%s)" % exc)
429
+ if len(values) != self.dim:
430
+ raise LocalEmbeddingError(
431
+ "dimension mismatch: expected %d, got %d"
432
+ % (self.dim, len(values)))
433
+ vectors.append(_normalize(values))
434
+ return vectors
435
+
436
+ def warmup(self):
437
+ """Force load + one inference so startup can prove health."""
438
+ self.encode(["warmup"])
439
+ return self
440
+
441
+
442
+ def create_local_embedder(model_path=None, dim=DEFAULT_DIM, tokenizer_path=None,
443
+ threads=DEFAULT_THREADS, max_length=DEFAULT_MAX_LENGTH):
444
+ """Build an embedder without loading it (caller decides when to load)."""
445
+ return LocalEmbedder(model_path=model_path, dim=dim,
446
+ tokenizer_path=tokenizer_path, threads=threads,
447
+ max_length=max_length)
@@ -0,0 +1,156 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Embedding backend mode resolution (auto probe; read-only).
3
+
4
+ task-211: embedding_mode now defaults to "auto" in the plugin schema. The
5
+ selection is a pure, read-only probe:
6
+
7
+ a) remote keys complete (base_url + model + key) -> remote
8
+ b) else local model file exists and onnxruntime is importable -> local
9
+ c) else -> off (lexical/strength recall only; never an error)
10
+
11
+ The probe never imports onnxruntime, never creates a session and never loads the
12
+ model. The model is loaded lazily on the first real embed (see
13
+ recall.embedding._LazyLocalEmbedder).
14
+
15
+ Explicit values keep working unchanged: off / remote / local /
16
+ remote_with_local_fallback. An old config without embedding_mode still uses the
17
+ legacy loader path (requested == "legacy").
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import importlib.util
22
+ import os
23
+ from pathlib import Path
24
+
25
+ MODES = ("auto", "off", "remote", "local", "remote_with_local_fallback")
26
+ REMOTE_KEYS = ("embedding_base_url", "embedding_model", "embedding_api_key")
27
+ ENV_BASE_URL = "BIOCORE_EMBEDDING_BASE_URL"
28
+ ENV_MODEL = "BIOCORE_EMBEDDING_MODEL"
29
+ ENV_API_KEY = "BIOCORE_EMBEDDING_KEY"
30
+ ENV_MODE = "BIOCORE_EMBEDDING_MODE"
31
+
32
+
33
+ def _text(value):
34
+ return str(value or "").strip()
35
+
36
+
37
+ def _flatten(config):
38
+ merged = {}
39
+ if isinstance(config, dict):
40
+ merged.update({key: value for key, value in config.items()
41
+ if not isinstance(value, dict)})
42
+ for key in ("embedding", "embeddings", "provider"):
43
+ nested = config.get(key)
44
+ if isinstance(nested, dict):
45
+ merged.update(nested)
46
+ return merged
47
+
48
+
49
+ def remote_settings(config=None):
50
+ """Remote endpoint keys with the same aliases/env fallbacks as recall."""
51
+ cfg = _flatten(config)
52
+ base_url = (cfg.get("embedding_base_url") or cfg.get("base_url")
53
+ or cfg.get("api_base") or cfg.get("url")
54
+ or cfg.get("embedding_api_base")
55
+ or os.environ.get(ENV_BASE_URL) or "")
56
+ model = (cfg.get("embedding_model") or cfg.get("model")
57
+ or os.environ.get(ENV_MODEL) or "")
58
+ api_key = (cfg.get("embedding_api_key") or cfg.get("api_key")
59
+ or cfg.get("key") or os.environ.get(ENV_API_KEY) or "")
60
+ return {"base_url": _text(base_url), "model": _text(model),
61
+ "api_key": _text(api_key)}
62
+
63
+
64
+ def remote_complete(config=None):
65
+ """True only when base_url, model AND key are all non-empty."""
66
+ settings = remote_settings(config)
67
+ return bool(settings["base_url"] and settings["model"]
68
+ and settings["api_key"]), settings
69
+
70
+
71
+ def local_model_file(model_path):
72
+ """Existing model.onnx path (a directory resolves to <dir>/model.onnx)."""
73
+ raw = _text(model_path)
74
+ if not raw:
75
+ return None
76
+ path = Path(raw)
77
+ if path.is_dir():
78
+ path = path / "model.onnx"
79
+ try:
80
+ return path if path.is_file() else None
81
+ except Exception:
82
+ return None
83
+
84
+
85
+ def onnxruntime_available(find_spec=None):
86
+ """Importability probe only: never imports or initializes onnxruntime."""
87
+ try:
88
+ finder = find_spec if find_spec is not None else importlib.util.find_spec
89
+ return finder("onnxruntime") is not None
90
+ except Exception:
91
+ return False
92
+
93
+
94
+ def resolve_mode(config=None, *, remote_ready=None, model_exists=None,
95
+ onnx_available=None, find_spec=None):
96
+ """Resolve the effective embedding backend.
97
+
98
+ Injectable booleans make the probe testable without a filesystem/runtime.
99
+ Returns {requested, mode, source, reason, remote_ready,
100
+ local_model_exists, local_model_path, onnxruntime_available}.
101
+ """
102
+ cfg = _flatten(config)
103
+ raw = cfg.get("embedding_mode")
104
+ if raw is None:
105
+ raw = os.environ.get(ENV_MODE)
106
+ requested = _text(raw).lower()
107
+ if remote_ready is None:
108
+ remote_ready, _settings = remote_complete(cfg)
109
+ else:
110
+ remote_ready = bool(remote_ready)
111
+ _settings = remote_settings(cfg)
112
+ model_path = _text(cfg.get("embedding_local_model_path"))
113
+ if model_exists is None:
114
+ local_file = local_model_file(model_path)
115
+ local_model_exists = local_file is not None
116
+ else:
117
+ local_model_exists = bool(model_exists)
118
+ local_file = Path(model_path) if model_path else None
119
+ if onnx_available is None:
120
+ onnx_available = onnxruntime_available(find_spec)
121
+ else:
122
+ onnx_available = bool(onnx_available)
123
+ base = {
124
+ "requested": requested or "legacy",
125
+ "remote_ready": bool(remote_ready),
126
+ "local_model_exists": bool(local_model_exists),
127
+ "local_model_path": str(local_file or model_path or ""),
128
+ "onnxruntime_available": bool(onnx_available),
129
+ }
130
+ if not requested:
131
+ base.update({"mode": "", "source": "legacy",
132
+ "reason": "旧配置未设置 embedding_mode:沿用旧规则"})
133
+ return base
134
+ if requested in ("off", "remote", "local", "remote_with_local_fallback"):
135
+ base.update({"mode": requested, "source": "explicit",
136
+ "reason": "显式配置 embedding_mode=" + requested})
137
+ return base
138
+ if requested != "auto":
139
+ base.update({"mode": "off", "source": "explicit",
140
+ "reason": "未知 embedding_mode=" + requested + ":按 off 处理"})
141
+ return base
142
+ if remote_ready:
143
+ base.update({"mode": "remote", "source": "auto",
144
+ "reason": "auto→remote:远程配置齐全(base_url + model + key)"})
145
+ return base
146
+ if local_model_exists and onnx_available:
147
+ base.update({"mode": "local", "source": "auto",
148
+ "reason": "auto→local:远程配置不全,但本地模型存在且 onnxruntime 可导入"})
149
+ return base
150
+ if local_model_exists and not onnx_available:
151
+ base.update({"mode": "off", "source": "auto",
152
+ "reason": "auto→off:本地模型存在但 onnxruntime 不可导入"})
153
+ return base
154
+ base.update({"mode": "off", "source": "auto",
155
+ "reason": "auto→off:远程配置不全且无本地模型"})
156
+ return base