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.
- package/README.md +2 -2
- package/lib/py/bridge.py +15 -7
- package/lib/py-bridge.js +4 -3
- package/package.json +3 -2
- package/python/biocore_memory/__init__.py +21 -0
- package/python/biocore_memory/adapters/__init__.py +2 -0
- package/python/biocore_memory/adapters/astrbot.py +24 -0
- package/python/biocore_memory/adapters/dsh_cli.py +335 -0
- package/python/biocore_memory/co_recall.py +324 -0
- package/python/biocore_memory/consolidate/__init__.py +11 -0
- package/python/biocore_memory/consolidate/gated.py +104 -0
- package/python/biocore_memory/consolidate/neighbors.py +68 -0
- package/python/biocore_memory/consolidate/reflection.py +45 -0
- package/python/biocore_memory/consolidate/tree.py +95 -0
- package/python/biocore_memory/consolidate/writeback.py +181 -0
- package/python/biocore_memory/core/__init__.py +4 -0
- package/python/biocore_memory/core/config.py +127 -0
- package/python/biocore_memory/core/permissions.py +69 -0
- package/python/biocore_memory/core/types.py +116 -0
- package/python/biocore_memory/core/util.py +26 -0
- package/python/biocore_memory/embedding_local.py +447 -0
- package/python/biocore_memory/embedding_mode.py +156 -0
- package/python/biocore_memory/engine.py +2476 -0
- package/python/biocore_memory/engine_helpers.py +266 -0
- package/python/biocore_memory/event/__init__.py +3 -0
- package/python/biocore_memory/event/_types.py +16 -0
- package/python/biocore_memory/event/consistency.py +117 -0
- package/python/biocore_memory/event/extract.py +48 -0
- package/python/biocore_memory/event/media.py +111 -0
- package/python/biocore_memory/event/notes.py +85 -0
- package/python/biocore_memory/event/notes_link.py +87 -0
- package/python/biocore_memory/event/vector.py +69 -0
- package/python/biocore_memory/evict/__init__.py +3 -0
- package/python/biocore_memory/evict/adaptive.py +73 -0
- package/python/biocore_memory/evict/consequence.py +58 -0
- package/python/biocore_memory/evict/heat.py +54 -0
- package/python/biocore_memory/evict/inhibition.py +105 -0
- package/python/biocore_memory/evict/policy.py +188 -0
- package/python/biocore_memory/evict/score.py +23 -0
- package/python/biocore_memory/identity/__init__.py +6 -0
- package/python/biocore_memory/identity/api.py +99 -0
- package/python/biocore_memory/identity/history.py +9 -0
- package/python/biocore_memory/identity/ledger.py +191 -0
- package/python/biocore_memory/identity/narrative.py +358 -0
- package/python/biocore_memory/identity/snapshot.py +13 -0
- package/python/biocore_memory/identity/writeback.py +61 -0
- package/python/biocore_memory/index/__init__.py +3 -0
- package/python/biocore_memory/index/cue_index.py +80 -0
- package/python/biocore_memory/index/entities.py +85 -0
- package/python/biocore_memory/index/entity_boost.py +104 -0
- package/python/biocore_memory/index/graph.py +172 -0
- package/python/biocore_memory/recall/__init__.py +5 -0
- package/python/biocore_memory/recall/api.py +65 -0
- package/python/biocore_memory/recall/bm25.py +118 -0
- package/python/biocore_memory/recall/channels.py +98 -0
- package/python/biocore_memory/recall/complexity.py +62 -0
- package/python/biocore_memory/recall/constraints.py +75 -0
- package/python/biocore_memory/recall/embedding.py +745 -0
- package/python/biocore_memory/recall/expand.py +26 -0
- package/python/biocore_memory/recall/filters.py +40 -0
- package/python/biocore_memory/recall/fusion.py +18 -0
- package/python/biocore_memory/recall/mmr.py +70 -0
- package/python/biocore_memory/recall/ppr.py +85 -0
- package/python/biocore_memory/recall/retrieval_practice.py +102 -0
- package/python/biocore_memory/relay/__init__.py +2 -0
- package/python/biocore_memory/relay/card.py +51 -0
- package/python/biocore_memory/slots/__init__.py +18 -0
- package/python/biocore_memory/slots/admission.py +64 -0
- package/python/biocore_memory/slots/boundary.py +128 -0
- package/python/biocore_memory/slots/evict_bridge.py +106 -0
- package/python/biocore_memory/slots/registry.py +135 -0
- package/python/biocore_memory/slots/switch.py +42 -0
- package/python/biocore_memory/store/__init__.py +4 -0
- package/python/biocore_memory/store/audit.py +74 -0
- package/python/biocore_memory/store/compress.py +81 -0
- package/python/biocore_memory/store/dedupe.py +178 -0
- package/python/biocore_memory/store/json_store.py +154 -0
- package/python/biocore_memory/store/media.py +145 -0
- package/python/biocore_memory/store/provenance.py +73 -0
- package/python/biocore_memory/store/rate_limit.py +93 -0
- package/python/biocore_memory/store/security.py +107 -0
- package/python/biocore_memory/store/source_consistency.py +123 -0
- package/python/biocore_memory/store/tiering.py +61 -0
- package/python/biocore_memory/store/tiering_exec.py +50 -0
- package/python/biocore_memory/store/tombstone.py +23 -0
- package/python/biocore_memory/store/versions.py +69 -0
- package/python/biocore_memory/thread/__init__.py +2 -0
- package/python/biocore_memory/thread/card.py +58 -0
- package/python/biocore_model/__init__.py +37 -0
- package/python/biocore_model/affective_labor.py +162 -0
- package/python/biocore_model/appraisal.py +257 -0
- package/python/biocore_model/appraisal_action.py +78 -0
- package/python/biocore_model/archive_queue.py +60 -0
- package/python/biocore_model/attachment_behavior.py +130 -0
- package/python/biocore_model/attachment_regulation.py +75 -0
- package/python/biocore_model/attachment_state.py +41 -0
- package/python/biocore_model/attribution.py +73 -0
- package/python/biocore_model/attributor.py +162 -0
- package/python/biocore_model/autonomic.py +91 -0
- package/python/biocore_model/autonomy_affect.py +51 -0
- package/python/biocore_model/autonomy_check.py +67 -0
- package/python/biocore_model/autonomy_loop.py +122 -0
- package/python/biocore_model/autonomy_mode.py +114 -0
- package/python/biocore_model/autonomy_model.py +733 -0
- package/python/biocore_model/autonomy_params.py +131 -0
- package/python/biocore_model/autonomy_reason.py +73 -0
- package/python/biocore_model/autonomy_tick.py +227 -0
- package/python/biocore_model/bio_appraisal.py +49 -0
- package/python/biocore_model/bio_autonomy_state.py +171 -0
- package/python/biocore_model/bio_config.py +221 -0
- package/python/biocore_model/bio_const.py +128 -0
- package/python/biocore_model/bio_daily.py +267 -0
- package/python/biocore_model/bio_emotion.py +538 -0
- package/python/biocore_model/bio_emotion2.py +214 -0
- package/python/biocore_model/bio_goal.py +220 -0
- package/python/biocore_model/bio_identity.py +82 -0
- package/python/biocore_model/bio_noise.py +119 -0
- package/python/biocore_model/bio_physiology2.py +114 -0
- package/python/biocore_model/bio_snapshot.py +324 -0
- package/python/biocore_model/bio_state.py +272 -0
- package/python/biocore_model/bio_tick.py +310 -0
- package/python/biocore_model/bio_util.py +82 -0
- package/python/biocore_model/blocks.py +264 -0
- package/python/biocore_model/boundary.py +251 -0
- package/python/biocore_model/boundary_gate.py +188 -0
- package/python/biocore_model/commitment_extractor.py +517 -0
- package/python/biocore_model/compassion_fatigue.py +84 -0
- package/python/biocore_model/connection_bids.py +120 -0
- package/python/biocore_model/core_bridge.py +177 -0
- package/python/biocore_model/core_client.py +127 -0
- package/python/biocore_model/core_commands.py +112 -0
- package/python/biocore_model/core_mode.py +104 -0
- package/python/biocore_model/core_mode_ui.mjs +104 -0
- package/python/biocore_model/criticality.py +81 -0
- package/python/biocore_model/defense_mechanisms.py +272 -0
- package/python/biocore_model/delta_budget.py +99 -0
- package/python/biocore_model/diag_trace.py +274 -0
- package/python/biocore_model/dissonance.py +134 -0
- package/python/biocore_model/emotion_geometry.py +108 -0
- package/python/biocore_model/emotion_probe.py +209 -0
- package/python/biocore_model/entity_graph.py +931 -0
- package/python/biocore_model/entity_resolve.py +260 -0
- package/python/biocore_model/envy.py +193 -0
- package/python/biocore_model/exit_or_silence.py +230 -0
- package/python/biocore_model/expr_guard.py +24 -0
- package/python/biocore_model/expr_intent.py +77 -0
- package/python/biocore_model/expr_state.py +204 -0
- package/python/biocore_model/expr_template.py +105 -0
- package/python/biocore_model/expr_tone.py +81 -0
- package/python/biocore_model/extreme_scenarios.py +205 -0
- package/python/biocore_model/feedback_flow.py +227 -0
- package/python/biocore_model/feedback_guard.py +129 -0
- package/python/biocore_model/feedback_llm.py +163 -0
- package/python/biocore_model/feedback_memory.py +96 -0
- package/python/biocore_model/feedback_user.py +93 -0
- package/python/biocore_model/frustration_curve.py +110 -0
- package/python/biocore_model/granularity.py +283 -0
- package/python/biocore_model/graph_extract.py +248 -0
- package/python/biocore_model/graph_recall.py +221 -0
- package/python/biocore_model/intent_render.py +91 -0
- package/python/biocore_model/intero_core.py +235 -0
- package/python/biocore_model/intero_style.py +116 -0
- package/python/biocore_model/layered_memory.py +297 -0
- package/python/biocore_model/learning_probe.py +120 -0
- package/python/biocore_model/memory_dual.py +224 -0
- package/python/biocore_model/memory_forgetting.py +125 -0
- package/python/biocore_model/memory_store.py +1399 -0
- package/python/biocore_model/metacog.py +153 -0
- package/python/biocore_model/migrate.py +733 -0
- package/python/biocore_model/migrate_store.py +337 -0
- package/python/biocore_model/mind_wander.py +111 -0
- package/python/biocore_model/mood.py +48 -0
- package/python/biocore_model/narrative.py +172 -0
- package/python/biocore_model/needs_esteem.py +116 -0
- package/python/biocore_model/needs_events.py +137 -0
- package/python/biocore_model/needs_sleep.py +72 -0
- package/python/biocore_model/noise_bank.py +95 -0
- package/python/biocore_model/persona_flow.mjs +25 -0
- package/python/biocore_model/pipeline.py +240 -0
- package/python/biocore_model/pipeline_api.py +200 -0
- package/python/biocore_model/plugin_config.py +83 -0
- package/python/biocore_model/plugin_routes.py +1435 -0
- package/python/biocore_model/policy_learning.py +132 -0
- package/python/biocore_model/possible_selves.py +110 -0
- package/python/biocore_model/predictor.py +178 -0
- package/python/biocore_model/preset.py +205 -0
- package/python/biocore_model/proactive_context.py +153 -0
- package/python/biocore_model/prompt_rules.py +90 -0
- package/python/biocore_model/prospective.py +183 -0
- package/python/biocore_model/provider_fallback.py +97 -0
- package/python/biocore_model/reconstruction.py +132 -0
- package/python/biocore_model/regulation_loop.py +179 -0
- package/python/biocore_model/regulation_strategies.py +294 -0
- package/python/biocore_model/relation_stage.py +130 -0
- package/python/biocore_model/relationship_stage.py +95 -0
- package/python/biocore_model/render.py +901 -0
- package/python/biocore_model/render_guard.mjs +34 -0
- package/python/biocore_model/repair_after_boundary.py +350 -0
- package/python/biocore_model/reply_limit.py +183 -0
- package/python/biocore_model/reply_split.py +203 -0
- package/python/biocore_model/rollback.py +143 -0
- package/python/biocore_model/runtime.py +479 -0
- package/python/biocore_model/sanitize.py +200 -0
- package/python/biocore_model/scheduler.py +123 -0
- package/python/biocore_model/schema.py +120 -0
- package/python/biocore_model/secrecy.py +145 -0
- package/python/biocore_model/self_esteem.py +50 -0
- package/python/biocore_model/self_intent.py +437 -0
- package/python/biocore_model/self_interest.py +263 -0
- package/python/biocore_model/self_verification.py +178 -0
- package/python/biocore_model/services.py +78 -0
- package/python/biocore_model/situation.py +254 -0
- package/python/biocore_model/sleep_circadian.py +116 -0
- package/python/biocore_model/social_pain.py +69 -0
- package/python/biocore_model/social_repair.py +111 -0
- package/python/biocore_model/social_theory.py +131 -0
- package/python/biocore_model/source_monitoring.py +292 -0
- package/python/biocore_model/stages.py +487 -0
- package/python/biocore_model/state_tag.py +231 -0
- package/python/biocore_model/switch_groups.mjs +201 -0
- package/python/biocore_model/tabs_state.mjs +37 -0
- package/python/biocore_model/template_expr.py +252 -0
- package/python/biocore_model/tools/caps_check.mjs +23 -0
- package/python/biocore_model/tools/init_symbols_check.mjs +11 -0
- package/python/biocore_model/tools/missing_ids.mjs +16 -0
- package/python/biocore_model/tools/scope_check.mjs +19 -0
- package/python/biocore_model/topic_control.py +268 -0
- package/python/biocore_model/turn_memory.py +152 -0
- package/python/biocore_model/ui_compat.mjs +48 -0
- package/python/biocore_model/ui_settings.py +266 -0
- package/python/biocore_model/ui_settings.py.bak_authority +108 -0
- package/python/biocore_model/value_order.py +186 -0
- package/python/biocore_model/view_mode.mjs +8 -0
- package/python/biocore_model/voice_bank.py +177 -0
- package/python/biocore_model/waiting_sense.py +229 -0
- package/python/biocore_model/x_directives.py +142 -0
- package/python/biocore_runtime/__init__.py +7 -0
- package/python/biocore_runtime/pipeline.py +1915 -0
- package/python/dsh_adapter/README.md +96 -0
- package/python/dsh_adapter/adapter.py +1077 -0
- package/python/dsh_adapter/cli.py +107 -0
- package/python/dsh_adapter/data/demo_state.json +164 -0
- package/python/dsh_adapter/data/verify_state.json +164 -0
- package/python/dsh_adapter/tests/__init__.py +1 -0
- package/python/dsh_adapter/tests/test_adapter.py +494 -0
- package/ui/panel-layout.js +3 -3
- package/ui/panel.css +53 -6
- package/ui/panel.html +33 -32
- package/ui/panel.js +9 -1
- 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
|