dsh-biocore 0.2.0 → 0.3.0

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 (245) hide show
  1. package/README.md +11 -0
  2. package/lib/core-worker.js +268 -0
  3. package/lib/index.js +160 -70
  4. package/lib/modules/autonomy.js +125 -0
  5. package/lib/modules/cache-stats.js +48 -0
  6. package/lib/modules/core-turn.js +24 -6
  7. package/lib/modules/expression-inject.js +0 -10
  8. package/lib/modules/expression-refresh.js +0 -9
  9. package/lib/modules/memory-capture.js +2 -1
  10. package/lib/modules/memory-inject.js +50 -5
  11. package/lib/modules/memory-tools.js +7 -4
  12. package/lib/modules/python-env.js +46 -0
  13. package/lib/modules/safety-gate.js +0 -3
  14. package/lib/modules/series.js +54 -0
  15. package/lib/modules/switch.js +16 -0
  16. package/lib/py-bridge.js +81 -15
  17. package/package.json +1 -1
  18. package/python/biocore_memory/core/config.py +138 -127
  19. package/python/biocore_memory/engine.py +2653 -2476
  20. package/python/biocore_memory/recall/embedding.py +129 -0
  21. package/python/biocore_memory/recall/feedback.py +202 -0
  22. package/python/biocore_model/preference_drift.py +255 -0
  23. package/python/biocore_model/tests/__init__.py +1 -0
  24. package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
  25. package/python/biocore_model/tests/core_status_check.mjs +29 -0
  26. package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
  27. package/python/biocore_model/tests/leak_samples_run.py +18 -0
  28. package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
  29. package/python/biocore_model/tests/panel_html_check.mjs +63 -0
  30. package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
  31. package/python/biocore_model/tests/render_guard_check.mjs +19 -0
  32. package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
  33. package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
  34. package/python/biocore_model/tests/test_affective_labor.py +102 -0
  35. package/python/biocore_model/tests/test_appraisal.py +137 -0
  36. package/python/biocore_model/tests/test_appraisal_action.py +55 -0
  37. package/python/biocore_model/tests/test_arch_convergence.py +35 -0
  38. package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
  39. package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
  40. package/python/biocore_model/tests/test_attachment_state.py +29 -0
  41. package/python/biocore_model/tests/test_attribution.py +56 -0
  42. package/python/biocore_model/tests/test_attributor.py +108 -0
  43. package/python/biocore_model/tests/test_autonomic.py +69 -0
  44. package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
  45. package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
  46. package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
  47. package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
  48. package/python/biocore_model/tests/test_autonomy_check.py +40 -0
  49. package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
  50. package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
  51. package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
  52. package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
  53. package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
  54. package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
  55. package/python/biocore_model/tests/test_autonomy_model.py +60 -0
  56. package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
  57. package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
  58. package/python/biocore_model/tests/test_autonomy_params.py +163 -0
  59. package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
  60. package/python/biocore_model/tests/test_autonomy_self.py +153 -0
  61. package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
  62. package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
  63. package/python/biocore_model/tests/test_autonomy_status.py +35 -0
  64. package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
  65. package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
  66. package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
  67. package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
  68. package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
  69. package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
  70. package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
  71. package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
  72. package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
  73. package/python/biocore_model/tests/test_bio_tick.py +202 -0
  74. package/python/biocore_model/tests/test_boundary.py +183 -0
  75. package/python/biocore_model/tests/test_boundary_gate.py +295 -0
  76. package/python/biocore_model/tests/test_brain_split.py +99 -0
  77. package/python/biocore_model/tests/test_co_recall.py +350 -0
  78. package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
  79. package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
  80. package/python/biocore_model/tests/test_connection_bids.py +69 -0
  81. package/python/biocore_model/tests/test_core_bridge.py +88 -0
  82. package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
  83. package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
  84. package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
  85. package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
  86. package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
  87. package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
  88. package/python/biocore_model/tests/test_core_client.py +156 -0
  89. package/python/biocore_model/tests/test_core_commands.py +146 -0
  90. package/python/biocore_model/tests/test_core_mode.py +161 -0
  91. package/python/biocore_model/tests/test_core_state.py +240 -0
  92. package/python/biocore_model/tests/test_criticality.py +61 -0
  93. package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
  94. package/python/biocore_model/tests/test_debug_values.py +169 -0
  95. package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
  96. package/python/biocore_model/tests/test_delta_budget.py +157 -0
  97. package/python/biocore_model/tests/test_diag_trace.py +201 -0
  98. package/python/biocore_model/tests/test_directives_compat.py +698 -0
  99. package/python/biocore_model/tests/test_dissonance.py +97 -0
  100. package/python/biocore_model/tests/test_drift_sign.py +87 -0
  101. package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
  102. package/python/biocore_model/tests/test_embedding_local.py +279 -0
  103. package/python/biocore_model/tests/test_embedding_mode.py +175 -0
  104. package/python/biocore_model/tests/test_embedding_modes.py +342 -0
  105. package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
  106. package/python/biocore_model/tests/test_emotion_probe.py +214 -0
  107. package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
  108. package/python/biocore_model/tests/test_entity_graph.py +198 -0
  109. package/python/biocore_model/tests/test_entity_resolve.py +446 -0
  110. package/python/biocore_model/tests/test_envy.py +173 -0
  111. package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
  112. package/python/biocore_model/tests/test_expr_guard.py +1 -0
  113. package/python/biocore_model/tests/test_expr_intent.py +66 -0
  114. package/python/biocore_model/tests/test_expr_state.py +66 -0
  115. package/python/biocore_model/tests/test_expr_template.py +71 -0
  116. package/python/biocore_model/tests/test_expr_tone.py +41 -0
  117. package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
  118. package/python/biocore_model/tests/test_feedback_flow.py +185 -0
  119. package/python/biocore_model/tests/test_feedback_guard.py +65 -0
  120. package/python/biocore_model/tests/test_feedback_llm.py +59 -0
  121. package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
  122. package/python/biocore_model/tests/test_feedback_memory.py +63 -0
  123. package/python/biocore_model/tests/test_feedback_user.py +61 -0
  124. package/python/biocore_model/tests/test_frustration_curve.py +66 -0
  125. package/python/biocore_model/tests/test_global_bio_state.py +116 -0
  126. package/python/biocore_model/tests/test_governance.py +181 -0
  127. package/python/biocore_model/tests/test_granularity.py +133 -0
  128. package/python/biocore_model/tests/test_graph_extract.py +224 -0
  129. package/python/biocore_model/tests/test_graph_recall.py +226 -0
  130. package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
  131. package/python/biocore_model/tests/test_growth.py +301 -0
  132. package/python/biocore_model/tests/test_intent_render.py +60 -0
  133. package/python/biocore_model/tests/test_intero_regulation.py +224 -0
  134. package/python/biocore_model/tests/test_intero_style.py +146 -0
  135. package/python/biocore_model/tests/test_item3_numeric.py +105 -0
  136. package/python/biocore_model/tests/test_item6_numeric.py +116 -0
  137. package/python/biocore_model/tests/test_layered_memory.py +174 -0
  138. package/python/biocore_model/tests/test_leak_guard.py +75 -0
  139. package/python/biocore_model/tests/test_learning_probe.py +93 -0
  140. package/python/biocore_model/tests/test_life_loop.py +222 -0
  141. package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
  142. package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
  143. package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
  144. package/python/biocore_model/tests/test_memory_backend.py +332 -0
  145. package/python/biocore_model/tests/test_memory_dedup.py +316 -0
  146. package/python/biocore_model/tests/test_memory_dual.py +219 -0
  147. package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
  148. package/python/biocore_model/tests/test_memory_feedback.py +241 -0
  149. package/python/biocore_model/tests/test_memory_fixes.py +84 -0
  150. package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
  151. package/python/biocore_model/tests/test_memory_inject.py +211 -0
  152. package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
  153. package/python/biocore_model/tests/test_memory_store.py +372 -0
  154. package/python/biocore_model/tests/test_metacog.py +103 -0
  155. package/python/biocore_model/tests/test_migrate.py +245 -0
  156. package/python/biocore_model/tests/test_migrate_store.py +194 -0
  157. package/python/biocore_model/tests/test_mind_wander.py +65 -0
  158. package/python/biocore_model/tests/test_mood.py +35 -0
  159. package/python/biocore_model/tests/test_narrative.py +114 -0
  160. package/python/biocore_model/tests/test_needs_esteem.py +51 -0
  161. package/python/biocore_model/tests/test_needs_events.py +205 -0
  162. package/python/biocore_model/tests/test_needs_sleep.py +54 -0
  163. package/python/biocore_model/tests/test_noise_bank.py +54 -0
  164. package/python/biocore_model/tests/test_numeric_schema.py +98 -0
  165. package/python/biocore_model/tests/test_output_protocol.py +65 -0
  166. package/python/biocore_model/tests/test_page_debug.py +147 -0
  167. package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
  168. package/python/biocore_model/tests/test_pipeline.py +345 -0
  169. package/python/biocore_model/tests/test_pipeline_api.py +192 -0
  170. package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
  171. package/python/biocore_model/tests/test_plugin_config.py +85 -0
  172. package/python/biocore_model/tests/test_policy_learning.py +116 -0
  173. package/python/biocore_model/tests/test_possible_selves.py +88 -0
  174. package/python/biocore_model/tests/test_predictor.py +227 -0
  175. package/python/biocore_model/tests/test_preference_drift.py +142 -0
  176. package/python/biocore_model/tests/test_preset.py +142 -0
  177. package/python/biocore_model/tests/test_proactive_context.py +70 -0
  178. package/python/biocore_model/tests/test_proactive_history.py +90 -0
  179. package/python/biocore_model/tests/test_proactive_memory.py +141 -0
  180. package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
  181. package/python/biocore_model/tests/test_probe_contract.py +71 -0
  182. package/python/biocore_model/tests/test_prospective.py +154 -0
  183. package/python/biocore_model/tests/test_provider_fallback.py +89 -0
  184. package/python/biocore_model/tests/test_reconstruction.py +111 -0
  185. package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
  186. package/python/biocore_model/tests/test_relation_stage.py +114 -0
  187. package/python/biocore_model/tests/test_relationship_stage.py +59 -0
  188. package/python/biocore_model/tests/test_release_140.py +55 -0
  189. package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
  190. package/python/biocore_model/tests/test_reply_limit.py +86 -0
  191. package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
  192. package/python/biocore_model/tests/test_reply_split.py +52 -0
  193. package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
  194. package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
  195. package/python/biocore_model/tests/test_rerank.py +229 -0
  196. package/python/biocore_model/tests/test_rollback.py +175 -0
  197. package/python/biocore_model/tests/test_runtime.py +175 -0
  198. package/python/biocore_model/tests/test_runtime_active.py +205 -0
  199. package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
  200. package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
  201. package/python/biocore_model/tests/test_scheduler.py +79 -0
  202. package/python/biocore_model/tests/test_secrecy.py +105 -0
  203. package/python/biocore_model/tests/test_self_esteem.py +45 -0
  204. package/python/biocore_model/tests/test_self_intent.py +347 -0
  205. package/python/biocore_model/tests/test_self_interest.py +183 -0
  206. package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
  207. package/python/biocore_model/tests/test_self_verification.py +113 -0
  208. package/python/biocore_model/tests/test_situation.py +142 -0
  209. package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
  210. package/python/biocore_model/tests/test_social_pain.py +49 -0
  211. package/python/biocore_model/tests/test_social_repair.py +67 -0
  212. package/python/biocore_model/tests/test_social_theory.py +78 -0
  213. package/python/biocore_model/tests/test_source_monitoring.py +204 -0
  214. package/python/biocore_model/tests/test_speak_like_group.py +36 -0
  215. package/python/biocore_model/tests/test_stages.py +226 -0
  216. package/python/biocore_model/tests/test_state_tag.py +86 -0
  217. package/python/biocore_model/tests/test_template_expr.py +65 -0
  218. package/python/biocore_model/tests/test_text_inference.py +70 -0
  219. package/python/biocore_model/tests/test_theta_params.py +115 -0
  220. package/python/biocore_model/tests/test_topic_control.py +200 -0
  221. package/python/biocore_model/tests/test_turn_memory.py +105 -0
  222. package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
  223. package/python/biocore_model/tests/test_ui_settings.py +84 -0
  224. package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
  225. package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
  226. package/python/biocore_model/tests/test_user_classify.py +122 -0
  227. package/python/biocore_model/tests/test_value_order.py +117 -0
  228. package/python/biocore_model/tests/test_voice_bank.py +105 -0
  229. package/python/biocore_model/tests/test_waiting_sense.py +216 -0
  230. package/python/biocore_model/tests/test_x_directives.py +101 -0
  231. package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
  232. package/python/biocore_model/tests/view_mode_check.mjs +17 -0
  233. package/python/biocore_runtime/autonomy.py +208 -0
  234. package/python/biocore_runtime/core_state.py +302 -0
  235. package/python/biocore_runtime/governance.py +495 -0
  236. package/python/biocore_runtime/growth.py +528 -0
  237. package/python/biocore_runtime/pipeline.py +850 -23
  238. package/python/biocore_runtime/relationship_state.py +214 -0
  239. package/python/biocore_runtime/self_layer.py +103 -0
  240. package/python/biocore_runtime/service.py +162 -0
  241. package/python/biocore_runtime/standby.py +472 -0
  242. package/ui/panel-layout.js +83 -39
  243. package/ui/panel.css +885 -743
  244. package/ui/panel.html +356 -296
  245. package/ui/panel.js +739 -865
