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