deeptutor-knowledge 1.7.0__py3-none-any.whl
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.
- deeptutor/api/routers/knowledge.py +5225 -0
- deeptutor/api/routers/marginnote4.py +278 -0
- deeptutor/capabilities/ima/__init__.py +6 -0
- deeptutor/capabilities/ima/binding.py +118 -0
- deeptutor/capabilities/ima/capability.py +92 -0
- deeptutor/capabilities/ima/prompts/en/system.md +33 -0
- deeptutor/capabilities/ima/prompts/zh/system.md +24 -0
- deeptutor/capabilities/ima/tools.py +476 -0
- deeptutor/capabilities/marginnote4/__init__.py +13 -0
- deeptutor/capabilities/marginnote4/binding.py +66 -0
- deeptutor/capabilities/marginnote4/capability.py +80 -0
- deeptutor/capabilities/marginnote4/models.py +173 -0
- deeptutor/capabilities/marginnote4/prompts/en/system.md +54 -0
- deeptutor/capabilities/marginnote4/prompts/zh/system.md +44 -0
- deeptutor/capabilities/marginnote4/store.py +562 -0
- deeptutor/capabilities/marginnote4/tools.py +329 -0
- deeptutor/capabilities/obsidian/__init__.py +6 -0
- deeptutor/capabilities/obsidian/binding.py +75 -0
- deeptutor/capabilities/obsidian/capability.py +82 -0
- deeptutor/capabilities/obsidian/prompts/en/system.md +32 -0
- deeptutor/capabilities/obsidian/prompts/zh/system.md +28 -0
- deeptutor/capabilities/obsidian/tools.py +399 -0
- deeptutor/capabilities/obsidian/vault.py +341 -0
- deeptutor/knowledge/__init__.py +28 -0
- deeptutor/knowledge/add_documents.py +734 -0
- deeptutor/knowledge/indexing_run.py +432 -0
- deeptutor/knowledge/initializer.py +448 -0
- deeptutor/knowledge/manager.py +2570 -0
- deeptutor/knowledge/module_service.py +29 -0
- deeptutor/knowledge/naming.py +40 -0
- deeptutor/knowledge/progress_events.py +45 -0
- deeptutor/knowledge/progress_tracker.py +355 -0
- deeptutor/knowledge/staging.py +69 -0
- deeptutor/services/config/knowledge_base_config.py +239 -0
- deeptutor/services/embedding/__init__.py +46 -0
- deeptutor/services/embedding/adapters/__init__.py +40 -0
- deeptutor/services/embedding/adapters/base.py +196 -0
- deeptutor/services/embedding/adapters/cohere.py +176 -0
- deeptutor/services/embedding/adapters/dashscope_native.py +242 -0
- deeptutor/services/embedding/adapters/gemini.py +462 -0
- deeptutor/services/embedding/adapters/jina.py +161 -0
- deeptutor/services/embedding/adapters/ollama.py +145 -0
- deeptutor/services/embedding/adapters/openai_compatible.py +406 -0
- deeptutor/services/embedding/adapters/openai_sdk.py +150 -0
- deeptutor/services/embedding/client.py +323 -0
- deeptutor/services/embedding/config.py +114 -0
- deeptutor/services/embedding/request_options.py +46 -0
- deeptutor/services/embedding/validation.py +128 -0
- deeptutor/services/github_source/__init__.py +5 -0
- deeptutor/services/github_source/client.py +164 -0
- deeptutor/services/github_source/sync.py +384 -0
- deeptutor/services/github_source/sync_service.py +72 -0
- deeptutor/services/rag/__init__.py +21 -0
- deeptutor/services/rag/embedding_binding.py +331 -0
- deeptutor/services/rag/embedding_signature.py +63 -0
- deeptutor/services/rag/eval/__init__.py +92 -0
- deeptutor/services/rag/eval/dataset.py +244 -0
- deeptutor/services/rag/eval/diff.py +312 -0
- deeptutor/services/rag/eval/matching.py +218 -0
- deeptutor/services/rag/eval/metrics.py +213 -0
- deeptutor/services/rag/eval/report.py +111 -0
- deeptutor/services/rag/eval/runner.py +309 -0
- deeptutor/services/rag/factory.py +369 -0
- deeptutor/services/rag/file_routing.py +5 -0
- deeptutor/services/rag/index_probe.py +327 -0
- deeptutor/services/rag/index_versioning.py +387 -0
- deeptutor/services/rag/kb_paths.py +50 -0
- deeptutor/services/rag/linked_kb.py +237 -0
- deeptutor/services/rag/pipelines/__init__.py +7 -0
- deeptutor/services/rag/pipelines/base.py +41 -0
- deeptutor/services/rag/pipelines/graphrag/__init__.py +18 -0
- deeptutor/services/rag/pipelines/graphrag/compatibility.py +42 -0
- deeptutor/services/rag/pipelines/graphrag/completion_adapter.py +251 -0
- deeptutor/services/rag/pipelines/graphrag/config.py +357 -0
- deeptutor/services/rag/pipelines/graphrag/engine.py +479 -0
- deeptutor/services/rag/pipelines/graphrag/errors.py +328 -0
- deeptutor/services/rag/pipelines/graphrag/ingestion.py +135 -0
- deeptutor/services/rag/pipelines/graphrag/pandas_compat.py +68 -0
- deeptutor/services/rag/pipelines/graphrag/pipeline.py +284 -0
- deeptutor/services/rag/pipelines/graphrag/provider.py +134 -0
- deeptutor/services/rag/pipelines/graphrag/storage.py +127 -0
- deeptutor/services/rag/pipelines/ima/__init__.py +28 -0
- deeptutor/services/rag/pipelines/ima/client.py +364 -0
- deeptutor/services/rag/pipelines/ima/config.py +140 -0
- deeptutor/services/rag/pipelines/ima/envelope.py +109 -0
- deeptutor/services/rag/pipelines/ima/inventory.py +209 -0
- deeptutor/services/rag/pipelines/ima/media.py +190 -0
- deeptutor/services/rag/pipelines/ima/models.py +348 -0
- deeptutor/services/rag/pipelines/ima/notes.py +240 -0
- deeptutor/services/rag/pipelines/ima/pipeline.py +238 -0
- deeptutor/services/rag/pipelines/ima/probe.py +105 -0
- deeptutor/services/rag/pipelines/ima/sources.py +105 -0
- deeptutor/services/rag/pipelines/ima/transport.py +130 -0
- deeptutor/services/rag/pipelines/kiwix/__init__.py +1 -0
- deeptutor/services/rag/pipelines/kiwix/client.py +369 -0
- deeptutor/services/rag/pipelines/kiwix/pipeline.py +75 -0
- deeptutor/services/rag/pipelines/lightrag/README.md +78 -0
- deeptutor/services/rag/pipelines/lightrag/__init__.py +15 -0
- deeptutor/services/rag/pipelines/lightrag/block_policy.py +266 -0
- deeptutor/services/rag/pipelines/lightrag/cache_reuse.py +137 -0
- deeptutor/services/rag/pipelines/lightrag/config.py +453 -0
- deeptutor/services/rag/pipelines/lightrag/engine.py +536 -0
- deeptutor/services/rag/pipelines/lightrag/indexing_policy.py +696 -0
- deeptutor/services/rag/pipelines/lightrag/ingress.py +364 -0
- deeptutor/services/rag/pipelines/lightrag/parser.py +99 -0
- deeptutor/services/rag/pipelines/lightrag/pipeline.py +692 -0
- deeptutor/services/rag/pipelines/lightrag/roles.py +132 -0
- deeptutor/services/rag/pipelines/lightrag/sidecar.py +320 -0
- deeptutor/services/rag/pipelines/lightrag/storage.py +338 -0
- deeptutor/services/rag/pipelines/lightrag/worker.py +259 -0
- deeptutor/services/rag/pipelines/lightrag/write_lock.py +55 -0
- deeptutor/services/rag/pipelines/lightrag_server/__init__.py +15 -0
- deeptutor/services/rag/pipelines/lightrag_server/client.py +149 -0
- deeptutor/services/rag/pipelines/lightrag_server/config.py +68 -0
- deeptutor/services/rag/pipelines/lightrag_server/pipeline.py +120 -0
- deeptutor/services/rag/pipelines/lightrag_server/probe.py +110 -0
- deeptutor/services/rag/pipelines/llamaindex/__init__.py +3 -0
- deeptutor/services/rag/pipelines/llamaindex/config.py +199 -0
- deeptutor/services/rag/pipelines/llamaindex/document_loader.py +648 -0
- deeptutor/services/rag/pipelines/llamaindex/embedding_adapter.py +264 -0
- deeptutor/services/rag/pipelines/llamaindex/errors.py +76 -0
- deeptutor/services/rag/pipelines/llamaindex/exercise_lookup.py +373 -0
- deeptutor/services/rag/pipelines/llamaindex/ingestion.py +129 -0
- deeptutor/services/rag/pipelines/llamaindex/pipeline.py +556 -0
- deeptutor/services/rag/pipelines/llamaindex/rerank.py +130 -0
- deeptutor/services/rag/pipelines/llamaindex/retrievers.py +360 -0
- deeptutor/services/rag/pipelines/llamaindex/storage.py +655 -0
- deeptutor/services/rag/pipelines/llamaindex/vector_store.py +288 -0
- deeptutor/services/rag/pipelines/modes.py +48 -0
- deeptutor/services/rag/pipelines/pageindex/__init__.py +47 -0
- deeptutor/services/rag/pipelines/pageindex/client.py +146 -0
- deeptutor/services/rag/pipelines/pageindex/config.py +49 -0
- deeptutor/services/rag/pipelines/pageindex/pipeline.py +313 -0
- deeptutor/services/rag/pipelines/pageindex/reasoning.py +262 -0
- deeptutor/services/rag/pipelines/pageindex/storage.py +133 -0
- deeptutor/services/rag/pipelines/pageindex/tools.py +221 -0
- deeptutor/services/rag/pipelines/weknora/__init__.py +13 -0
- deeptutor/services/rag/pipelines/weknora/client.py +106 -0
- deeptutor/services/rag/pipelines/weknora/config.py +56 -0
- deeptutor/services/rag/pipelines/weknora/pipeline.py +74 -0
- deeptutor/services/rag/pipelines/weknora/probe.py +80 -0
- deeptutor/services/rag/preflight.py +274 -0
- deeptutor/services/rag/provider_binding.py +70 -0
- deeptutor/services/rag/query_rewrite.py +186 -0
- deeptutor/services/rag/service.py +379 -0
- deeptutor/services/rag/smart_retriever.py +81 -0
- deeptutor/services/rag/source_pages.py +292 -0
- deeptutor/services/rag/source_visuals.py +434 -0
- deeptutor/services/rag/visual_assets.py +430 -0
- deeptutor/services/rag/visual_coverage.py +165 -0
- deeptutor/services/web_source/__init__.py +1 -0
- deeptutor/services/web_source/bilingual.py +255 -0
- deeptutor/services/web_source/crawler.py +681 -0
- deeptutor/services/web_source/html_extractor.py +643 -0
- deeptutor/services/web_source/markdown.py +28 -0
- deeptutor/services/web_source/navigation.py +72 -0
- deeptutor/services/web_source/repository.py +640 -0
- deeptutor/services/web_source/robots.py +182 -0
- deeptutor/services/web_source/scheduler.py +336 -0
- deeptutor/services/web_source/snapshot_assets.py +171 -0
- deeptutor/services/web_source/sync.py +253 -0
- deeptutor/services/workspace/kb_move.py +446 -0
- deeptutor/tools/rag_tool.py +110 -0
- deeptutor_cli/kb.py +756 -0
- deeptutor_knowledge-1.7.0.dist-info/METADATA +15 -0
- deeptutor_knowledge-1.7.0.dist-info/RECORD +168 -0
- deeptutor_knowledge-1.7.0.dist-info/WHEEL +5 -0
- deeptutor_knowledge-1.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from deeptutor.services.path_service import get_path_service
|
|
9
|
+
from deeptutor.services.rag.factory import (
|
|
10
|
+
DEFAULT_PROVIDER,
|
|
11
|
+
KNOWN_PROVIDERS,
|
|
12
|
+
has_ready_provider_index,
|
|
13
|
+
normalize_provider_name,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Legacy fallback only — frozen at admin scope at import time. Production code
|
|
19
|
+
# must enter through ``get_kb_config_service()`` (not used directly here, see
|
|
20
|
+
# ``deeptutor/services/config/__init__.py``) which resolves the path lazily.
|
|
21
|
+
DEFAULT_CONFIG_PATH = get_path_service().get_knowledge_bases_root() / "kb_config.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _default_payload() -> dict[str, Any]:
|
|
25
|
+
return {
|
|
26
|
+
"defaults": {
|
|
27
|
+
"default_kb": None,
|
|
28
|
+
"rag_provider": DEFAULT_PROVIDER,
|
|
29
|
+
"search_mode": "hybrid",
|
|
30
|
+
# Per-engine default retrieval mode, set from the engine cards
|
|
31
|
+
# (e.g. {"lightrag": "hybrid", "graphrag": "local"}). A KB's own
|
|
32
|
+
# ``search_mode`` still wins; this is the fallback for that engine.
|
|
33
|
+
"provider_modes": {},
|
|
34
|
+
},
|
|
35
|
+
"knowledge_bases": {},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class KnowledgeBaseConfigService:
|
|
40
|
+
_instances: dict[str, "KnowledgeBaseConfigService"] = {}
|
|
41
|
+
|
|
42
|
+
def __init__(self, config_path: Path | None = None):
|
|
43
|
+
self.config_path = config_path or DEFAULT_CONFIG_PATH
|
|
44
|
+
self._config = self._load_config()
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def get_instance(cls, config_path: Path | None = None) -> "KnowledgeBaseConfigService":
|
|
48
|
+
resolved = (
|
|
49
|
+
config_path or get_path_service().get_knowledge_bases_root() / "kb_config.json"
|
|
50
|
+
).resolve()
|
|
51
|
+
key = str(resolved)
|
|
52
|
+
if key not in cls._instances:
|
|
53
|
+
cls._instances[key] = cls(resolved)
|
|
54
|
+
return cls._instances[key]
|
|
55
|
+
|
|
56
|
+
def _load_config(self) -> dict[str, Any]:
|
|
57
|
+
payload = _default_payload()
|
|
58
|
+
if self.config_path.exists():
|
|
59
|
+
try:
|
|
60
|
+
with open(self.config_path, "r", encoding="utf-8") as handle:
|
|
61
|
+
loaded = json.load(handle) or {}
|
|
62
|
+
payload.update({k: v for k, v in loaded.items() if k != "defaults"})
|
|
63
|
+
payload["defaults"].update(loaded.get("defaults", {}))
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
logger.warning(f"Failed to load KB config: {exc}")
|
|
66
|
+
payload.setdefault("knowledge_bases", {})
|
|
67
|
+
payload.setdefault("defaults", _default_payload()["defaults"])
|
|
68
|
+
payload = self._normalize_payload(payload)
|
|
69
|
+
return payload
|
|
70
|
+
|
|
71
|
+
def _normalize_payload(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
72
|
+
defaults = payload.setdefault("defaults", _default_payload()["defaults"])
|
|
73
|
+
defaults["rag_provider"] = normalize_provider_name(defaults.get("rag_provider"))
|
|
74
|
+
|
|
75
|
+
knowledge_bases = payload.setdefault("knowledge_bases", {})
|
|
76
|
+
kb_base_dir = self.config_path.parent
|
|
77
|
+
for kb_name, config in knowledge_bases.items():
|
|
78
|
+
if not isinstance(config, dict):
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
raw_provider = config.get("rag_provider")
|
|
82
|
+
config["rag_provider"] = normalize_provider_name(raw_provider)
|
|
83
|
+
|
|
84
|
+
# A KB indexed with an engine that no longer exists collapses to the
|
|
85
|
+
# default and must be rebuilt.
|
|
86
|
+
if (
|
|
87
|
+
isinstance(raw_provider, str)
|
|
88
|
+
and raw_provider.strip()
|
|
89
|
+
and raw_provider.strip().lower() not in KNOWN_PROVIDERS
|
|
90
|
+
):
|
|
91
|
+
config["needs_reindex"] = True
|
|
92
|
+
|
|
93
|
+
kb_dir = kb_base_dir / kb_name
|
|
94
|
+
legacy_storage = kb_dir / "rag_storage"
|
|
95
|
+
has_llamaindex_index = has_ready_provider_index(kb_dir, DEFAULT_PROVIDER)
|
|
96
|
+
if (
|
|
97
|
+
config["rag_provider"] == DEFAULT_PROVIDER
|
|
98
|
+
and legacy_storage.exists()
|
|
99
|
+
and legacy_storage.is_dir()
|
|
100
|
+
and not has_llamaindex_index
|
|
101
|
+
):
|
|
102
|
+
config["needs_reindex"] = True
|
|
103
|
+
if config.get("status") == "ready":
|
|
104
|
+
config["status"] = "needs_reindex"
|
|
105
|
+
|
|
106
|
+
return payload
|
|
107
|
+
|
|
108
|
+
def _save(self) -> None:
|
|
109
|
+
from deeptutor.services.file_io import atomic_write_json
|
|
110
|
+
|
|
111
|
+
self._config = self._normalize_payload(self._config)
|
|
112
|
+
atomic_write_json(self.config_path, self._config)
|
|
113
|
+
|
|
114
|
+
def _refresh(self) -> None:
|
|
115
|
+
"""Re-read kb_config.json so this singleton sees changes made by
|
|
116
|
+
``KnowledgeBaseManager`` (orphan pruning, new KB registrations).
|
|
117
|
+
|
|
118
|
+
Without this, every mutating call would rewrite the file from the
|
|
119
|
+
in-memory snapshot taken at process start and undo any external
|
|
120
|
+
cleanup. Read-modify-write keeps the two writers consistent.
|
|
121
|
+
"""
|
|
122
|
+
self._config = self._load_config()
|
|
123
|
+
|
|
124
|
+
def _ensure_kb(self, kb_name: str) -> dict[str, Any]:
|
|
125
|
+
knowledge_bases = self._config.setdefault("knowledge_bases", {})
|
|
126
|
+
if kb_name not in knowledge_bases:
|
|
127
|
+
knowledge_bases[kb_name] = {
|
|
128
|
+
"path": kb_name,
|
|
129
|
+
"description": f"Knowledge base: {kb_name}",
|
|
130
|
+
}
|
|
131
|
+
return knowledge_bases[kb_name]
|
|
132
|
+
|
|
133
|
+
def get_kb_config(self, kb_name: str) -> dict[str, Any]:
|
|
134
|
+
self._refresh()
|
|
135
|
+
defaults = dict(self._config.get("defaults", {}))
|
|
136
|
+
kb_config = dict(self._config.get("knowledge_bases", {}).get(kb_name, {}))
|
|
137
|
+
merged = {
|
|
138
|
+
"default_kb": defaults.get("default_kb"),
|
|
139
|
+
"rag_provider": defaults.get("rag_provider", DEFAULT_PROVIDER),
|
|
140
|
+
"search_mode": kb_config.get("search_mode") or defaults.get("search_mode", "hybrid"),
|
|
141
|
+
"needs_reindex": bool(kb_config.get("needs_reindex", False)),
|
|
142
|
+
**kb_config,
|
|
143
|
+
}
|
|
144
|
+
merged["rag_provider"] = normalize_provider_name(merged.get("rag_provider"))
|
|
145
|
+
return merged
|
|
146
|
+
|
|
147
|
+
def set_kb_config(self, kb_name: str, config: dict[str, Any]) -> None:
|
|
148
|
+
self._refresh()
|
|
149
|
+
entry = self._ensure_kb(kb_name)
|
|
150
|
+
entry.update(config)
|
|
151
|
+
self._save()
|
|
152
|
+
|
|
153
|
+
def get_rag_provider(self, kb_name: str) -> str:
|
|
154
|
+
return normalize_provider_name(self.get_kb_config(kb_name).get("rag_provider"))
|
|
155
|
+
|
|
156
|
+
def set_rag_provider(self, kb_name: str, provider: str) -> None:
|
|
157
|
+
self.set_kb_config(kb_name, {"rag_provider": normalize_provider_name(provider)})
|
|
158
|
+
|
|
159
|
+
def get_search_mode(self, kb_name: str) -> str:
|
|
160
|
+
return str(self.get_kb_config(kb_name).get("search_mode", "hybrid"))
|
|
161
|
+
|
|
162
|
+
def set_search_mode(self, kb_name: str, mode: str) -> None:
|
|
163
|
+
self.set_kb_config(kb_name, {"search_mode": mode})
|
|
164
|
+
|
|
165
|
+
def get_provider_mode(self, provider: str) -> str:
|
|
166
|
+
"""Global default retrieval mode for an engine ("" when unset)."""
|
|
167
|
+
self._refresh()
|
|
168
|
+
modes = self._config.get("defaults", {}).get("provider_modes", {})
|
|
169
|
+
return str(modes.get(provider, "")) if isinstance(modes, dict) else ""
|
|
170
|
+
|
|
171
|
+
def set_provider_mode(self, provider: str, mode: str) -> None:
|
|
172
|
+
self._refresh()
|
|
173
|
+
defaults = self._config.setdefault("defaults", _default_payload()["defaults"])
|
|
174
|
+
modes = defaults.setdefault("provider_modes", {})
|
|
175
|
+
modes[provider] = mode
|
|
176
|
+
self._save()
|
|
177
|
+
|
|
178
|
+
def delete_kb_config(self, kb_name: str) -> None:
|
|
179
|
+
self._refresh()
|
|
180
|
+
knowledge_bases = self._config.get("knowledge_bases", {})
|
|
181
|
+
if kb_name in knowledge_bases:
|
|
182
|
+
del knowledge_bases[kb_name]
|
|
183
|
+
self._save()
|
|
184
|
+
|
|
185
|
+
def get_all_configs(self) -> dict[str, Any]:
|
|
186
|
+
self._refresh()
|
|
187
|
+
return self._config
|
|
188
|
+
|
|
189
|
+
def set_global_defaults(self, defaults: dict[str, Any]) -> None:
|
|
190
|
+
self._refresh()
|
|
191
|
+
current = self._config.setdefault("defaults", _default_payload()["defaults"])
|
|
192
|
+
current.update(defaults)
|
|
193
|
+
self._save()
|
|
194
|
+
|
|
195
|
+
def set_default_kb(self, kb_name: str | None) -> None:
|
|
196
|
+
self._refresh()
|
|
197
|
+
self._config.setdefault("defaults", _default_payload()["defaults"])["default_kb"] = kb_name
|
|
198
|
+
self._save()
|
|
199
|
+
|
|
200
|
+
def get_default_kb(self) -> str | None:
|
|
201
|
+
self._refresh()
|
|
202
|
+
return self._config.get("defaults", {}).get("default_kb")
|
|
203
|
+
|
|
204
|
+
def sync_from_metadata(self, kb_name: str, kb_base_dir: Path) -> None:
|
|
205
|
+
metadata_file = kb_base_dir / kb_name / "metadata.json"
|
|
206
|
+
if not metadata_file.exists():
|
|
207
|
+
return
|
|
208
|
+
try:
|
|
209
|
+
with open(metadata_file, "r", encoding="utf-8") as handle:
|
|
210
|
+
metadata = json.load(handle)
|
|
211
|
+
except Exception as exc:
|
|
212
|
+
logger.warning(f"Failed to load KB metadata for {kb_name}: {exc}")
|
|
213
|
+
return
|
|
214
|
+
config: dict[str, Any] = {}
|
|
215
|
+
if metadata.get("rag_provider"):
|
|
216
|
+
raw_provider = metadata["rag_provider"]
|
|
217
|
+
config["rag_provider"] = normalize_provider_name(raw_provider)
|
|
218
|
+
if str(raw_provider).strip().lower() not in KNOWN_PROVIDERS:
|
|
219
|
+
config["needs_reindex"] = True
|
|
220
|
+
if metadata.get("search_mode"):
|
|
221
|
+
config["search_mode"] = metadata["search_mode"]
|
|
222
|
+
if config:
|
|
223
|
+
self.set_kb_config(kb_name, config)
|
|
224
|
+
|
|
225
|
+
def sync_all_from_metadata(self, kb_base_dir: Path) -> None:
|
|
226
|
+
if not kb_base_dir.exists():
|
|
227
|
+
return
|
|
228
|
+
for kb_dir in kb_base_dir.iterdir():
|
|
229
|
+
if kb_dir.is_dir() and not kb_dir.name.startswith("."):
|
|
230
|
+
self.sync_from_metadata(kb_dir.name, kb_base_dir)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_kb_config_service() -> KnowledgeBaseConfigService:
|
|
234
|
+
return KnowledgeBaseConfigService.get_instance(
|
|
235
|
+
get_path_service().get_knowledge_bases_root() / "kb_config.json"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = ["KnowledgeBaseConfigService", "get_kb_config_service"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Unified embedding client and adapters for all DeepTutor modules.
|
|
2
|
+
|
|
3
|
+
Supported bindings are resolved by ``services.config.provider_runtime`` and
|
|
4
|
+
currently include openai, custom, azure_openai, cohere, jina, ollama, vllm,
|
|
5
|
+
siliconflow, aliyun, openrouter, plus legacy custom_openai_sdk configs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib import import_module
|
|
9
|
+
|
|
10
|
+
from .config import EmbeddingConfig, get_embedding_config
|
|
11
|
+
from .validation import validate_embedding_batch
|
|
12
|
+
|
|
13
|
+
_CLIENT_EXPORTS = {"EmbeddingClient", "get_embedding_client", "reset_embedding_client"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def __getattr__(name: str):
|
|
17
|
+
# Reading model metadata must not initialize every embedding SDK. Background
|
|
18
|
+
# KB catalog scans need configuration even when no embedding call will run.
|
|
19
|
+
if name in _CLIENT_EXPORTS:
|
|
20
|
+
value = getattr(import_module(".client", __name__), name)
|
|
21
|
+
elif name in __all__:
|
|
22
|
+
value = getattr(import_module(".adapters", __name__), name)
|
|
23
|
+
else:
|
|
24
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
25
|
+
globals()[name] = value
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"EmbeddingClient",
|
|
31
|
+
"EmbeddingConfig",
|
|
32
|
+
"get_embedding_client",
|
|
33
|
+
"get_embedding_config",
|
|
34
|
+
"reset_embedding_client",
|
|
35
|
+
"validate_embedding_batch",
|
|
36
|
+
"BaseEmbeddingAdapter",
|
|
37
|
+
"EmbeddingProviderError",
|
|
38
|
+
"EmbeddingRequest",
|
|
39
|
+
"EmbeddingResponse",
|
|
40
|
+
"OpenAICompatibleEmbeddingAdapter",
|
|
41
|
+
"OpenAISDKEmbeddingAdapter",
|
|
42
|
+
"DashScopeMultiModalEmbeddingAdapter",
|
|
43
|
+
"CohereEmbeddingAdapter",
|
|
44
|
+
"JinaEmbeddingAdapter",
|
|
45
|
+
"OllamaEmbeddingAdapter",
|
|
46
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Embedding adapter implementations and backend registry."""
|
|
2
|
+
|
|
3
|
+
from .base import (
|
|
4
|
+
BaseEmbeddingAdapter,
|
|
5
|
+
EmbeddingProviderError,
|
|
6
|
+
EmbeddingRequest,
|
|
7
|
+
EmbeddingResponse,
|
|
8
|
+
)
|
|
9
|
+
from .cohere import CohereEmbeddingAdapter
|
|
10
|
+
from .dashscope_native import DashScopeMultiModalEmbeddingAdapter
|
|
11
|
+
from .gemini import GeminiEmbeddingAdapter
|
|
12
|
+
from .jina import JinaEmbeddingAdapter
|
|
13
|
+
from .ollama import OllamaEmbeddingAdapter
|
|
14
|
+
from .openai_compatible import OpenAICompatibleEmbeddingAdapter
|
|
15
|
+
from .openai_sdk import OpenAISDKEmbeddingAdapter
|
|
16
|
+
|
|
17
|
+
ADAPTER_BACKENDS: dict[str, type[BaseEmbeddingAdapter]] = {
|
|
18
|
+
"openai_compat": OpenAICompatibleEmbeddingAdapter,
|
|
19
|
+
"openai_sdk": OpenAISDKEmbeddingAdapter,
|
|
20
|
+
"cohere": CohereEmbeddingAdapter,
|
|
21
|
+
"jina": JinaEmbeddingAdapter,
|
|
22
|
+
"ollama": OllamaEmbeddingAdapter,
|
|
23
|
+
"dashscope_native": DashScopeMultiModalEmbeddingAdapter,
|
|
24
|
+
"gemini": GeminiEmbeddingAdapter,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"ADAPTER_BACKENDS",
|
|
29
|
+
"BaseEmbeddingAdapter",
|
|
30
|
+
"EmbeddingProviderError",
|
|
31
|
+
"EmbeddingRequest",
|
|
32
|
+
"EmbeddingResponse",
|
|
33
|
+
"OpenAICompatibleEmbeddingAdapter",
|
|
34
|
+
"OpenAISDKEmbeddingAdapter",
|
|
35
|
+
"DashScopeMultiModalEmbeddingAdapter",
|
|
36
|
+
"GeminiEmbeddingAdapter",
|
|
37
|
+
"JinaEmbeddingAdapter",
|
|
38
|
+
"CohereEmbeddingAdapter",
|
|
39
|
+
"OllamaEmbeddingAdapter",
|
|
40
|
+
]
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Embedding Adapter
|
|
3
|
+
=======================
|
|
4
|
+
|
|
5
|
+
Abstract base class for all embedding adapters.
|
|
6
|
+
Defines the contract that all embedding providers must implement.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from deeptutor.services.keypool import KeyPool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def looks_like_multimodal_embedding_model(model_name: Optional[str]) -> bool:
|
|
17
|
+
"""Best-effort guard for OpenAI-compatible multimodal embedding models."""
|
|
18
|
+
if not model_name:
|
|
19
|
+
return False
|
|
20
|
+
normalized = model_name.lower().replace("_", "-")
|
|
21
|
+
return any(
|
|
22
|
+
marker in normalized
|
|
23
|
+
for marker in (
|
|
24
|
+
"qwen3-vl-embedding",
|
|
25
|
+
"multimodal-embedding",
|
|
26
|
+
"vision-embedding",
|
|
27
|
+
"vl-embedding",
|
|
28
|
+
"image-embedding",
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class EmbeddingRequest:
|
|
35
|
+
"""
|
|
36
|
+
Standard embedding request structure.
|
|
37
|
+
|
|
38
|
+
Provider-agnostic request format. Different providers interpret fields differently:
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
texts: List of texts to embed
|
|
42
|
+
model: Model name to use
|
|
43
|
+
dimensions: Embedding vector dimensions (optional)
|
|
44
|
+
input_type: Input type hint for task-aware embeddings (optional)
|
|
45
|
+
- Cohere: Maps to 'input_type' ("search_document", "search_query", "classification", "clustering")
|
|
46
|
+
- Jina: Maps to 'task' ("retrieval.passage", "retrieval.query", etc.)
|
|
47
|
+
- Gemini Embedding 2: Maps to retrieval-specific text instructions
|
|
48
|
+
- OpenAI/Ollama: Ignored
|
|
49
|
+
encoding_format: Output format ("float" or "base64"). ``None`` (the
|
|
50
|
+
default) lets each adapter decide: OpenAI-compatible gateways omit
|
|
51
|
+
the param entirely (several, e.g. SiliconFlow, return HTTP 400 when
|
|
52
|
+
it is present), while the official OpenAI SDK path pins "float".
|
|
53
|
+
truncate: Whether to truncate texts that exceed max tokens (default: True)
|
|
54
|
+
normalized: Whether to return L2-normalized embeddings (Jina/Ollama only)
|
|
55
|
+
late_chunking: Enable late chunking for long context (Jina v3 only)
|
|
56
|
+
contents: Multimodal content list of dicts like
|
|
57
|
+
``[{"text": "..."}, {"image": "url|data: URI"}, {"video": "..."}]``.
|
|
58
|
+
Adapters that support multimodal (DashScope, SiliconFlow Qwen3-VL,
|
|
59
|
+
Cohere v4) consume this directly; text-only adapters MUST raise
|
|
60
|
+
``ValueError`` if it is set so the caller can route differently.
|
|
61
|
+
When ``contents`` is set, ``texts`` is ignored.
|
|
62
|
+
enable_fusion: DashScope-specific. ``True`` fuses all multimodal items
|
|
63
|
+
into one vector; ``False`` (or None) returns one vector per item.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
texts: List[str]
|
|
67
|
+
model: str
|
|
68
|
+
dimensions: Optional[int] = None
|
|
69
|
+
input_type: Optional[str] = None
|
|
70
|
+
encoding_format: Optional[str] = None
|
|
71
|
+
truncate: Optional[bool] = True
|
|
72
|
+
normalized: Optional[bool] = True
|
|
73
|
+
late_chunking: Optional[bool] = False
|
|
74
|
+
contents: Optional[List[Dict[str, Any]]] = None
|
|
75
|
+
enable_fusion: Optional[bool] = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class EmbeddingResponse:
|
|
80
|
+
"""Standard embedding response structure."""
|
|
81
|
+
|
|
82
|
+
embeddings: List[List[float]]
|
|
83
|
+
model: str
|
|
84
|
+
dimensions: int
|
|
85
|
+
usage: Dict[str, Any]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class EmbeddingProviderError(RuntimeError):
|
|
89
|
+
"""Structured error raised by embedding adapters on provider failures.
|
|
90
|
+
|
|
91
|
+
Carries the HTTP status, response body excerpt, model name, and request
|
|
92
|
+
URL so downstream callers (task log streams, UI surfaces) can show
|
|
93
|
+
actionable diagnostics instead of a bare exception string.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
message: str,
|
|
99
|
+
*,
|
|
100
|
+
status: Optional[int] = None,
|
|
101
|
+
body: Optional[str] = None,
|
|
102
|
+
model: Optional[str] = None,
|
|
103
|
+
url: Optional[str] = None,
|
|
104
|
+
provider: Optional[str] = None,
|
|
105
|
+
) -> None:
|
|
106
|
+
super().__init__(message)
|
|
107
|
+
self.status = status
|
|
108
|
+
self.body = body
|
|
109
|
+
self.model = model
|
|
110
|
+
self.url = url
|
|
111
|
+
self.provider = provider
|
|
112
|
+
|
|
113
|
+
def __str__(self) -> str: # noqa: D401 - succinct
|
|
114
|
+
parts = [super().__str__()]
|
|
115
|
+
if self.provider:
|
|
116
|
+
parts.append(f"provider={self.provider}")
|
|
117
|
+
if self.model:
|
|
118
|
+
parts.append(f"model={self.model}")
|
|
119
|
+
if self.status is not None:
|
|
120
|
+
parts.append(f"status={self.status}")
|
|
121
|
+
if self.url:
|
|
122
|
+
parts.append(f"url={self.url}")
|
|
123
|
+
if self.body:
|
|
124
|
+
snippet = self.body if len(self.body) <= 500 else self.body[:500] + "...(truncated)"
|
|
125
|
+
parts.append(f"body={snippet}")
|
|
126
|
+
return " | ".join(parts)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class BaseEmbeddingAdapter(ABC):
|
|
130
|
+
"""
|
|
131
|
+
Base class for all embedding adapters.
|
|
132
|
+
|
|
133
|
+
Each adapter implements the specific API interface for a provider
|
|
134
|
+
(OpenAI, Cohere, Ollama, etc.) while exposing a unified interface.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
# Whether this adapter turns ``EmbeddingRequest.input_type`` into a
|
|
138
|
+
# provider parameter. Opt-in, because doing so changes the vectors a
|
|
139
|
+
# provider returns for the same text: switching it on for a backend that
|
|
140
|
+
# previously sent no role invalidates every index already built with it.
|
|
141
|
+
SUPPORTS_INPUT_TYPE: bool = False
|
|
142
|
+
|
|
143
|
+
def __init__(self, config: Dict[str, Any]):
|
|
144
|
+
"""
|
|
145
|
+
Initialize the adapter with configuration.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
config: Dictionary containing:
|
|
149
|
+
- api_key: API authentication key (optional for local)
|
|
150
|
+
- base_url: API endpoint URL
|
|
151
|
+
- model: Model name to use
|
|
152
|
+
- dimensions: Embedding vector dimensions
|
|
153
|
+
- send_dimensions: Tri-state opt-in for the `dimensions`
|
|
154
|
+
request param. ``True`` always sends, ``False`` never
|
|
155
|
+
sends, ``None`` lets the adapter decide based on the
|
|
156
|
+
model family (default).
|
|
157
|
+
- request_timeout: Request timeout in seconds
|
|
158
|
+
"""
|
|
159
|
+
raw_api_key = config.get("api_key")
|
|
160
|
+
keys = raw_api_key if isinstance(raw_api_key, list) else [raw_api_key]
|
|
161
|
+
keys = [str(key).strip() for key in keys if str(key or "").strip()]
|
|
162
|
+
self._key_pool = KeyPool(keys) if keys else None
|
|
163
|
+
self.api_key = keys[0] if keys else ""
|
|
164
|
+
self.base_url = config.get("base_url")
|
|
165
|
+
self.api_version = config.get("api_version")
|
|
166
|
+
self.model = config.get("model")
|
|
167
|
+
self.dimensions = config.get("dimensions")
|
|
168
|
+
self.send_dimensions: Optional[bool] = config.get("send_dimensions")
|
|
169
|
+
self.request_timeout = config.get("request_timeout", 60)
|
|
170
|
+
self.extra_headers = config.get("extra_headers") or {}
|
|
171
|
+
|
|
172
|
+
@abstractmethod
|
|
173
|
+
async def embed(self, request: EmbeddingRequest) -> EmbeddingResponse:
|
|
174
|
+
"""
|
|
175
|
+
Generate embeddings for a list of texts.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
request: EmbeddingRequest with texts and parameters
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
EmbeddingResponse with embeddings and metadata
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
httpx.HTTPError: If the API request fails
|
|
185
|
+
"""
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
@abstractmethod
|
|
189
|
+
def get_model_info(self) -> Dict[str, Any]:
|
|
190
|
+
"""
|
|
191
|
+
Return information about the configured model.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Dictionary with model metadata (name, dimensions, etc.)
|
|
195
|
+
"""
|
|
196
|
+
pass
|