@@ -0,0 +1,279 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-192 本地 int8 ONNX embedder 单测(CPU-only、懒加载、明确异常)。
3
+
4
+ 不依赖真实模型/onnxruntime:用临时 tokenizer.json + 假 session/fake onnxruntime
5
+ 模块验证加载、池化/归一化、维度校验、异常与 tokenizer 行为。
6
+ """
7
+ import json
8
+ import tempfile
9
+ import types
10
+ import unittest
11
+ from pathlib import Path
12
+ from unittest import mock
13
+
14
+ from biocore_memory import embedding_local as el
15
+
16
+ VOCAB = {
17
+ "[PAD]": 0, "[UNK]": 100, "[CLS]": 101, "[SEP]": 102,
18
+ "你": 200, "好": 201, ",": 202, "世": 203, "界": 204,
19
+ "今": 205, "天": 206, "abc": 207,
20
+ }
21
+
22
+
23
+ def _write_tokenizer(directory, vocab=None):
24
+ path = Path(directory) / "tokenizer.json"
25
+ payload = {"model": {"type": "WordPiece", "vocab": vocab or VOCAB,
26
+ "unk_token": "[UNK]",
27
+ "continuing_subword_prefix": "##"}}
28
+ path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
29
+ return path
30
+
31
+
32
+ class FakeSession:
33
+ def __init__(self, output):
34
+ self.output = output
35
+ self.calls = 0
36
+ self.feeds = None
37
+
38
+ def run(self, names, feeds):
39
+ self.calls += 1
40
+ self.feeds = feeds
41
+ return [self.output]
42
+
43
+
44
+ class FakeOptions:
45
+ intra_op_num_threads = 0
46
+ inter_op_num_threads = 0
47
+ graph_optimization_level = 0
48
+ log_severity_level = 0
49
+
50
+
51
+ class FakeOrtModule:
52
+ captured = {}
53
+
54
+ class GraphOptimizationLevel:
55
+ ORT_ENABLE_ALL = 99
56
+
57
+ @staticmethod
58
+ def SessionOptions():
59
+ return FakeOptions()
60
+
61
+ class InferenceSession:
62
+ def __init__(self, path, sess_options=None, providers=None):
63
+ FakeOrtModule.captured = {
64
+ "path": str(path), "threads": getattr(
65
+ sess_options, "intra_op_num_threads", None),
66
+ "providers": list(providers or []),
67
+ }
68
+ self.batch_size = 0
69
+
70
+ def get_outputs(self):
71
+ return [types.SimpleNamespace(name="embedding")]
72
+
73
+ def run(self, names, feeds):
74
+ size = len(feeds["input_ids"])
75
+ return [[[0.0, 1.0, 0.0, 0.0] for _ in range(size)]]
76
+
77
+
78
+ class LocalEmbedderTest(unittest.TestCase):
79
+ def setUp(self):
80
+ self._tmp = tempfile.TemporaryDirectory()
81
+ self.root = Path(self._tmp.name)
82
+ self.model = self.root / "model.onnx"
83
+ self.model.write_bytes(b"fake-onnx")
84
+ self.tokenizer = _write_tokenizer(self.root)
85
+
86
+ def tearDown(self):
87
+ self._tmp.cleanup()
88
+
89
+ def _embedder(self, **kwargs):
90
+ return el.LocalEmbedder(self.model, dim=4,
91
+ tokenizer_path=self.tokenizer, **kwargs)
92
+
93
+ def test_status_missing_before_load(self):
94
+ embedder = self._embedder()
95
+ self.assertEqual(embedder.status(), "missing")
96
+ self.assertEqual(embedder.health()["local_embedding"], "missing")
97
+
98
+ def test_status_ok_after_injected_session(self):
99
+ embedder = self._embedder()
100
+ embedder._session = FakeSession([[3.0, 4.0, 0.0, 0.0]])
101
+ embedder._tokenizer = el._VocabTokenizer(self.tokenizer)
102
+ self.assertEqual(embedder.status(), "ok")
103
+
104
+ def test_encode_cls_and_l2_normalize(self):
105
+ embedder = self._embedder()
106
+ embedder._session = FakeSession([[3.0, 4.0, 0.0, 0.0]])
107
+ embedder._tokenizer = el._VocabTokenizer(self.tokenizer)
108
+ vectors = embedder.encode("你好")
109
+ self.assertEqual(len(vectors), 1)
110
+ self.assertAlmostEqual(vectors[0][0], 0.6, places=5)
111
+ self.assertAlmostEqual(vectors[0][1], 0.8, places=5)
112
+ norm = sum(value * value for value in vectors[0]) ** 0.5
113
+ self.assertAlmostEqual(norm, 1.0, places=5)
114
+
115
+ def test_batch_order_and_padding(self):
116
+ embedder = self._embedder()
117
+ embedder._session = FakeSession([[1.0, 0.0, 0.0, 0.0],
118
+ [0.0, 1.0, 0.0, 0.0]])
119
+ embedder._tokenizer = el._VocabTokenizer(self.tokenizer)
120
+ vectors = embedder.encode(["你好", "今天,世界"])
121
+ self.assertEqual(len(vectors), 2)
122
+ self.assertAlmostEqual(vectors[0][0], 1.0)
123
+ self.assertAlmostEqual(vectors[1][1], 1.0)
124
+ feeds = embedder._session.feeds
125
+ self.assertEqual(len(feeds["input_ids"]), 2)
126
+ self.assertEqual(feeds["input_ids"].shape[0], 2)
127
+ self.assertIn(0, feeds["attention_mask"][0])
128
+
129
+ def test_dimension_mismatch_raises(self):
130
+ embedder = self._embedder()
131
+ embedder._session = FakeSession([[1.0, 0.0, 0.0]])
132
+ embedder._tokenizer = el._VocabTokenizer(self.tokenizer)
133
+ with self.assertRaises(el.LocalEmbeddingError):
134
+ embedder.encode("你好")
135
+
136
+ def test_non_numeric_output_raises(self):
137
+ embedder = self._embedder()
138
+ embedder._session = FakeSession([["x", 0.0, 0.0, 0.0]])
139
+ embedder._tokenizer = el._VocabTokenizer(self.tokenizer)
140
+ with self.assertRaises(el.LocalEmbeddingError):
141
+ embedder.encode("你好")
142
+
143
+ def test_malformed_texts(self):
144
+ embedder = self._embedder()
145
+ embedder._session = FakeSession([[1.0, 0.0, 0.0, 0.0]])
146
+ embedder._tokenizer = el._VocabTokenizer(self.tokenizer)
147
+ self.assertEqual(embedder.encode(None), [])
148
+ self.assertEqual(embedder.encode([]), [])
149
+ with self.assertRaises(el.LocalEmbeddingError):
150
+ embedder.encode(123)
151
+ with self.assertRaises(el.LocalEmbeddingError):
152
+ embedder.encode({"a": 1})
153
+
154
+ def test_threads_default_and_clamped(self):
155
+ self.assertEqual(el.LocalEmbedder(self.model).threads, 2)
156
+ self.assertEqual(el.LocalEmbedder(self.model, threads=99).threads, 8)
157
+ self.assertEqual(el.LocalEmbedder(self.model, threads=0).threads, 2)
158
+
159
+
160
+ class LoadFailureTest(unittest.TestCase):
161
+ def setUp(self):
162
+ self._tmp = tempfile.TemporaryDirectory()
163
+ self.root = Path(self._tmp.name)
164
+ self.tokenizer = _write_tokenizer(self.root)
165
+
166
+ def tearDown(self):
167
+ self._tmp.cleanup()
168
+
169
+ def test_missing_model_path(self):
170
+ embedder = el.LocalEmbedder("", tokenizer_path=self.tokenizer)
171
+ self.assertEqual(embedder.status(), "missing")
172
+ with self.assertRaises(el.LocalEmbeddingUnavailable):
173
+ embedder.encode(["x"])
174
+
175
+ def test_model_file_absent(self):
176
+ embedder = el.LocalEmbedder(self.root / "nope.onnx",
177
+ tokenizer_path=self.tokenizer)
178
+ with self.assertRaises(el.LocalEmbeddingUnavailable) as ctx:
179
+ embedder.load()
180
+ self.assertIn("not found", str(ctx.exception))
181
+ self.assertEqual(embedder.status(), "failed")
182
+
183
+ def test_missing_onnxruntime(self):
184
+ model = self.root / "model.onnx"
185
+ model.write_bytes(b"fake")
186
+ embedder = el.LocalEmbedder(model, tokenizer_path=self.tokenizer)
187
+ with mock.patch.object(el, "_onnxruntime",
188
+ side_effect=ImportError("no onnxruntime")):
189
+ with self.assertRaises(el.LocalEmbeddingUnavailable) as ctx:
190
+ embedder.load()
191
+ self.assertIn("onnxruntime", str(ctx.exception))
192
+ self.assertEqual(embedder.status(), "failed")
193
+
194
+ def test_missing_tokenizer(self):
195
+ isolated = self.root / "isolated"
196
+ isolated.mkdir()
197
+ model = isolated / "model.onnx"
198
+ model.write_bytes(b"fake")
199
+ with mock.patch.dict("os.environ",
200
+ {el.ENV_TOKENIZER: ""}):
201
+ embedder = el.LocalEmbedder(model)
202
+ with self.assertRaises(el.LocalEmbeddingUnavailable) as ctx:
203
+ embedder.load()
204
+ self.assertIn("tokenizer", str(ctx.exception))
205
+
206
+ def test_cpu_only_session_options(self):
207
+ model = self.root / "model.onnx"
208
+ model.write_bytes(b"fake")
209
+ embedder = el.LocalEmbedder(model, dim=4,
210
+ tokenizer_path=self.tokenizer, threads=3)
211
+ fake_numpy = types.SimpleNamespace(
212
+ asarray=lambda seq, dtype=None: seq, int64=object)
213
+ with mock.patch.object(el, "_onnxruntime",
214
+ return_value=FakeOrtModule), \
215
+ mock.patch.object(el, "_numpy", return_value=fake_numpy):
216
+ vectors = embedder.encode(["你好"])
217
+ self.assertEqual(len(vectors), 1)
218
+ captured = FakeOrtModule.captured
219
+ self.assertEqual(captured["providers"], ["CPUExecutionProvider"])
220
+ self.assertEqual(captured["threads"], 3)
221
+ self.assertNotIn("CUDAExecutionProvider",
222
+ " ".join(captured["providers"]))
223
+
224
+ def test_no_torch_or_cuda_source_dependency(self):
225
+ source = Path(el.__file__).read_text(encoding="utf-8")
226
+ self.assertNotIn("import torch", source)
227
+ self.assertNotIn("CUDAExecutionProvider", source)
228
+
229
+
230
+ class TokenizerTest(unittest.TestCase):
231
+ def setUp(self):
232
+ self._tmp = tempfile.TemporaryDirectory()
233
+ self.tokenizer = _write_tokenizer(self._tmp.name)
234
+ self.vocab = el._VocabTokenizer(self.tokenizer)
235
+
236
+ def tearDown(self):
237
+ self._tmp.cleanup()
238
+
239
+ def test_chinese_char_split(self):
240
+ ids, mask = self.vocab.encode("你好")
241
+ self.assertEqual(ids, [101, 200, 201, 102])
242
+ self.assertEqual(mask, [1, 1, 1, 1])
243
+
244
+ def test_punctuation_split(self):
245
+ ids, _ = self.vocab.encode("你好,天")
246
+ self.assertEqual(ids, [101, 200, 201, 202, 206, 102])
247
+
248
+ def test_batch_padding_masks(self):
249
+ ids_rows, mask_rows = self.vocab.encode_batch(["你好", "你好,天"])
250
+ self.assertEqual(len(ids_rows), 2)
251
+ self.assertEqual(len(ids_rows[0]), len(ids_rows[1]))
252
+ self.assertEqual(len(ids_rows[1]), 6)
253
+ self.assertEqual(mask_rows[0][:4], [1, 1, 1, 1])
254
+ self.assertEqual(mask_rows[0][-2:], [0, 0])
255
+ self.assertTrue(all(value == 1 for value in mask_rows[1]))
256
+
257
+ def test_max_length_truncation(self):
258
+ ids, _ = self.vocab.encode("今天世界" * 20, max_length=8)
259
+ self.assertLessEqual(len(ids), 8)
260
+ self.assertEqual(ids[0], 101)
261
+ self.assertEqual(ids[-1], 102)
262
+
263
+ def test_vocab_txt_fallback(self):
264
+ txt = Path(self._tmp.name) / "vocab.txt"
265
+ lines = ["[PAD]", "[UNK]", "[CLS]", "[SEP]"] + ["你", "好"]
266
+ txt.write_text("\n".join(lines), encoding="utf-8")
267
+ vocab = el._VocabTokenizer(txt)
268
+ self.assertEqual(vocab.encode("你好")[0], [2, 4, 5, 3])
269
+
270
+ def test_missing_specials_rejected(self):
271
+ path = Path(self._tmp.name) / "bad.json"
272
+ path.write_text(json.dumps({"model": {"vocab": {"a": 0}}}),
273
+ encoding="utf-8")
274
+ with self.assertRaises(el.LocalEmbeddingUnavailable):
275
+ el._VocabTokenizer(path)
276
+
277
+
278
+ if __name__ == "__main__":
279
+ unittest.main()
@@ -0,0 +1,175 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-211: embedding_mode auto probe tests (>=8 cases).
3
+
4
+ Pure resolver tests plus one loader integration check. No model/onnxruntime
5
+ import is ever required: the probe is resolved from injectable booleans.
6
+ """
7
+ import tempfile
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ from biocore_memory.embedding_mode import (
12
+ resolve_mode, remote_complete, local_model_file, onnxruntime_available)
13
+ from biocore_memory.recall import embedding
14
+
15
+
16
+ REMOTE = {"embedding_base_url": "https://api.example.com/v1",
17
+ "embedding_model": "bge-m3", "embedding_api_key": "sk-test"}
18
+
19
+
20
+ class RemoteProbeTest(unittest.TestCase):
21
+ def test_remote_complete_requires_all_three(self):
22
+ self.assertTrue(remote_complete(REMOTE)[0])
23
+ for key in ("embedding_base_url", "embedding_model", "embedding_api_key"):
24
+ partial = dict(REMOTE)
25
+ partial[key] = ""
26
+ self.assertFalse(remote_complete(partial)[0], key)
27
+
28
+ def test_remote_aliases(self):
29
+ alias = {"base_url": "https://x/v1", "model": "m", "api_key": "k"}
30
+ self.assertTrue(remote_complete(alias)[0])
31
+
32
+
33
+ class AutoMatrixTest(unittest.TestCase):
34
+ def test_auto_remote_when_complete(self):
35
+ out = resolve_mode({"embedding_mode": "auto", **REMOTE},
36
+ remote_ready=True, model_exists=False,
37
+ onnx_available=False)
38
+ self.assertEqual(out["mode"], "remote")
39
+ self.assertEqual(out["source"], "auto")
40
+ self.assertIn("远程配置齐全", out["reason"])
41
+
42
+ def test_auto_local_when_remote_missing(self):
43
+ out = resolve_mode({"embedding_mode": "auto"},
44
+ remote_ready=False, model_exists=True,
45
+ onnx_available=True)
46
+ self.assertEqual(out["mode"], "local")
47
+ self.assertEqual(out["source"], "auto")
48
+ self.assertIn("本地模型存在", out["reason"])
49
+
50
+ def test_auto_off_when_neither(self):
51
+ out = resolve_mode({"embedding_mode": "auto"},
52
+ remote_ready=False, model_exists=False,
53
+ onnx_available=False)
54
+ self.assertEqual(out["mode"], "off")
55
+ self.assertIn("无本地模型", out["reason"])
56
+
57
+ def test_auto_off_when_onnx_missing(self):
58
+ out = resolve_mode({"embedding_mode": "auto"},
59
+ remote_ready=False, model_exists=True,
60
+ onnx_available=False)
61
+ self.assertEqual(out["mode"], "off")
62
+ self.assertIn("onnxruntime 不可导入", out["reason"])
63
+
64
+ def test_auto_prefers_remote_over_local(self):
65
+ out = resolve_mode({"embedding_mode": "auto", **REMOTE},
66
+ remote_ready=True, model_exists=True,
67
+ onnx_available=True)
68
+ self.assertEqual(out["mode"], "remote")
69
+
70
+
71
+ class ExplicitOverrideTest(unittest.TestCase):
72
+ def test_explicit_off_wins(self):
73
+ out = resolve_mode({"embedding_mode": "off", **REMOTE},
74
+ remote_ready=True, model_exists=True,
75
+ onnx_available=True)
76
+ self.assertEqual(out["mode"], "off")
77
+ self.assertEqual(out["source"], "explicit")
78
+
79
+ def test_explicit_remote_wins_even_if_incomplete(self):
80
+ out = resolve_mode({"embedding_mode": "remote"},
81
+ remote_ready=False, model_exists=True,
82
+ onnx_available=True)
83
+ self.assertEqual(out["mode"], "remote")
84
+ self.assertEqual(out["source"], "explicit")
85
+
86
+ def test_explicit_local_wins_even_without_model(self):
87
+ out = resolve_mode({"embedding_mode": "local", **REMOTE},
88
+ remote_ready=True, model_exists=False,
89
+ onnx_available=False)
90
+ self.assertEqual(out["mode"], "local")
91
+ self.assertEqual(out["source"], "explicit")
92
+
93
+ def test_explicit_fallback_wins(self):
94
+ out = resolve_mode({"embedding_mode": "remote_with_local_fallback"},
95
+ remote_ready=True, model_exists=True,
96
+ onnx_available=True)
97
+ self.assertEqual(out["mode"], "remote_with_local_fallback")
98
+ self.assertEqual(out["source"], "explicit")
99
+
100
+ def test_unknown_value_falls_back_to_off(self):
101
+ out = resolve_mode({"embedding_mode": "bogus"},
102
+ remote_ready=True, model_exists=True,
103
+ onnx_available=True)
104
+ self.assertEqual(out["mode"], "off")
105
+ self.assertEqual(out["source"], "explicit")
106
+
107
+ def test_legacy_config_keeps_old_behaviour(self):
108
+ out = resolve_mode({}, remote_ready=True, model_exists=True,
109
+ onnx_available=True)
110
+ self.assertEqual(out["mode"], "")
111
+ self.assertEqual(out["source"], "legacy")
112
+
113
+
114
+ class FilesystemProbeTest(unittest.TestCase):
115
+ def test_local_model_file_accepts_dir_and_file(self):
116
+ with tempfile.TemporaryDirectory() as tmp:
117
+ model = Path(tmp) / "model.onnx"
118
+ model.write_bytes(b"fake")
119
+ self.assertEqual(local_model_file(tmp), model)
120
+ self.assertEqual(local_model_file(str(model)), model)
121
+ self.assertIsNone(local_model_file(""))
122
+
123
+ def test_real_probe_with_injected_find_spec(self):
124
+ with tempfile.TemporaryDirectory() as tmp:
125
+ model = Path(tmp) / "model.onnx"
126
+ model.write_bytes(b"fake")
127
+ out = resolve_mode(
128
+ {"embedding_mode": "auto", "embedding_local_model_path": str(model)},
129
+ find_spec=lambda name: object())
130
+ self.assertEqual(out["mode"], "local")
131
+ self.assertTrue(out["local_model_exists"])
132
+ self.assertTrue(out["onnxruntime_available"])
133
+
134
+ def test_onnxruntime_probe_is_boolean(self):
135
+ self.assertIsInstance(onnxruntime_available(), bool)
136
+
137
+
138
+ class LoaderIntegrationTest(unittest.TestCase):
139
+ def test_loader_auto_remote_attaches_mode(self):
140
+ provider = embedding.load_provider(
141
+ {"embedding_mode": "auto", **REMOTE})
142
+ self.assertIsNotNone(provider)
143
+ self.assertEqual(getattr(provider, "embedding_mode", ""), "remote")
144
+ self.assertEqual(getattr(provider, "embedding_mode_source", ""), "auto")
145
+
146
+ def test_loader_auto_off_returns_none(self):
147
+ provider = embedding.load_provider({"embedding_mode": "auto"})
148
+ self.assertIsNone(provider)
149
+
150
+ def test_loader_health_reports_mode(self):
151
+ provider = embedding.load_provider(
152
+ {"embedding_mode": "auto", **REMOTE})
153
+ health = embedding.embedding_health(provider)
154
+ self.assertEqual(health.get("embedding_mode"), "remote")
155
+ self.assertEqual(health.get("embedding_mode_source"), "auto")
156
+
157
+ @unittest.skipUnless(onnxruntime_available(),
158
+ "auto local probe needs onnxruntime importable")
159
+ def test_loader_auto_local_is_lazy(self):
160
+ with tempfile.TemporaryDirectory() as tmp:
161
+ model = Path(tmp) / "model.onnx"
162
+ model.write_bytes(b"not-a-real-onnx")
163
+ provider = embedding.load_provider({
164
+ "embedding_mode": "auto",
165
+ "embedding_local_model_path": str(model),
166
+ })
167
+ self.assertIsNotNone(provider)
168
+ self.assertEqual(getattr(provider, "embedding_mode", ""), "local")
169
+ # Model load is deferred to the first real encode.
170
+ self.assertIsInstance(provider.local_embedder,
171
+ embedding._LazyLocalEmbedder)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ unittest.main()