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,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
|
+
]
|