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,284 @@
|
|
|
1
|
+
"""GraphRAG-backed RAG pipeline orchestration.
|
|
2
|
+
|
|
3
|
+
Implements the same contract as :class:`LlamaIndexPipeline` (see
|
|
4
|
+
``..base.RAGPipeline``) but delegates indexing and retrieval to a local
|
|
5
|
+
microsoft/graphrag project. Each KB owns a self-contained GraphRAG project under
|
|
6
|
+
its ``version-N`` directory (see ``storage``); documents are parsed to text by
|
|
7
|
+
DeepTutor first (see ``ingestion``) so GraphRAG only ever sees ``.txt`` input.
|
|
8
|
+
|
|
9
|
+
GraphRAG is an optional dependency: every method fails with a clear, actionable
|
|
10
|
+
message when it is not installed instead of an opaque ``ImportError``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import shutil
|
|
19
|
+
import tempfile
|
|
20
|
+
import traceback
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from deeptutor.logging import PROCESS_LOG_PRIVATE_ATTR
|
|
24
|
+
from deeptutor.runtime.home import get_runtime_data_root
|
|
25
|
+
from deeptutor.services.rag.index_versioning import (
|
|
26
|
+
resolve_storage_dir_for_read,
|
|
27
|
+
resolve_storage_dir_for_rebuild,
|
|
28
|
+
)
|
|
29
|
+
from deeptutor.services.rag.kb_paths import resolve_kb_dir
|
|
30
|
+
|
|
31
|
+
from . import config as gr_config
|
|
32
|
+
from . import ingestion, storage
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
DEFAULT_KB_BASE_DIR = str(get_runtime_data_root() / "knowledge_bases")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GraphRagPipeline:
|
|
40
|
+
"""Index/retrieve KB content via a local microsoft/graphrag project."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, kb_base_dir: Optional[str] = None, **_: Any) -> None:
|
|
43
|
+
self.logger = logging.getLogger(__name__)
|
|
44
|
+
self.kb_base_dir = kb_base_dir or DEFAULT_KB_BASE_DIR
|
|
45
|
+
|
|
46
|
+
# ----- helpers --------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def _ensure_available(self) -> None:
|
|
49
|
+
if not gr_config.is_graphrag_available():
|
|
50
|
+
raise gr_config.GraphRagNotAvailableError(
|
|
51
|
+
"GraphRAG is not installed. Install it with "
|
|
52
|
+
"`pip install 'deeptutor[graphrag]'` to use GraphRAG knowledge bases."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def _resolve_mode(self, kb_name: str, kwargs: dict[str, Any]) -> str:
|
|
56
|
+
from ..modes import resolve_kb_mode
|
|
57
|
+
|
|
58
|
+
return resolve_kb_mode(
|
|
59
|
+
self.kb_base_dir,
|
|
60
|
+
kb_name,
|
|
61
|
+
storage.PROVIDER,
|
|
62
|
+
explicit=kwargs.get("mode"),
|
|
63
|
+
supported=gr_config.SUPPORTED_MODES,
|
|
64
|
+
default=gr_config.DEFAULT_MODE,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def _cleanup_failed_version_dir(self, root_dir: Path) -> None:
|
|
68
|
+
try:
|
|
69
|
+
if root_dir.is_dir() and not (root_dir / storage.META_FILENAME).exists():
|
|
70
|
+
shutil.rmtree(root_dir)
|
|
71
|
+
except Exception as exc: # pragma: no cover - best-effort
|
|
72
|
+
self.logger.warning("Could not clean up failed version dir %s: %s", root_dir, exc)
|
|
73
|
+
|
|
74
|
+
async def _preflight_settings(self, settings: dict[str, Any]) -> None:
|
|
75
|
+
"""Probe a settings snapshot without touching an existing KB version."""
|
|
76
|
+
from . import engine
|
|
77
|
+
|
|
78
|
+
with tempfile.TemporaryDirectory(prefix="deeptutor-graphrag-preflight-") as temp_dir:
|
|
79
|
+
probe_root = Path(temp_dir)
|
|
80
|
+
gr_config.write_settings_payload(probe_root, settings)
|
|
81
|
+
# Check the cheaper embedding request first so an invalid endpoint
|
|
82
|
+
# does not trigger a structured completion call unnecessarily.
|
|
83
|
+
await engine.preflight_embedding(probe_root)
|
|
84
|
+
await engine.preflight_completion(probe_root)
|
|
85
|
+
|
|
86
|
+
# ----- indexing -------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
async def initialize(self, kb_name: str, file_paths: List[str], **kwargs) -> bool:
|
|
89
|
+
self._ensure_available()
|
|
90
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
91
|
+
root_dir = resolve_storage_dir_for_rebuild(kb_dir, None)
|
|
92
|
+
self.logger.info(
|
|
93
|
+
"Initializing KB '%s' with %d file(s) using GraphRAG", kb_name, len(file_paths)
|
|
94
|
+
)
|
|
95
|
+
try:
|
|
96
|
+
gr_config.write_settings(root_dir)
|
|
97
|
+
source_for_input: dict[str, str] = {}
|
|
98
|
+
count = await ingestion.prepare_input(
|
|
99
|
+
file_paths, root_dir, source_for_input=source_for_input
|
|
100
|
+
)
|
|
101
|
+
if count == 0:
|
|
102
|
+
self.logger.error("GraphRAG: no extractable documents for '%s'", kb_name)
|
|
103
|
+
self._cleanup_failed_version_dir(root_dir)
|
|
104
|
+
return False
|
|
105
|
+
await self._build(root_dir, is_update=False)
|
|
106
|
+
storage.write_meta(root_dir)
|
|
107
|
+
if indexed_file_callback := kwargs.get("indexed_file_callback"):
|
|
108
|
+
names = await asyncio.to_thread(storage.indexed_input_names, root_dir)
|
|
109
|
+
indexed_file_callback(
|
|
110
|
+
sorted(source_for_input[name] for name in names if name in source_for_input)
|
|
111
|
+
)
|
|
112
|
+
self.logger.info("KB '%s' initialized with GraphRAG (%d docs)", kb_name, count)
|
|
113
|
+
return True
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
self.logger.error("Failed to initialize GraphRAG KB: %s", exc)
|
|
116
|
+
self.logger.error(
|
|
117
|
+
traceback.format_exc(),
|
|
118
|
+
extra={PROCESS_LOG_PRIVATE_ATTR: True},
|
|
119
|
+
)
|
|
120
|
+
self._cleanup_failed_version_dir(root_dir)
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
async def add_documents(self, kb_name: str, file_paths: List[str], **kwargs) -> bool:
|
|
124
|
+
self._ensure_available()
|
|
125
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
126
|
+
existing = resolve_storage_dir_for_read(kb_dir, None)
|
|
127
|
+
from deeptutor.services.rag.embedding_binding import bound_graph_storage_root
|
|
128
|
+
|
|
129
|
+
existing = bound_graph_storage_root(kb_dir, storage.PROVIDER, existing)
|
|
130
|
+
is_update = existing is not None and storage.has_output(existing)
|
|
131
|
+
root_dir = (
|
|
132
|
+
existing if existing is not None else resolve_storage_dir_for_rebuild(kb_dir, None)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self.logger.info(
|
|
136
|
+
"Adding %d document(s) to GraphRAG KB '%s' (update=%s)",
|
|
137
|
+
len(file_paths),
|
|
138
|
+
kb_name,
|
|
139
|
+
is_update,
|
|
140
|
+
)
|
|
141
|
+
try:
|
|
142
|
+
# Refresh settings so a changed model/endpoint is picked up.
|
|
143
|
+
settings = gr_config.build_settings()
|
|
144
|
+
if is_update:
|
|
145
|
+
# An update reuses the active version directory. Validate the
|
|
146
|
+
# exact settings snapshot first so a bad provider configuration
|
|
147
|
+
# cannot overwrite a working settings.yaml or add input files.
|
|
148
|
+
await self._preflight_settings(settings)
|
|
149
|
+
gr_config.write_settings_payload(root_dir, settings)
|
|
150
|
+
count = await ingestion.prepare_input(file_paths, root_dir)
|
|
151
|
+
if count == 0:
|
|
152
|
+
self.logger.warning("GraphRAG: no extractable documents to add for '%s'", kb_name)
|
|
153
|
+
return False
|
|
154
|
+
await self._build(
|
|
155
|
+
root_dir,
|
|
156
|
+
is_update=is_update,
|
|
157
|
+
preflight_embedding_model=not is_update,
|
|
158
|
+
)
|
|
159
|
+
storage.write_meta(root_dir)
|
|
160
|
+
self.logger.info("Added %d doc(s) to GraphRAG KB '%s'", count, kb_name)
|
|
161
|
+
return True
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
self.logger.error("Failed to add documents to GraphRAG KB: %s", exc)
|
|
164
|
+
self.logger.error(
|
|
165
|
+
traceback.format_exc(),
|
|
166
|
+
extra={PROCESS_LOG_PRIVATE_ATTR: True},
|
|
167
|
+
)
|
|
168
|
+
if not is_update:
|
|
169
|
+
self._cleanup_failed_version_dir(root_dir)
|
|
170
|
+
raise
|
|
171
|
+
|
|
172
|
+
async def _build(
|
|
173
|
+
self,
|
|
174
|
+
root_dir: Path,
|
|
175
|
+
*,
|
|
176
|
+
is_update: bool,
|
|
177
|
+
preflight_embedding_model: bool = True,
|
|
178
|
+
) -> None:
|
|
179
|
+
from . import engine
|
|
180
|
+
|
|
181
|
+
await engine.build(
|
|
182
|
+
root_dir,
|
|
183
|
+
is_update=is_update,
|
|
184
|
+
preflight_embedding_model=preflight_embedding_model,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# ----- retrieval ------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
async def search(self, query: str, kb_name: str, **kwargs) -> Dict[str, Any]:
|
|
190
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
191
|
+
root_dir = resolve_storage_dir_for_read(kb_dir, None)
|
|
192
|
+
from deeptutor.services.rag.embedding_binding import bound_graph_storage_root
|
|
193
|
+
|
|
194
|
+
root_dir = bound_graph_storage_root(kb_dir, storage.PROVIDER, root_dir)
|
|
195
|
+
|
|
196
|
+
if root_dir is None or not storage.has_output(root_dir):
|
|
197
|
+
return {
|
|
198
|
+
"query": query,
|
|
199
|
+
"answer": (
|
|
200
|
+
"This GraphRAG knowledge base has no index yet. Add documents before querying."
|
|
201
|
+
),
|
|
202
|
+
"content": "",
|
|
203
|
+
"sources": [],
|
|
204
|
+
"provider": storage.PROVIDER,
|
|
205
|
+
"needs_reindex": True,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
mode = self._resolve_mode(kb_name, kwargs)
|
|
209
|
+
try:
|
|
210
|
+
self._ensure_available()
|
|
211
|
+
from . import engine
|
|
212
|
+
|
|
213
|
+
response, context_data = await engine.search(root_dir, query, mode)
|
|
214
|
+
except gr_config.GraphRagNotAvailableError as exc:
|
|
215
|
+
return self._error_result(query, exc, error_type="not_configured")
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
self.logger.error("GraphRAG search failed: %s", exc)
|
|
218
|
+
self.logger.error(
|
|
219
|
+
traceback.format_exc(),
|
|
220
|
+
extra={PROCESS_LOG_PRIVATE_ATTR: True},
|
|
221
|
+
)
|
|
222
|
+
return self._error_result(query, exc, error_type="retrieval_error")
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
"query": query,
|
|
226
|
+
"answer": response,
|
|
227
|
+
"content": response,
|
|
228
|
+
"sources": _context_to_sources(context_data),
|
|
229
|
+
"provider": storage.PROVIDER,
|
|
230
|
+
"mode": mode,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
def _error_result(self, query: str, exc: Exception, *, error_type: str) -> Dict[str, Any]:
|
|
234
|
+
return {
|
|
235
|
+
"query": query,
|
|
236
|
+
"answer": str(exc),
|
|
237
|
+
"content": "",
|
|
238
|
+
"sources": [],
|
|
239
|
+
"provider": storage.PROVIDER,
|
|
240
|
+
"error_type": error_type,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# ----- lifecycle ------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
async def delete(self, kb_name: str, **kwargs) -> bool:
|
|
246
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
247
|
+
if kb_dir.exists():
|
|
248
|
+
shutil.rmtree(kb_dir)
|
|
249
|
+
self.logger.info("Deleted GraphRAG KB '%s'", kb_name)
|
|
250
|
+
return True
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _context_to_sources(context_data: dict[str, Any]) -> list[dict[str, Any]]:
|
|
255
|
+
"""Map GraphRAG context records into DeepTutor's source-citation shape."""
|
|
256
|
+
sources: list[dict[str, Any]] = []
|
|
257
|
+
if not isinstance(context_data, dict):
|
|
258
|
+
return sources
|
|
259
|
+
# ``sources`` are the text units; ``reports`` are community summaries. Prefer
|
|
260
|
+
# the most concrete provenance available.
|
|
261
|
+
for key in ("sources", "reports", "entities"):
|
|
262
|
+
records = context_data.get(key)
|
|
263
|
+
if not isinstance(records, list):
|
|
264
|
+
continue
|
|
265
|
+
for rec in records:
|
|
266
|
+
if not isinstance(rec, dict):
|
|
267
|
+
continue
|
|
268
|
+
text = str(rec.get("text") or rec.get("content") or rec.get("description") or "")
|
|
269
|
+
sources.append(
|
|
270
|
+
{
|
|
271
|
+
"title": str(rec.get("title") or rec.get("name") or f"GraphRAG {key}"),
|
|
272
|
+
"content": text[:200],
|
|
273
|
+
"source": str(rec.get("source") or ""),
|
|
274
|
+
"page": "",
|
|
275
|
+
"chunk_id": str(rec.get("id") or ""),
|
|
276
|
+
"score": rec.get("rank") or rec.get("score") or "",
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
if sources:
|
|
280
|
+
break
|
|
281
|
+
return sources
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
__all__ = ["GraphRagPipeline"]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Resolve DeepTutor model bindings at the GraphRAG/LiteLLM boundary."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from deeptutor.services.llm.reasoning_params import build_openai_compatible_reasoning_kwargs
|
|
8
|
+
from deeptutor.services.provider_registry import (
|
|
9
|
+
effective_backend,
|
|
10
|
+
find_by_name,
|
|
11
|
+
strip_provider_prefix,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .errors import GraphRagUnsupportedProviderError
|
|
15
|
+
|
|
16
|
+
COMPLETION_TYPE = "deeptutor_litellm"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _runtime_binding(llm_cfg: Any) -> str:
|
|
20
|
+
return str(
|
|
21
|
+
getattr(llm_cfg, "binding", None) or getattr(llm_cfg, "provider_name", None) or "openai"
|
|
22
|
+
).strip()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_completion_provider(llm_cfg: Any) -> str:
|
|
26
|
+
"""Map a resolved DeepTutor provider to the narrow LiteLLM transport GraphRAG needs."""
|
|
27
|
+
binding = _runtime_binding(llm_cfg)
|
|
28
|
+
spec = find_by_name(binding)
|
|
29
|
+
if spec is None:
|
|
30
|
+
return "openai"
|
|
31
|
+
backend = effective_backend(spec, getattr(llm_cfg, "api_format", "auto"))
|
|
32
|
+
if backend == "anthropic":
|
|
33
|
+
return "anthropic"
|
|
34
|
+
if backend == "azure_openai":
|
|
35
|
+
return "azure"
|
|
36
|
+
if backend in {"openai_codex", "github_copilot"} or spec.is_oauth:
|
|
37
|
+
raise GraphRagUnsupportedProviderError(
|
|
38
|
+
"GraphRAG cannot use this OAuth-only model provider. Choose an API-key profile."
|
|
39
|
+
)
|
|
40
|
+
if backend == "openai_compat":
|
|
41
|
+
# DeepSeek's LiteLLM provider owns its parameter compatibility logic.
|
|
42
|
+
# Other DeepTutor OpenAI-compatible profiles already expose an OpenAI
|
|
43
|
+
# chat-completions endpoint and are safest on the generic transport.
|
|
44
|
+
return "deepseek" if spec.name == "deepseek" else "openai"
|
|
45
|
+
raise GraphRagUnsupportedProviderError(
|
|
46
|
+
"GraphRAG does not support the selected model provider transport."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_completion_model(llm_cfg: Any) -> str:
|
|
51
|
+
"""Return the model identifier expected by the selected LiteLLM transport."""
|
|
52
|
+
model = str(getattr(llm_cfg, "model", "") or "")
|
|
53
|
+
spec = find_by_name(_runtime_binding(llm_cfg))
|
|
54
|
+
return strip_provider_prefix(model, spec)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def resolve_completion_call_args(llm_cfg: Any) -> dict[str, Any]:
|
|
58
|
+
"""Return the request options shared by GraphRAG probes and indexing."""
|
|
59
|
+
call_args: dict[str, Any] = {}
|
|
60
|
+
extra_headers = getattr(llm_cfg, "extra_headers", None)
|
|
61
|
+
if isinstance(extra_headers, dict) and extra_headers:
|
|
62
|
+
call_args["extra_headers"] = dict(extra_headers)
|
|
63
|
+
|
|
64
|
+
binding = _runtime_binding(llm_cfg)
|
|
65
|
+
spec = find_by_name(binding)
|
|
66
|
+
reasoning_effort = getattr(llm_cfg, "reasoning_effort", None)
|
|
67
|
+
if spec is not None and spec.backend == "openai_compat":
|
|
68
|
+
call_args.update(
|
|
69
|
+
build_openai_compatible_reasoning_kwargs(
|
|
70
|
+
spec=spec,
|
|
71
|
+
binding=binding,
|
|
72
|
+
model=resolve_completion_model(llm_cfg),
|
|
73
|
+
reasoning_effort=reasoning_effort,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
elif reasoning_effort:
|
|
77
|
+
# Preserve the existing GraphRAG behavior for Anthropic, Azure, and
|
|
78
|
+
# unknown transports; the OpenAI-compatible normalizer is not valid for
|
|
79
|
+
# those backends.
|
|
80
|
+
call_args["reasoning_effort"] = reasoning_effort
|
|
81
|
+
return call_args
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def resolve_persisted_completion_provider(model_config: Any) -> str:
|
|
85
|
+
"""Recover a provider for old DeepTutor GraphRAG settings without rewriting them."""
|
|
86
|
+
current = str(getattr(model_config, "model_provider", "") or "openai")
|
|
87
|
+
if current != "openai":
|
|
88
|
+
return current
|
|
89
|
+
|
|
90
|
+
model = str(getattr(model_config, "model", "") or "")
|
|
91
|
+
api_base = str(getattr(model_config, "api_base", "") or "").rstrip("/")
|
|
92
|
+
try:
|
|
93
|
+
from deeptutor.services.config import (
|
|
94
|
+
get_model_catalog_service,
|
|
95
|
+
resolve_llm_runtime_config,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
service = get_model_catalog_service()
|
|
99
|
+
catalog = service.load()
|
|
100
|
+
profiles = catalog.get("services", {}).get("llm", {}).get("profiles", [])
|
|
101
|
+
matches: list[tuple[str, str]] = []
|
|
102
|
+
for profile in profiles:
|
|
103
|
+
profile_base = str(profile.get("base_url") or "").rstrip("/")
|
|
104
|
+
if profile_base != api_base:
|
|
105
|
+
continue
|
|
106
|
+
for candidate in profile.get("models", []):
|
|
107
|
+
if str(candidate.get("model") or "") == model:
|
|
108
|
+
matches.append((str(profile.get("id") or ""), str(candidate.get("id") or "")))
|
|
109
|
+
if len(matches) == 1:
|
|
110
|
+
profile_id, model_id = matches[0]
|
|
111
|
+
resolved = resolve_llm_runtime_config(
|
|
112
|
+
catalog=catalog,
|
|
113
|
+
service=service,
|
|
114
|
+
llm_selection={"profile_id": profile_id, "model_id": model_id},
|
|
115
|
+
)
|
|
116
|
+
return resolve_completion_provider(resolved)
|
|
117
|
+
except GraphRagUnsupportedProviderError:
|
|
118
|
+
raise
|
|
119
|
+
except Exception:
|
|
120
|
+
# Old settings remain usable even when the catalog is unavailable.
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
if "api.deepseek.com" in api_base.lower():
|
|
124
|
+
return "deepseek"
|
|
125
|
+
return current
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
__all__ = [
|
|
129
|
+
"COMPLETION_TYPE",
|
|
130
|
+
"resolve_completion_call_args",
|
|
131
|
+
"resolve_completion_model",
|
|
132
|
+
"resolve_completion_provider",
|
|
133
|
+
"resolve_persisted_completion_provider",
|
|
134
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""On-disk layout for a GraphRAG-backed knowledge base.
|
|
2
|
+
|
|
3
|
+
A GraphRAG KB keeps a self-contained project inside the KB's flat ``version-N``
|
|
4
|
+
directory (reused from ``index_versioning`` with a ``None`` signature, exactly
|
|
5
|
+
like the PageIndex pipeline). The version dir doubles as GraphRAG's project
|
|
6
|
+
root::
|
|
7
|
+
|
|
8
|
+
<kb_dir>/version-N/
|
|
9
|
+
settings.yaml # generated from DeepTutor config (see config.py)
|
|
10
|
+
input/ # parsed *.txt fed to the indexer
|
|
11
|
+
output/ # GraphRAG's parquet artefacts + lancedb
|
|
12
|
+
cache/ logs/
|
|
13
|
+
meta.json # synthetic "ready" marker (see write_meta)
|
|
14
|
+
|
|
15
|
+
The synthetic ``meta.json`` is what makes the existing "is this KB initialised?"
|
|
16
|
+
and Index-versions UI checks treat a GraphRAG KB as ready, without teaching the
|
|
17
|
+
manager about GraphRAG internals.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
import logging
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from deeptutor.services.file_io import atomic_write_json
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
META_FILENAME = "meta.json"
|
|
31
|
+
PROVIDER = "graphrag"
|
|
32
|
+
|
|
33
|
+
INPUT_DIRNAME = "input"
|
|
34
|
+
OUTPUT_DIRNAME = "output"
|
|
35
|
+
|
|
36
|
+
# Parquet artefacts GraphRAG writes on a successful index; their presence is our
|
|
37
|
+
# "the index actually built" signal (independent of the synthetic meta marker).
|
|
38
|
+
OUTPUT_TABLES = (
|
|
39
|
+
"entities",
|
|
40
|
+
"communities",
|
|
41
|
+
"community_reports",
|
|
42
|
+
"text_units",
|
|
43
|
+
"relationships",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def input_dir(root_dir: Path) -> Path:
|
|
48
|
+
return Path(root_dir) / INPUT_DIRNAME
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def output_dir(root_dir: Path) -> Path:
|
|
52
|
+
return Path(root_dir) / OUTPUT_DIRNAME
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def has_output(root_dir: Path | None) -> bool:
|
|
56
|
+
"""True when GraphRAG has produced at least its core parquet tables."""
|
|
57
|
+
if root_dir is None:
|
|
58
|
+
return False
|
|
59
|
+
out = output_dir(root_dir)
|
|
60
|
+
if not out.is_dir():
|
|
61
|
+
return False
|
|
62
|
+
return any((out / f"{name}.parquet").exists() for name in OUTPUT_TABLES)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def indexed_input_names(root_dir: Path) -> set[str]:
|
|
66
|
+
"""Input filenames with persisted GraphRAG text units; empty on unknown output."""
|
|
67
|
+
documents = output_dir(root_dir) / "documents.parquet"
|
|
68
|
+
text_units = output_dir(root_dir) / "text_units.parquet"
|
|
69
|
+
if not documents.is_file() or not text_units.is_file():
|
|
70
|
+
return set()
|
|
71
|
+
try:
|
|
72
|
+
import pandas as pd
|
|
73
|
+
|
|
74
|
+
rows = pd.read_parquet(documents, columns=["title", "text_unit_ids"])
|
|
75
|
+
unit_ids = set(pd.read_parquet(text_units, columns=["id"])["id"])
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
logger.warning("Could not inspect GraphRAG document receipts: %s", exc)
|
|
78
|
+
return set()
|
|
79
|
+
|
|
80
|
+
names: set[str] = set()
|
|
81
|
+
for title, text_unit_ids in rows.itertuples(index=False, name=None):
|
|
82
|
+
if not isinstance(title, str) or isinstance(text_unit_ids, (str, bytes)):
|
|
83
|
+
continue
|
|
84
|
+
try:
|
|
85
|
+
has_persisted_unit = any(unit_id in unit_ids for unit_id in text_unit_ids)
|
|
86
|
+
except TypeError:
|
|
87
|
+
has_persisted_unit = False
|
|
88
|
+
if has_persisted_unit:
|
|
89
|
+
names.add(Path(title).name)
|
|
90
|
+
return names
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def write_meta(root_dir: Path) -> None:
|
|
94
|
+
"""Write a flat-layout ``meta.json`` so the version is listed as ready.
|
|
95
|
+
|
|
96
|
+
Mirrors ``index_versioning.write_version_meta`` but carries a synthetic
|
|
97
|
+
``graphrag`` signature instead of an embedding hash. The embedding identity
|
|
98
|
+
is stamped alongside so an externally-linked index can be checked for
|
|
99
|
+
embedding compatibility at connect time (GraphRAG otherwise fails retrieval
|
|
100
|
+
silently on a dimension mismatch).
|
|
101
|
+
"""
|
|
102
|
+
from deeptutor.services.rag.embedding_signature import embedding_meta_fields
|
|
103
|
+
|
|
104
|
+
target = Path(root_dir)
|
|
105
|
+
payload = {
|
|
106
|
+
"version": target.name,
|
|
107
|
+
"signature": PROVIDER,
|
|
108
|
+
"provider": PROVIDER,
|
|
109
|
+
"layout": "flat",
|
|
110
|
+
"created_at": datetime.now(timezone.utc).replace(tzinfo=None).isoformat() + "Z",
|
|
111
|
+
**embedding_meta_fields(),
|
|
112
|
+
}
|
|
113
|
+
atomic_write_json(target / META_FILENAME, payload)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
__all__ = [
|
|
117
|
+
"META_FILENAME",
|
|
118
|
+
"PROVIDER",
|
|
119
|
+
"INPUT_DIRNAME",
|
|
120
|
+
"OUTPUT_DIRNAME",
|
|
121
|
+
"OUTPUT_TABLES",
|
|
122
|
+
"input_dir",
|
|
123
|
+
"output_dir",
|
|
124
|
+
"has_output",
|
|
125
|
+
"indexed_input_names",
|
|
126
|
+
"write_meta",
|
|
127
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Tencent IMA integration — retrieval, inventory and authoring over a live library.
|
|
2
|
+
|
|
3
|
+
A KB bound to the ``ima`` provider is a connection pointer to a library the user
|
|
4
|
+
keeps in IMA (https://ima.qq.com) and curates there. DeepTutor never indexes or
|
|
5
|
+
stores anything locally; everything is an OpenAPI call:
|
|
6
|
+
|
|
7
|
+
* :mod:`.pipeline` — the ``rag`` engine: ``search_knowledge`` plus a bounded
|
|
8
|
+
full-text top-up for matches whose snippet is too thin to reason from.
|
|
9
|
+
* :mod:`.inventory` — what the library *contains* (``get_knowledge_list``), which
|
|
10
|
+
is what makes "list the documents in this knowledge base" answerable; read from
|
|
11
|
+
the synchronous manifest layer, so it is blocking and cached.
|
|
12
|
+
* :mod:`.notes` — the notes module, including the sort orders that answer "my
|
|
13
|
+
most recent items" (knowledge items carry no timestamps; notes do).
|
|
14
|
+
* :mod:`.client` — the knowledge-base method table, with :mod:`.transport`,
|
|
15
|
+
:mod:`.envelope`, :mod:`.models` and :mod:`.media` underneath it.
|
|
16
|
+
* :mod:`.probe` — the connect-time health check.
|
|
17
|
+
|
|
18
|
+
The IMA credentials are configured once for the account on the engine page (a KB
|
|
19
|
+
may override them to reach another account); the knowledge base id is always
|
|
20
|
+
per-KB. The agentic surface over these calls is
|
|
21
|
+
:mod:`deeptutor.capabilities.ima`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from .pipeline import ImaPipeline
|
|
27
|
+
|
|
28
|
+
__all__ = ["ImaPipeline"]
|