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.
Files changed (168) hide show
  1. deeptutor/api/routers/knowledge.py +5225 -0
  2. deeptutor/api/routers/marginnote4.py +278 -0
  3. deeptutor/capabilities/ima/__init__.py +6 -0
  4. deeptutor/capabilities/ima/binding.py +118 -0
  5. deeptutor/capabilities/ima/capability.py +92 -0
  6. deeptutor/capabilities/ima/prompts/en/system.md +33 -0
  7. deeptutor/capabilities/ima/prompts/zh/system.md +24 -0
  8. deeptutor/capabilities/ima/tools.py +476 -0
  9. deeptutor/capabilities/marginnote4/__init__.py +13 -0
  10. deeptutor/capabilities/marginnote4/binding.py +66 -0
  11. deeptutor/capabilities/marginnote4/capability.py +80 -0
  12. deeptutor/capabilities/marginnote4/models.py +173 -0
  13. deeptutor/capabilities/marginnote4/prompts/en/system.md +54 -0
  14. deeptutor/capabilities/marginnote4/prompts/zh/system.md +44 -0
  15. deeptutor/capabilities/marginnote4/store.py +562 -0
  16. deeptutor/capabilities/marginnote4/tools.py +329 -0
  17. deeptutor/capabilities/obsidian/__init__.py +6 -0
  18. deeptutor/capabilities/obsidian/binding.py +75 -0
  19. deeptutor/capabilities/obsidian/capability.py +82 -0
  20. deeptutor/capabilities/obsidian/prompts/en/system.md +32 -0
  21. deeptutor/capabilities/obsidian/prompts/zh/system.md +28 -0
  22. deeptutor/capabilities/obsidian/tools.py +399 -0
  23. deeptutor/capabilities/obsidian/vault.py +341 -0
  24. deeptutor/knowledge/__init__.py +28 -0
  25. deeptutor/knowledge/add_documents.py +734 -0
  26. deeptutor/knowledge/indexing_run.py +432 -0
  27. deeptutor/knowledge/initializer.py +448 -0
  28. deeptutor/knowledge/manager.py +2570 -0
  29. deeptutor/knowledge/module_service.py +29 -0
  30. deeptutor/knowledge/naming.py +40 -0
  31. deeptutor/knowledge/progress_events.py +45 -0
  32. deeptutor/knowledge/progress_tracker.py +355 -0
  33. deeptutor/knowledge/staging.py +69 -0
  34. deeptutor/services/config/knowledge_base_config.py +239 -0
  35. deeptutor/services/embedding/__init__.py +46 -0
  36. deeptutor/services/embedding/adapters/__init__.py +40 -0
  37. deeptutor/services/embedding/adapters/base.py +196 -0
  38. deeptutor/services/embedding/adapters/cohere.py +176 -0
  39. deeptutor/services/embedding/adapters/dashscope_native.py +242 -0
  40. deeptutor/services/embedding/adapters/gemini.py +462 -0
  41. deeptutor/services/embedding/adapters/jina.py +161 -0
  42. deeptutor/services/embedding/adapters/ollama.py +145 -0
  43. deeptutor/services/embedding/adapters/openai_compatible.py +406 -0
  44. deeptutor/services/embedding/adapters/openai_sdk.py +150 -0
  45. deeptutor/services/embedding/client.py +323 -0
  46. deeptutor/services/embedding/config.py +114 -0
  47. deeptutor/services/embedding/request_options.py +46 -0
  48. deeptutor/services/embedding/validation.py +128 -0
  49. deeptutor/services/github_source/__init__.py +5 -0
  50. deeptutor/services/github_source/client.py +164 -0
  51. deeptutor/services/github_source/sync.py +384 -0
  52. deeptutor/services/github_source/sync_service.py +72 -0
  53. deeptutor/services/rag/__init__.py +21 -0
  54. deeptutor/services/rag/embedding_binding.py +331 -0
  55. deeptutor/services/rag/embedding_signature.py +63 -0
  56. deeptutor/services/rag/eval/__init__.py +92 -0
  57. deeptutor/services/rag/eval/dataset.py +244 -0
  58. deeptutor/services/rag/eval/diff.py +312 -0
  59. deeptutor/services/rag/eval/matching.py +218 -0
  60. deeptutor/services/rag/eval/metrics.py +213 -0
  61. deeptutor/services/rag/eval/report.py +111 -0
  62. deeptutor/services/rag/eval/runner.py +309 -0
  63. deeptutor/services/rag/factory.py +369 -0
  64. deeptutor/services/rag/file_routing.py +5 -0
  65. deeptutor/services/rag/index_probe.py +327 -0
  66. deeptutor/services/rag/index_versioning.py +387 -0
  67. deeptutor/services/rag/kb_paths.py +50 -0
  68. deeptutor/services/rag/linked_kb.py +237 -0
  69. deeptutor/services/rag/pipelines/__init__.py +7 -0
  70. deeptutor/services/rag/pipelines/base.py +41 -0
  71. deeptutor/services/rag/pipelines/graphrag/__init__.py +18 -0
  72. deeptutor/services/rag/pipelines/graphrag/compatibility.py +42 -0
  73. deeptutor/services/rag/pipelines/graphrag/completion_adapter.py +251 -0
  74. deeptutor/services/rag/pipelines/graphrag/config.py +357 -0
  75. deeptutor/services/rag/pipelines/graphrag/engine.py +479 -0
  76. deeptutor/services/rag/pipelines/graphrag/errors.py +328 -0
  77. deeptutor/services/rag/pipelines/graphrag/ingestion.py +135 -0
  78. deeptutor/services/rag/pipelines/graphrag/pandas_compat.py +68 -0
  79. deeptutor/services/rag/pipelines/graphrag/pipeline.py +284 -0
  80. deeptutor/services/rag/pipelines/graphrag/provider.py +134 -0
  81. deeptutor/services/rag/pipelines/graphrag/storage.py +127 -0
  82. deeptutor/services/rag/pipelines/ima/__init__.py +28 -0
  83. deeptutor/services/rag/pipelines/ima/client.py +364 -0
  84. deeptutor/services/rag/pipelines/ima/config.py +140 -0
  85. deeptutor/services/rag/pipelines/ima/envelope.py +109 -0
  86. deeptutor/services/rag/pipelines/ima/inventory.py +209 -0
  87. deeptutor/services/rag/pipelines/ima/media.py +190 -0
  88. deeptutor/services/rag/pipelines/ima/models.py +348 -0
  89. deeptutor/services/rag/pipelines/ima/notes.py +240 -0
  90. deeptutor/services/rag/pipelines/ima/pipeline.py +238 -0
  91. deeptutor/services/rag/pipelines/ima/probe.py +105 -0
  92. deeptutor/services/rag/pipelines/ima/sources.py +105 -0
  93. deeptutor/services/rag/pipelines/ima/transport.py +130 -0
  94. deeptutor/services/rag/pipelines/kiwix/__init__.py +1 -0
  95. deeptutor/services/rag/pipelines/kiwix/client.py +369 -0
  96. deeptutor/services/rag/pipelines/kiwix/pipeline.py +75 -0
  97. deeptutor/services/rag/pipelines/lightrag/README.md +78 -0
  98. deeptutor/services/rag/pipelines/lightrag/__init__.py +15 -0
  99. deeptutor/services/rag/pipelines/lightrag/block_policy.py +266 -0
  100. deeptutor/services/rag/pipelines/lightrag/cache_reuse.py +137 -0
  101. deeptutor/services/rag/pipelines/lightrag/config.py +453 -0
  102. deeptutor/services/rag/pipelines/lightrag/engine.py +536 -0
  103. deeptutor/services/rag/pipelines/lightrag/indexing_policy.py +696 -0
  104. deeptutor/services/rag/pipelines/lightrag/ingress.py +364 -0
  105. deeptutor/services/rag/pipelines/lightrag/parser.py +99 -0
  106. deeptutor/services/rag/pipelines/lightrag/pipeline.py +692 -0
  107. deeptutor/services/rag/pipelines/lightrag/roles.py +132 -0
  108. deeptutor/services/rag/pipelines/lightrag/sidecar.py +320 -0
  109. deeptutor/services/rag/pipelines/lightrag/storage.py +338 -0
  110. deeptutor/services/rag/pipelines/lightrag/worker.py +259 -0
  111. deeptutor/services/rag/pipelines/lightrag/write_lock.py +55 -0
  112. deeptutor/services/rag/pipelines/lightrag_server/__init__.py +15 -0
  113. deeptutor/services/rag/pipelines/lightrag_server/client.py +149 -0
  114. deeptutor/services/rag/pipelines/lightrag_server/config.py +68 -0
  115. deeptutor/services/rag/pipelines/lightrag_server/pipeline.py +120 -0
  116. deeptutor/services/rag/pipelines/lightrag_server/probe.py +110 -0
  117. deeptutor/services/rag/pipelines/llamaindex/__init__.py +3 -0
  118. deeptutor/services/rag/pipelines/llamaindex/config.py +199 -0
  119. deeptutor/services/rag/pipelines/llamaindex/document_loader.py +648 -0
  120. deeptutor/services/rag/pipelines/llamaindex/embedding_adapter.py +264 -0
  121. deeptutor/services/rag/pipelines/llamaindex/errors.py +76 -0
  122. deeptutor/services/rag/pipelines/llamaindex/exercise_lookup.py +373 -0
  123. deeptutor/services/rag/pipelines/llamaindex/ingestion.py +129 -0
  124. deeptutor/services/rag/pipelines/llamaindex/pipeline.py +556 -0
  125. deeptutor/services/rag/pipelines/llamaindex/rerank.py +130 -0
  126. deeptutor/services/rag/pipelines/llamaindex/retrievers.py +360 -0
  127. deeptutor/services/rag/pipelines/llamaindex/storage.py +655 -0
  128. deeptutor/services/rag/pipelines/llamaindex/vector_store.py +288 -0
  129. deeptutor/services/rag/pipelines/modes.py +48 -0
  130. deeptutor/services/rag/pipelines/pageindex/__init__.py +47 -0
  131. deeptutor/services/rag/pipelines/pageindex/client.py +146 -0
  132. deeptutor/services/rag/pipelines/pageindex/config.py +49 -0
  133. deeptutor/services/rag/pipelines/pageindex/pipeline.py +313 -0
  134. deeptutor/services/rag/pipelines/pageindex/reasoning.py +262 -0
  135. deeptutor/services/rag/pipelines/pageindex/storage.py +133 -0
  136. deeptutor/services/rag/pipelines/pageindex/tools.py +221 -0
  137. deeptutor/services/rag/pipelines/weknora/__init__.py +13 -0
  138. deeptutor/services/rag/pipelines/weknora/client.py +106 -0
  139. deeptutor/services/rag/pipelines/weknora/config.py +56 -0
  140. deeptutor/services/rag/pipelines/weknora/pipeline.py +74 -0
  141. deeptutor/services/rag/pipelines/weknora/probe.py +80 -0
  142. deeptutor/services/rag/preflight.py +274 -0
  143. deeptutor/services/rag/provider_binding.py +70 -0
  144. deeptutor/services/rag/query_rewrite.py +186 -0
  145. deeptutor/services/rag/service.py +379 -0
  146. deeptutor/services/rag/smart_retriever.py +81 -0
  147. deeptutor/services/rag/source_pages.py +292 -0
  148. deeptutor/services/rag/source_visuals.py +434 -0
  149. deeptutor/services/rag/visual_assets.py +430 -0
  150. deeptutor/services/rag/visual_coverage.py +165 -0
  151. deeptutor/services/web_source/__init__.py +1 -0
  152. deeptutor/services/web_source/bilingual.py +255 -0
  153. deeptutor/services/web_source/crawler.py +681 -0
  154. deeptutor/services/web_source/html_extractor.py +643 -0
  155. deeptutor/services/web_source/markdown.py +28 -0
  156. deeptutor/services/web_source/navigation.py +72 -0
  157. deeptutor/services/web_source/repository.py +640 -0
  158. deeptutor/services/web_source/robots.py +182 -0
  159. deeptutor/services/web_source/scheduler.py +336 -0
  160. deeptutor/services/web_source/snapshot_assets.py +171 -0
  161. deeptutor/services/web_source/sync.py +253 -0
  162. deeptutor/services/workspace/kb_move.py +446 -0
  163. deeptutor/tools/rag_tool.py +110 -0
  164. deeptutor_cli/kb.py +756 -0
  165. deeptutor_knowledge-1.7.0.dist-info/METADATA +15 -0
  166. deeptutor_knowledge-1.7.0.dist-info/RECORD +168 -0
  167. deeptutor_knowledge-1.7.0.dist-info/WHEEL +5 -0
  168. 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