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,453 @@
1
+ """Bridge DeepTutor runtime configuration into the LightRAG 1.5 native SDK.
2
+
3
+ This module is the decoupling seam: it exposes availability + mode helpers and
4
+ builds the three adapters LightRAG needs from DeepTutor's already-resolved LLM,
5
+ vision, and embedding clients. LightRAG imports remain lazy so every other RAG
6
+ provider can import without the optional extra installed.
7
+
8
+ Decoupling notes:
9
+ * ``llm_model_func`` / ``vision_model_func`` wrap DeepTutor's unified model
10
+ callables and DROP LightRAG's internal kwargs (``hashing_kv``,
11
+ ``keyword_extraction``, …) so they never leak into ``factory.complete``.
12
+ * ``embedding_func`` reuses DeepTutor's embedding client, wrapped in LightRAG's
13
+ ``EmbeddingFunc`` with the active model's dimension.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ from collections.abc import Awaitable, Callable
20
+ from copy import deepcopy
21
+ import importlib.util
22
+ import inspect
23
+ import logging
24
+ import re
25
+ from typing import TYPE_CHECKING, Any, TypeVar
26
+
27
+ if TYPE_CHECKING:
28
+ from lightrag.utils import EmbeddingFunc
29
+
30
+ from deeptutor.multi_user.models import CurrentUser
31
+ from deeptutor.services.embedding.config import EmbeddingConfig
32
+ from deeptutor.services.llm.config import LLMConfig
33
+
34
+ from .worker import OwnerLoopBridge
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ _T = TypeVar("_T")
39
+
40
+ # LightRAG's native retrieval modes. ``hybrid`` (KG + vector) is the safest
41
+ # general default and matches the shared per-KB ``search_mode`` default.
42
+ SUPPORTED_MODES = ("naive", "local", "global", "hybrid", "mix")
43
+ DEFAULT_MODE = "hybrid"
44
+
45
+ # Conservative cap for the embedding wrapper when the model doesn't advertise one.
46
+ _DEFAULT_MAX_TOKEN_SIZE = 8192
47
+
48
+ # Keep retries at the LightRAG adapter boundary so the SDK receives one
49
+ # predictable policy for both text and vision calls. Provider retries are disabled
50
+ # on every attempt to prevent the two retry layers from multiplying.
51
+ _ADAPTER_MAX_ATTEMPTS = 3
52
+ _ADAPTER_RETRY_DELAYS_SECONDS = (1.0, 2.0)
53
+ _ADAPTER_MAX_RETRY_DELAY_SECONDS = 60.0
54
+ _RETRYABLE_HTTP_STATUS_CODES = frozenset({408, 429, 500, 502, 503, 504, 529})
55
+ _HTTP_STATUS_PATTERN = re.compile(
56
+ r"\b(?:http(?: status)?|status(?: code)?|error code)\s*[:=-]?\s*(\d{3})\b",
57
+ re.I,
58
+ )
59
+
60
+
61
+ class LightRagNotAvailableError(RuntimeError):
62
+ """Raised when the optional ``lightrag-hku`` dependency is not installed."""
63
+
64
+
65
+ class LightRagNotConfiguredError(RuntimeError):
66
+ """Raised when DeepTutor's LLM / embedding config can't back LightRAG."""
67
+
68
+
69
+ def _http_status_code(exc: Exception) -> int | None:
70
+ """Return a structured or safely normalized HTTP status for an LLM error."""
71
+ status_code = getattr(exc, "status_code", None)
72
+ if isinstance(status_code, int) and not isinstance(status_code, bool):
73
+ return status_code
74
+
75
+ response = getattr(exc, "response", None)
76
+ response_status = getattr(response, "status_code", None)
77
+ if isinstance(response_status, int) and not isinstance(response_status, bool):
78
+ return response_status
79
+
80
+ # The Codex provider currently returns a safe, normalized ``HTTP NNN``
81
+ # message through LLMAPIError instead of preserving the status attribute.
82
+ is_llm_api_error = any(cls.__name__ == "LLMAPIError" for cls in type(exc).__mro__)
83
+ message = getattr(exc, "message", None)
84
+ if is_llm_api_error and isinstance(message, str):
85
+ match = _HTTP_STATUS_PATTERN.search(message)
86
+ if match is not None:
87
+ return int(match.group(1))
88
+ return None
89
+
90
+
91
+ def _retry_classification(exc: Exception) -> tuple[bool, str]:
92
+ """Classify retryability without inspecting or logging provider payloads."""
93
+ from deeptutor.services.llm.request_compat import is_transient_transport_error
94
+
95
+ status_code = _http_status_code(exc)
96
+ if status_code is not None:
97
+ return status_code in _RETRYABLE_HTTP_STATUS_CODES, f"http_{status_code}"
98
+ if is_transient_transport_error(exc):
99
+ return True, "transport"
100
+ return False, "non_retryable"
101
+
102
+
103
+ def _retry_delay_seconds(exc: Exception, scheduled_delay: float) -> float:
104
+ """Honor a safe Retry-After value without allowing unbounded sleeps."""
105
+ from deeptutor.services.llm.error_mapping import retry_after_seconds
106
+
107
+ requested_delay = retry_after_seconds(exc)
108
+ if requested_delay is None:
109
+ return scheduled_delay
110
+ return min(requested_delay, _ADAPTER_MAX_RETRY_DELAY_SECONDS)
111
+
112
+
113
+ async def _run_adapter_with_retry(
114
+ request: Callable[[], Awaitable[_T]],
115
+ *,
116
+ io_bridge: OwnerLoopBridge | None,
117
+ ) -> _T:
118
+ """Run one adapter request with bounded, non-multiplying retries."""
119
+ for attempt in range(1, _ADAPTER_MAX_ATTEMPTS + 1):
120
+ try:
121
+ if io_bridge is not None:
122
+ return await io_bridge.run(request)
123
+ return await request()
124
+ except Exception as exc:
125
+ should_retry, status = _retry_classification(exc)
126
+ if not should_retry or attempt == _ADAPTER_MAX_ATTEMPTS:
127
+ raise
128
+ logger.warning(
129
+ "LightRAG adapter retry attempt=%d exception=%s status=%s",
130
+ attempt,
131
+ type(exc).__name__,
132
+ status,
133
+ )
134
+ delay = _retry_delay_seconds(exc, _ADAPTER_RETRY_DELAYS_SECONDS[attempt - 1])
135
+ await asyncio.sleep(delay)
136
+
137
+ raise AssertionError("unreachable")
138
+
139
+
140
+ def is_lightrag_available() -> bool:
141
+ """True when the native LightRAG SDK can be imported.
142
+
143
+ Opt-in extra: ``pip install 'deeptutor[rag-lightrag]'``. Until installed the
144
+ provider is hidden / blocked in the UI.
145
+ """
146
+ return importlib.util.find_spec("lightrag") is not None
147
+
148
+
149
+ def normalize_mode(mode: str | None) -> str:
150
+ """Coerce a stored ``search_mode`` to a valid LightRAG query mode.
151
+
152
+ The per-KB ``search_mode`` field is shared across engines; anything that
153
+ isn't a LightRAG mode falls back to :data:`DEFAULT_MODE`.
154
+ """
155
+ candidate = (mode or "").strip().lower()
156
+ return candidate if candidate in SUPPORTED_MODES else DEFAULT_MODE
157
+
158
+
159
+ def query_kwargs_from_settings() -> dict:
160
+ """``QueryParam`` values from runtime settings."""
161
+ try:
162
+ from deeptutor.services.config import load_lightrag_settings
163
+
164
+ settings = load_lightrag_settings()
165
+ return {
166
+ "top_k": int(settings.get("top_k", 60)),
167
+ "response_type": str(settings.get("response_type") or "Multiple Paragraphs"),
168
+ }
169
+ except Exception:
170
+ return {}
171
+
172
+
173
+ def indexing_kwargs_from_settings() -> dict:
174
+ """Native parser-pool knobs from runtime settings."""
175
+ try:
176
+ from deeptutor.services.config import load_lightrag_settings
177
+
178
+ settings = load_lightrag_settings()
179
+ return {"max_parallel_parse_native": int(settings.get("max_concurrent_files", 1))}
180
+ except Exception:
181
+ return {}
182
+
183
+
184
+ def constructor_kwargs_from_settings() -> dict:
185
+ """Direct LightRAG constructor knobs from runtime settings."""
186
+ try:
187
+ from deeptutor.services.config import load_lightrag_settings
188
+
189
+ settings = load_lightrag_settings()
190
+ return {
191
+ "llm_model_max_async": int(settings.get("llm_model_max_async", 4)),
192
+ "entity_extract_max_gleaning": int(settings.get("entity_extract_max_gleaning", 1)),
193
+ "default_llm_timeout": int(settings.get("llm_timeout", 240)),
194
+ }
195
+ except Exception:
196
+ return {}
197
+
198
+
199
+ def _lightrag_llm_selection_from_settings(*, strict: bool) -> dict[str, str] | None:
200
+ try:
201
+ from deeptutor.services.config import load_lightrag_settings
202
+
203
+ settings = load_lightrag_settings()
204
+ profile_id = str(settings.get("llm_profile_id") or "").strip()
205
+ model_id = str(settings.get("llm_model_id") or "").strip()
206
+ if not profile_id and not model_id:
207
+ return None
208
+ if not profile_id or not model_id:
209
+ if strict:
210
+ raise ValueError("The LightRAG LLM selection is incomplete.")
211
+ logger.warning("Ignoring incomplete LightRAG LLM selection; using the active model")
212
+ return None
213
+ return {"profile_id": profile_id, "model_id": model_id}
214
+ except Exception:
215
+ if strict:
216
+ raise
217
+ logger.warning(
218
+ "Could not read LightRAG LLM selection; using the active model",
219
+ exc_info=True,
220
+ )
221
+ return None
222
+
223
+
224
+ def lightrag_llm_selection_from_settings() -> dict[str, str] | None:
225
+ """Return the released query-model selection with its fallback semantics."""
226
+ return _lightrag_llm_selection_from_settings(strict=False)
227
+
228
+
229
+ def lightrag_indexing_selection_from_settings() -> dict[str, str] | None:
230
+ """Return the indexing default, rejecting unreadable or partial settings."""
231
+ return _lightrag_llm_selection_from_settings(strict=True)
232
+
233
+
234
+ def resolve_lightrag_query_llm_config():
235
+ """Resolve legacy settings, access-checking the active fallback as well."""
236
+ from deeptutor.multi_user.model_access import apply_allowed_llm_selection
237
+ from deeptutor.services.model_selection.runtime import resolve_llm_config_for_selection
238
+
239
+ from .indexing_policy import _active_catalog_selection
240
+
241
+ selection = lightrag_llm_selection_from_settings() or _active_catalog_selection()
242
+ if selection is None:
243
+ raise LightRagNotConfiguredError("Choose an accessible LightRAG model in engine settings.")
244
+ apply_allowed_llm_selection(selection)
245
+ try:
246
+ return resolve_llm_config_for_selection(selection)
247
+ except ValueError:
248
+ fallback = _active_catalog_selection()
249
+ if fallback is None or fallback == selection:
250
+ raise
251
+ apply_allowed_llm_selection(fallback)
252
+ return resolve_llm_config_for_selection(fallback)
253
+
254
+
255
+ def build_llm_model_func(
256
+ *,
257
+ io_bridge: OwnerLoopBridge | None = None,
258
+ llm_config: LLMConfig | None = None,
259
+ owner: CurrentUser | None = None,
260
+ ):
261
+ """Wrap DeepTutor's unified LLM callable for LightRAG.
262
+
263
+ Preserve provider-facing structured-output requests while keeping
264
+ LightRAG's orchestration-only kwargs out of DeepTutor's provider layer.
265
+
266
+ ``enable_cot`` is intentionally an output-formatting switch in LightRAG,
267
+ not a request to enable provider reasoning. DeepTutor controls reasoning
268
+ through the frozen role configuration and returns answer content without
269
+ exposing provider reasoning text.
270
+ """
271
+ if llm_config is None:
272
+ from deeptutor.services.llm import get_llm_client
273
+
274
+ base = get_llm_client().get_model_func()
275
+ else:
276
+ from deeptutor.services.llm.client import build_model_func_for_config
277
+
278
+ base = build_model_func_for_config(llm_config, allow_multimodal=False)
279
+
280
+ async def llm_model_func(
281
+ prompt="",
282
+ system_prompt=None,
283
+ history_messages=None,
284
+ messages=None,
285
+ response_format=None,
286
+ enable_cot=False,
287
+ **_ignored,
288
+ ):
289
+ del enable_cot
290
+
291
+ async def request() -> Any:
292
+ async def complete() -> Any:
293
+ provider_kwargs = {}
294
+ if response_format is not None:
295
+ provider_kwargs["response_format"] = response_format
296
+ return await base(
297
+ prompt or "",
298
+ system_prompt=system_prompt,
299
+ history_messages=history_messages or [],
300
+ messages=messages,
301
+ max_retries=0,
302
+ allow_image_fallback=False,
303
+ **provider_kwargs,
304
+ )
305
+
306
+ if owner is None:
307
+ return await complete()
308
+ from deeptutor.multi_user.paths import user_context
309
+
310
+ with user_context(owner):
311
+ return await complete()
312
+
313
+ return await _run_adapter_with_retry(request, io_bridge=io_bridge)
314
+
315
+ return llm_model_func
316
+
317
+
318
+ def build_vision_model_func(
319
+ *,
320
+ io_bridge: OwnerLoopBridge | None = None,
321
+ llm_config: LLMConfig | None = None,
322
+ owner: CurrentUser | None = None,
323
+ ):
324
+ """Map rc2 ``image_inputs`` to DeepTutor's vision callable."""
325
+ if llm_config is None:
326
+ from deeptutor.services.llm import get_llm_client
327
+
328
+ base = get_llm_client().get_vision_model_func()
329
+ else:
330
+ from deeptutor.services.llm.client import build_model_func_for_config
331
+
332
+ base = build_model_func_for_config(llm_config, allow_multimodal=True)
333
+
334
+ async def vision_model_func(
335
+ prompt="",
336
+ system_prompt=None,
337
+ history_messages=None,
338
+ image_inputs=None,
339
+ messages=None,
340
+ response_format=None,
341
+ **_ignored,
342
+ ):
343
+ if not isinstance(image_inputs, list) or len(image_inputs) != 1:
344
+ raise ValueError("LightRAG vision requests must contain exactly one image input")
345
+ payload = image_inputs[0]
346
+ if not isinstance(payload, dict):
347
+ raise ValueError("LightRAG vision image input must be an object")
348
+ image_data = payload.get("base64")
349
+ if not isinstance(image_data, str) or not image_data.strip():
350
+ raise ValueError("LightRAG vision image input requires a non-empty base64 value")
351
+
352
+ async def request() -> Any:
353
+ async def complete() -> Any:
354
+ provider_kwargs = {}
355
+ if response_format is not None:
356
+ provider_kwargs["response_format"] = response_format
357
+ return await base(
358
+ prompt or "",
359
+ system_prompt=system_prompt,
360
+ history_messages=history_messages or [],
361
+ image_data=image_data,
362
+ messages=messages,
363
+ max_retries=0,
364
+ allow_image_fallback=False,
365
+ **provider_kwargs,
366
+ )
367
+
368
+ if owner is None:
369
+ return await complete()
370
+ from deeptutor.multi_user.paths import user_context
371
+
372
+ with user_context(owner):
373
+ return await complete()
374
+
375
+ return await _run_adapter_with_retry(request, io_bridge=io_bridge)
376
+
377
+ return vision_model_func
378
+
379
+
380
+ def vision_model_available() -> bool:
381
+ """Return whether the active DeepTutor model is explicitly vision-capable."""
382
+ try:
383
+ from deeptutor.services.llm import get_llm_client
384
+
385
+ return bool(get_llm_client().supports_multimodal_images())
386
+ except Exception:
387
+ return False
388
+
389
+
390
+ def build_embedding_func(
391
+ *,
392
+ io_bridge: OwnerLoopBridge | None = None,
393
+ embedding_config: EmbeddingConfig | None = None,
394
+ ) -> EmbeddingFunc:
395
+ """Wrap one captured embedding configuration in LightRAG's adapter."""
396
+ from lightrag.utils import EmbeddingFunc
397
+
398
+ from deeptutor.services.embedding import get_embedding_config
399
+ from deeptutor.services.embedding.client import EmbeddingClient
400
+
401
+ cfg = deepcopy(embedding_config if embedding_config is not None else get_embedding_config())
402
+ dim = int(getattr(cfg, "dim", 0) or 0)
403
+ if not dim:
404
+ raise LightRagNotConfiguredError(
405
+ "No active embedding model with a known dimension. Configure one under "
406
+ "Settings → Catalog before using a LightRAG knowledge base."
407
+ )
408
+
409
+ client = EmbeddingClient(config=cfg)
410
+
411
+ async def embedding_func(texts: list[str], context: str | None = None, **_ignored: Any) -> Any:
412
+ import numpy as np
413
+
414
+ # No context means no role. Defaulting to "document" would label
415
+ # queries as passages.
416
+ input_type = {
417
+ "query": "search_query",
418
+ "document": "search_document",
419
+ }.get(str(context or "").strip().lower())
420
+
421
+ async def request() -> Any:
422
+ return await client.embed(texts, input_type=input_type)
423
+
424
+ vectors = await io_bridge.run(request) if io_bridge is not None else await request()
425
+ return np.asarray(vectors, dtype=np.float32)
426
+
427
+ embedding_kwargs = {
428
+ "embedding_dim": dim,
429
+ "max_token_size": int(getattr(cfg, "max_tokens", 0) or _DEFAULT_MAX_TOKEN_SIZE),
430
+ "func": embedding_func,
431
+ }
432
+ if "supports_asymmetric" in inspect.signature(EmbeddingFunc).parameters:
433
+ embedding_kwargs["supports_asymmetric"] = True
434
+ return EmbeddingFunc(**embedding_kwargs)
435
+
436
+
437
+ __all__ = [
438
+ "SUPPORTED_MODES",
439
+ "DEFAULT_MODE",
440
+ "LightRagNotAvailableError",
441
+ "LightRagNotConfiguredError",
442
+ "is_lightrag_available",
443
+ "normalize_mode",
444
+ "query_kwargs_from_settings",
445
+ "indexing_kwargs_from_settings",
446
+ "constructor_kwargs_from_settings",
447
+ "lightrag_llm_selection_from_settings",
448
+ "resolve_lightrag_query_llm_config",
449
+ "build_llm_model_func",
450
+ "build_vision_model_func",
451
+ "vision_model_available",
452
+ "build_embedding_func",
453
+ ]