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,536 @@
1
+ """Exact-version adapter over the LightRAG 1.5 native Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Awaitable, Callable, Iterable, Mapping
7
+ import hashlib
8
+ from importlib.metadata import PackageNotFoundError, version
9
+ import inspect
10
+ import json
11
+ import logging
12
+ from pathlib import Path
13
+ import re
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ if TYPE_CHECKING:
17
+ from deeptutor.services.embedding.config import EmbeddingConfig
18
+
19
+ from .config import (
20
+ DEFAULT_MODE,
21
+ build_embedding_func,
22
+ build_llm_model_func,
23
+ build_vision_model_func,
24
+ constructor_kwargs_from_settings,
25
+ indexing_kwargs_from_settings,
26
+ normalize_mode,
27
+ query_kwargs_from_settings,
28
+ )
29
+ from .ingress import IngressError, StagedDocument, pending_root
30
+ from .worker import OwnerLoopBridge
31
+
32
+ LIGHTRAG_DISTRIBUTION = "lightrag-hku"
33
+ LIGHTRAG_VERSION = "1.5.7"
34
+ PARSER_ENGINE = "deeptutor"
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class LightRagContractError(RuntimeError):
40
+ """Raised when the pinned SDK returns a shape DeepTutor cannot trust."""
41
+
42
+
43
+ def installed_version() -> str:
44
+ try:
45
+ return version(LIGHTRAG_DISTRIBUTION)
46
+ except PackageNotFoundError as exc:
47
+ raise LightRagContractError(
48
+ "LightRAG is not installed. Install `deeptutor[rag-lightrag]`."
49
+ ) from exc
50
+
51
+
52
+ def _require_exact_version() -> None:
53
+ current = installed_version()
54
+ if current != LIGHTRAG_VERSION:
55
+ raise LightRagContractError(
56
+ f"DeepTutor requires {LIGHTRAG_DISTRIBUTION}=={LIGHTRAG_VERSION}; found {current}"
57
+ )
58
+
59
+
60
+ def _validate_component(value: str, *, label: str) -> str:
61
+ path = Path(str(value or ""))
62
+ if path.is_absolute() or ".." in path.parts or path.name != str(value):
63
+ raise IngressError(f"{label} must be a canonical basename: {value!r}")
64
+ if not path.name or path.name in {".", ".."}:
65
+ raise IngressError(f"{label} is empty or invalid")
66
+ return path.name
67
+
68
+
69
+ def _safe_candidate(root: Path, candidate: Path) -> Path | None:
70
+ if candidate.is_symlink() or not candidate.is_file():
71
+ return None
72
+ resolved_root = root.resolve(strict=True)
73
+ resolved = candidate.resolve(strict=True)
74
+ try:
75
+ resolved.relative_to(resolved_root)
76
+ except ValueError as exc:
77
+ raise IngressError(
78
+ f"LightRAG source resolver escaped version ingress: {candidate}"
79
+ ) from exc
80
+ return resolved
81
+
82
+
83
+ def _controlled_class():
84
+ from lightrag import LightRAG
85
+
86
+ class DeepTutorLightRAG(LightRAG):
87
+ def _resolve_source_file_for_parser(
88
+ self,
89
+ file_path: str,
90
+ *,
91
+ source_file: str | None = None,
92
+ parser_engine: str | None = None,
93
+ ) -> str:
94
+ if parser_engine not in (None, PARSER_ENGINE):
95
+ raise IngressError(
96
+ f"Unsupported parser engine for DeepTutor ingress: {parser_engine}"
97
+ )
98
+ name = _validate_component(file_path, label="file_path")
99
+ if source_file is not None:
100
+ source_name = _validate_component(source_file, label="source_file")
101
+ if source_name != name:
102
+ raise IngressError(
103
+ f"LightRAG source hints disagree: {name!r} != {source_name!r}"
104
+ )
105
+ root = pending_root(Path(self.working_dir))
106
+ root.mkdir(parents=True, exist_ok=True)
107
+ matches = [
108
+ resolved
109
+ for candidate in (root / name, root / "__parsed__" / name)
110
+ if (resolved := _safe_candidate(root, candidate)) is not None
111
+ ]
112
+ if len(matches) != 1:
113
+ reason = "missing" if not matches else "ambiguous"
114
+ raise IngressError(f"Version-local LightRAG source is {reason}: {name}")
115
+ return str(matches[0])
116
+
117
+ return DeepTutorLightRAG
118
+
119
+
120
+ _SUFFIXES = frozenset(
121
+ {
122
+ "txt",
123
+ "md",
124
+ "mdx",
125
+ "pdf",
126
+ "doc",
127
+ "docx",
128
+ "ppt",
129
+ "pptx",
130
+ "xls",
131
+ "xlsx",
132
+ "rtf",
133
+ "odt",
134
+ "tex",
135
+ "epub",
136
+ "html",
137
+ "htm",
138
+ "csv",
139
+ "json",
140
+ "xml",
141
+ "yaml",
142
+ "yml",
143
+ "log",
144
+ "conf",
145
+ "ini",
146
+ "properties",
147
+ "sql",
148
+ "sh",
149
+ "c",
150
+ "h",
151
+ "cpp",
152
+ "hpp",
153
+ "py",
154
+ "java",
155
+ "js",
156
+ "ts",
157
+ "swift",
158
+ "go",
159
+ "rb",
160
+ "php",
161
+ "css",
162
+ "scss",
163
+ "less",
164
+ "png",
165
+ "jpg",
166
+ "jpeg",
167
+ "webp",
168
+ "gif",
169
+ "bmp",
170
+ "tiff",
171
+ }
172
+ )
173
+
174
+
175
+ def _register_parser() -> None:
176
+ from lightrag.parser.registry import ParserSpec, register_parser
177
+
178
+ register_parser(
179
+ ParserSpec(
180
+ engine_name=PARSER_ENGINE,
181
+ impl="deeptutor.services.rag.pipelines.lightrag.parser:DeepTutorParser",
182
+ suffixes=_SUFFIXES,
183
+ queue_group="native",
184
+ )
185
+ )
186
+
187
+
188
+ def workspace_for(working_dir: Path) -> str:
189
+ root = Path(working_dir)
190
+ # A published version keeps its native LightRAG workspace name when its
191
+ # containing KB is moved. The name is a hash of the original absolute
192
+ # version path; recomputing it after a move would open an empty store.
193
+ meta_path = root / "meta.json"
194
+ try:
195
+ meta = json.loads(meta_path.read_text(encoding="utf-8"))
196
+ published = str(meta.get("workspace") or "") if isinstance(meta, dict) else ""
197
+ if (
198
+ isinstance(meta, dict)
199
+ and meta.get("provider") == "lightrag"
200
+ and re.fullmatch(r"deeptutor_[0-9a-f]{16}", published)
201
+ ):
202
+ return published
203
+ except FileNotFoundError:
204
+ pass
205
+ except (OSError, ValueError) as exc:
206
+ logger.warning(
207
+ "LightRAG workspace meta at %s exists but cannot be used (%s: %s); "
208
+ "falling back to path-hash workspace name, which may open an empty store",
209
+ meta_path,
210
+ type(exc).__name__,
211
+ exc,
212
+ )
213
+ identity = str(root.resolve()).encode("utf-8")
214
+ return f"deeptutor_{hashlib.sha256(identity).hexdigest()[:16]}"
215
+
216
+
217
+ def build_rag(
218
+ working_dir: Path,
219
+ *,
220
+ io_bridge: OwnerLoopBridge | None = None,
221
+ enable_vlm: bool = False,
222
+ indexing_snapshot: Any | None = None,
223
+ query_roles: dict[str, Any] | None = None,
224
+ embedding_config: EmbeddingConfig | None = None,
225
+ ) -> Any:
226
+ """Construct one exact-version, version-isolated LightRAG instance."""
227
+ _require_exact_version()
228
+ _register_parser()
229
+ from lightrag.llm_roles import RoleLLMConfig
230
+
231
+ llm_adapter_kwargs: dict[str, Any] = {}
232
+ embedding_adapter_kwargs: dict[str, Any] = {}
233
+ if io_bridge is not None:
234
+ llm_adapter_kwargs["io_bridge"] = io_bridge
235
+ embedding_adapter_kwargs["io_bridge"] = io_bridge
236
+ if indexing_snapshot is not None:
237
+ embedding_config = indexing_snapshot.embedding_config
238
+ if embedding_config is not None:
239
+ embedding_adapter_kwargs["embedding_config"] = embedding_config
240
+ from .indexing_policy import cache_identity, cache_identity_for_config
241
+ from .roles import resolve_query_roles
242
+
243
+ role_configs: dict[str, Any] = {}
244
+ if indexing_snapshot is None:
245
+ calls = query_roles if query_roles is not None else resolve_query_roles()
246
+ for role, call in calls.items():
247
+ role_configs[role] = RoleLLMConfig(
248
+ func=build_llm_model_func(
249
+ **llm_adapter_kwargs, llm_config=call.config, owner=call.owner
250
+ ),
251
+ metadata={
252
+ "binding": call.config.binding,
253
+ "model": cache_identity_for_config(call.config),
254
+ },
255
+ max_async=call.max_async,
256
+ timeout=call.timeout,
257
+ )
258
+ base = calls["query"]
259
+ base_config, base_owner = base.config, base.owner
260
+ base_identity = cache_identity_for_config(base_config)
261
+ else:
262
+ base = indexing_snapshot.extract
263
+ base_config, base_owner = base.config, base.owner
264
+ base_identity = cache_identity(base)
265
+ for role, snapshot in (("extract", base), ("vlm", indexing_snapshot.vlm)):
266
+ if snapshot is None or (role == "vlm" and not enable_vlm):
267
+ continue
268
+ builder = build_vision_model_func if role == "vlm" else build_llm_model_func
269
+ role_configs[role] = RoleLLMConfig(
270
+ func=builder(
271
+ **llm_adapter_kwargs, llm_config=snapshot.config, owner=snapshot.owner
272
+ ),
273
+ metadata={"binding": snapshot.config.binding, "model": cache_identity(snapshot)},
274
+ **indexing_snapshot.limits.get(role, {}),
275
+ )
276
+ if enable_vlm and (indexing_snapshot is None or indexing_snapshot.vlm is None):
277
+ raise LightRagContractError("Image analysis requires a pinned VLM; run a full re-index.")
278
+ constructor = {
279
+ "working_dir": str(Path(working_dir)),
280
+ "workspace": workspace_for(working_dir),
281
+ "llm_model_func": build_llm_model_func(
282
+ **llm_adapter_kwargs, llm_config=base_config, owner=base_owner
283
+ ),
284
+ "llm_model_name": base_identity,
285
+ "embedding_func": build_embedding_func(**embedding_adapter_kwargs),
286
+ "auto_manage_storages_states": False,
287
+ "vlm_process_enable": bool(enable_vlm),
288
+ **indexing_kwargs_from_settings(),
289
+ **constructor_kwargs_from_settings(),
290
+ "role_llm_configs": role_configs,
291
+ }
292
+ return _controlled_class()(**constructor)
293
+
294
+
295
+ async def initialize(rag: Any) -> None:
296
+ result = rag.initialize_storages()
297
+ if inspect.isawaitable(result):
298
+ await result
299
+
300
+
301
+ async def enqueue(rag: Any, staged: list[StagedDocument]) -> str:
302
+ if not staged:
303
+ raise ValueError("Cannot enqueue an empty LightRAG batch")
304
+ return await rag.apipeline_enqueue_documents(
305
+ [""] * len(staged),
306
+ file_paths=[item.canonical_name for item in staged],
307
+ docs_format="pending_parse",
308
+ parse_engine=PARSER_ENGINE,
309
+ process_options=[item.process_options for item in staged],
310
+ chunk_options=[item.chunk_options for item in staged],
311
+ )
312
+
313
+
314
+ def _document_id(canonical_name: str) -> str:
315
+ from lightrag.utils import compute_mdhash_id
316
+ from lightrag.utils_pipeline import (
317
+ has_known_document_source,
318
+ normalize_document_file_path,
319
+ )
320
+
321
+ source = normalize_document_file_path(canonical_name)
322
+ if not has_known_document_source(source):
323
+ raise LightRagContractError(
324
+ f"LightRAG cannot derive a stable document ID for {canonical_name!r}"
325
+ )
326
+ return compute_mdhash_id(source, prefix="doc-")
327
+
328
+
329
+ async def confirmed_unaccepted(rag: Any, staged: list[StagedDocument]) -> list[StagedDocument]:
330
+ """Return documents whose missing doc_status row was strictly confirmed."""
331
+ ids_by_name = {item.canonical_name: _document_id(item.canonical_name) for item in staged}
332
+ rows = await rag.doc_status.get_docs_by_ids(
333
+ list(ids_by_name.values()),
334
+ strict=True,
335
+ )
336
+ if not isinstance(rows, dict):
337
+ raise LightRagContractError("doc_status.get_docs_by_ids must return an object")
338
+ return [item for item in staged if ids_by_name[item.canonical_name] not in rows]
339
+
340
+
341
+ def _managed_queue_funcs(rag: Any) -> Iterable[Callable[..., Any]]:
342
+ role_funcs = getattr(rag, "role_llm_funcs", {})
343
+ candidates: list[object] = list(role_funcs.values()) if isinstance(role_funcs, Mapping) else []
344
+ embedding = getattr(rag, "embedding_func", None)
345
+ candidates.append(getattr(embedding, "func", None))
346
+ candidates.append(getattr(rag, "rerank_model_func", None))
347
+ seen: set[int] = set()
348
+ for candidate in candidates:
349
+ if callable(candidate) and id(candidate) not in seen:
350
+ seen.add(id(candidate))
351
+ yield candidate
352
+
353
+
354
+ async def _shutdown_queues(rag: Any, *, cancel_pending: bool) -> None:
355
+ shutdowns: list[Awaitable[Any]] = []
356
+ for func in _managed_queue_funcs(rag):
357
+ shutdown = getattr(func, "shutdown", None)
358
+ if callable(shutdown):
359
+ result = shutdown(graceful=not cancel_pending, timeout=5.0)
360
+ if inspect.isawaitable(result):
361
+ shutdowns.append(result)
362
+ if shutdowns:
363
+ results = await asyncio.gather(*shutdowns, return_exceptions=True)
364
+ failures = [item for item in results if isinstance(item, BaseException)]
365
+ if failures:
366
+ raise RuntimeError(
367
+ f"Failed to shut down {len(failures)} LightRAG queue(s)"
368
+ ) from failures[0]
369
+
370
+
371
+ async def finalize(rag: Any, *, cancel_pending: bool) -> None:
372
+ await _shutdown_queues(rag, cancel_pending=cancel_pending)
373
+ result = rag.finalize_storages()
374
+ if inspect.isawaitable(result):
375
+ await result
376
+
377
+
378
+ def _string(value: Any, *, label: str) -> str:
379
+ if not isinstance(value, str):
380
+ raise LightRagContractError(f"LightRAG query {label} must be a string")
381
+ return value
382
+
383
+
384
+ def _records(data: dict[str, Any], key: str) -> list[dict[str, Any]]:
385
+ value = data.get(key)
386
+ if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
387
+ raise LightRagContractError(f"LightRAG query data.{key} must be an object array")
388
+ return value
389
+
390
+
391
+ def _query_sources(data: dict[str, Any], metadata: dict[str, Any]) -> list[dict[str, Any]]:
392
+ references = _records(data, "references")
393
+ ref_paths = {
394
+ str(item.get("reference_id")): str(item.get("file_path") or item.get("source") or "")
395
+ for item in references
396
+ if item.get("reference_id") is not None
397
+ }
398
+ sources: list[dict[str, Any]] = []
399
+
400
+ def add(kind: str, record: dict[str, Any], rank: int) -> None:
401
+ ref = str(record.get("reference_id") or "")
402
+ path = str(record.get("file_path") or ref_paths.get(ref, ""))
403
+ if kind == "chunk":
404
+ stable_id = str(record.get("chunk_id") or "")
405
+ title = Path(path).name if path else "LightRAG chunk"
406
+ content = _string(record.get("content", ""), label="chunk content")
407
+ elif kind == "entity":
408
+ stable_id = str(record.get("entity_name") or record.get("entity_id") or "")
409
+ title = stable_id or "LightRAG entity"
410
+ content = _string(record.get("description", ""), label="entity description")
411
+ else:
412
+ src_id = str(record.get("src_id") or "")
413
+ tgt_id = str(record.get("tgt_id") or "")
414
+ stable_id = str(record.get("relation_id") or f"{src_id}->{tgt_id}")
415
+ title = stable_id or "LightRAG relationship"
416
+ content = _string(record.get("description", ""), label="relationship description")
417
+ item = {
418
+ "kind": kind,
419
+ "title": title,
420
+ "content": content,
421
+ "source": path,
422
+ "page": str(record.get("page") or ""),
423
+ "reference_id": ref,
424
+ "rank": rank,
425
+ }
426
+ if kind == "chunk":
427
+ item["chunk_id"] = stable_id
428
+ elif kind == "entity":
429
+ item.update(
430
+ {
431
+ "entity_id": stable_id,
432
+ "entity_name": str(record.get("entity_name") or ""),
433
+ "entity_type": str(record.get("entity_type") or ""),
434
+ "source_id": str(record.get("source_id") or ""),
435
+ }
436
+ )
437
+ else:
438
+ item.update(
439
+ {
440
+ "relation_id": stable_id,
441
+ "src_id": str(record.get("src_id") or ""),
442
+ "tgt_id": str(record.get("tgt_id") or ""),
443
+ "keywords": record.get("keywords"),
444
+ "weight": record.get("weight"),
445
+ "source_id": str(record.get("source_id") or ""),
446
+ }
447
+ )
448
+ sources.append(item)
449
+
450
+ for kind, key in (
451
+ ("chunk", "chunks"),
452
+ ("entity", "entities"),
453
+ ("relationship", "relationships"),
454
+ ):
455
+ for record in _records(data, key):
456
+ add(kind, record, len(sources) + 1)
457
+ for reference in references:
458
+ reference_id = str(reference.get("reference_id") or "")
459
+ path = str(reference.get("file_path") or reference.get("source") or "")
460
+ sources.append(
461
+ {
462
+ "kind": "reference",
463
+ "title": Path(path).name if path else reference_id or "LightRAG reference",
464
+ "content": "",
465
+ "source": path,
466
+ "page": str(reference.get("page") or ""),
467
+ "reference_id": reference_id,
468
+ "rank": len(sources) + 1,
469
+ "reference": reference,
470
+ }
471
+ )
472
+ sources.append(
473
+ {
474
+ "kind": "query_metadata",
475
+ "title": "LightRAG query metadata",
476
+ "content": "",
477
+ "source": "",
478
+ "page": "",
479
+ "reference_id": "",
480
+ "rank": len(sources) + 1,
481
+ "metadata": metadata,
482
+ }
483
+ )
484
+ return sources
485
+
486
+
487
+ async def query_with_sources(
488
+ rag: Any, question: str, mode: str | None = None
489
+ ) -> tuple[str, list[dict[str, Any]]]:
490
+ from lightrag import QueryParam
491
+
492
+ param = QueryParam(
493
+ mode=normalize_mode(mode) or DEFAULT_MODE,
494
+ stream=False,
495
+ include_references=True,
496
+ **query_kwargs_from_settings(),
497
+ )
498
+ result = await rag.aquery_llm(question, param=param)
499
+ if not isinstance(result, dict):
500
+ raise LightRagContractError("LightRAG query response must be an object")
501
+ if result.get("status") != "success":
502
+ message = result.get("message")
503
+ raise LightRagContractError(
504
+ str(message).strip()
505
+ if isinstance(message, str) and message.strip()
506
+ else "LightRAG retrieval failed"
507
+ )
508
+ response = result.get("llm_response")
509
+ data = result.get("data")
510
+ metadata = result.get("metadata")
511
+ if (
512
+ not isinstance(response, dict)
513
+ or not isinstance(data, dict)
514
+ or not isinstance(metadata, dict)
515
+ ):
516
+ raise LightRagContractError("LightRAG query response is missing structured fields")
517
+ if response.get("is_streaming") is not False:
518
+ raise LightRagContractError("LightRAG returned streaming output for a non-streaming query")
519
+ content = _string(response.get("content"), label="llm_response.content")
520
+ return content, _query_sources(data, metadata)
521
+
522
+
523
+ __all__ = [
524
+ "LIGHTRAG_DISTRIBUTION",
525
+ "LIGHTRAG_VERSION",
526
+ "LightRagContractError",
527
+ "PARSER_ENGINE",
528
+ "build_rag",
529
+ "confirmed_unaccepted",
530
+ "enqueue",
531
+ "finalize",
532
+ "initialize",
533
+ "installed_version",
534
+ "query_with_sources",
535
+ "workspace_for",
536
+ ]