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,479 @@
1
+ """Thin adapter over the GraphRAG (microsoft/graphrag) Python API.
2
+
3
+ This is the ONLY module that imports ``graphrag``. Everything GraphRAG-version
4
+ sensitive lives here, so a schema/API shift between releases is a one-file fix.
5
+ Pinned to the 3.x line (``graphrag>=3,<4``); the indexing/query surface mirrors
6
+ ``graphrag.cli.{index,query}`` for that line.
7
+
8
+ All imports are lazy so the package only loads when a GraphRAG KB is actually
9
+ used — DeepTutor runs fine without the optional dependency installed.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ from collections.abc import Awaitable, Callable
16
+ import logging
17
+ from pathlib import Path
18
+ from typing import Any, TypeVar
19
+
20
+ from .config import (
21
+ COMPLETION_MODEL_ID,
22
+ DEFAULT_MODE,
23
+ normalize_mode,
24
+ query_config_from_settings,
25
+ )
26
+ from .errors import (
27
+ EMBEDDING_RESPONSE_MESSAGE,
28
+ GraphRagEmbeddingDimensionError,
29
+ GraphRagEmbeddingProbeError,
30
+ GraphRagEmbeddingResponseError,
31
+ GraphRagModelIncompatibleError,
32
+ GraphRagStructuredOutputError,
33
+ GraphRagUnsupportedProviderError,
34
+ classify_embedding_error,
35
+ classify_model_error,
36
+ )
37
+ from .pandas_compat import prepare_pandas_arrow_extensions
38
+ from .provider import (
39
+ COMPLETION_TYPE,
40
+ resolve_completion_call_args,
41
+ resolve_completion_model,
42
+ resolve_completion_provider,
43
+ resolve_persisted_completion_provider,
44
+ )
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+ # Fallback response style + community granularity. The live values come from the
49
+ # persisted graphrag.json slice (query_config_from_settings); these constants are
50
+ # kept for tests / call sites that reference the defaults directly.
51
+ RESPONSE_TYPE = "Multiple Paragraphs"
52
+ DEFAULT_COMMUNITY_LEVEL = 2
53
+ # Deliberately not in agents.yaml with the other budgets: this is the
54
+ # reachability probe, not a generation call a reader ever sees. It asks a
55
+ # model to prove it answers at all, and a user tuning it would only be
56
+ # able to make the health check lie.
57
+ PROBE_MAX_TOKENS = 1024
58
+ PROBE_TIMEOUT_SECONDS = 25
59
+ EMBEDDING_PROBE_TEXT = "DeepTutor GraphRAG embedding compatibility test"
60
+
61
+ # Per-mode output tables the query API needs (mirrors graphrag.cli.query).
62
+ _OUTPUTS_BY_MODE: dict[str, tuple[list[str], list[str]]] = {
63
+ "global": (["entities", "communities", "community_reports"], []),
64
+ "local": (
65
+ ["communities", "community_reports", "text_units", "relationships", "entities"],
66
+ ["covariates"],
67
+ ),
68
+ "drift": (
69
+ ["communities", "community_reports", "text_units", "relationships", "entities"],
70
+ [],
71
+ ),
72
+ "basic": (["text_units"], []),
73
+ }
74
+
75
+
76
+ _T = TypeVar("_T")
77
+
78
+
79
+ def _load_config(root_dir: Path):
80
+ from graphrag.config.load_config import load_config
81
+
82
+ from .completion_adapter import register_completion_adapter
83
+
84
+ register_completion_adapter()
85
+ config = load_config(root_dir=Path(root_dir))
86
+ from deeptutor.services.embedding.config import scoped_embedding_config
87
+
88
+ embedding = scoped_embedding_config()
89
+ if embedding is not None:
90
+ from .config import _embedding_model_entry, graphrag_embedding_api_base
91
+
92
+ fields = _embedding_model_entry(
93
+ model=embedding.model,
94
+ api_base=graphrag_embedding_api_base(
95
+ embedding.binding, embedding.effective_url or embedding.base_url
96
+ ),
97
+ api_key=embedding.api_key,
98
+ binding=embedding.binding,
99
+ dimension=embedding.dim,
100
+ send_dimensions=embedding.send_dimensions,
101
+ extra_headers=embedding.extra_headers,
102
+ )
103
+ for name, model_config in config.embedding_models.items():
104
+ config.embedding_models[name] = model_config.model_copy(update=fields)
105
+ for model_config in config.completion_models.values():
106
+ if model_config.type in {"litellm", COMPLETION_TYPE}:
107
+ model_config.type = COMPLETION_TYPE
108
+ model_config.model_provider = resolve_persisted_completion_provider(model_config)
109
+ return config
110
+
111
+
112
+ async def _run_isolated(work: Callable[[], Awaitable[_T]]) -> _T:
113
+ """Run one GraphRAG entry point on a private, plain-asyncio event loop.
114
+
115
+ ``graphrag_llm`` calls ``nest_asyncio2.apply()`` at import time, and that
116
+ patch refuses uvloop ("Can't patch loop of type <class 'uvloop.Loop'>") —
117
+ which is precisely the loop ``uvicorn[standard]`` gives the backend, so on
118
+ macOS/Linux the first GraphRAG import aborted indexing before it started
119
+ (issue #695). Importing and driving GraphRAG from a worker thread that owns
120
+ a stock asyncio loop hands that patch — and GraphRAG's own nested
121
+ ``run_until_complete`` calls — the reentrant loop they expect, and leaves
122
+ the server's uvloop loop untouched.
123
+
124
+ Every ``graphrag`` import therefore has to happen inside ``work``, which is
125
+ why the entry points below keep their imports function-local.
126
+ """
127
+
128
+ def _runner() -> _T:
129
+ prepare_pandas_arrow_extensions()
130
+ loop = asyncio.new_event_loop()
131
+ asyncio.set_event_loop(loop)
132
+ try:
133
+ return loop.run_until_complete(work())
134
+ finally:
135
+ try:
136
+ loop.close()
137
+ finally:
138
+ asyncio.set_event_loop(None)
139
+
140
+ return await asyncio.to_thread(_runner)
141
+
142
+
143
+ def _create_probe_completion(llm_cfg: Any):
144
+ """Create the same adapted completion client/schema used by community reports."""
145
+ from graphrag.index.operations.summarize_communities.community_reports_extractor import (
146
+ CommunityReportResponse,
147
+ )
148
+ from graphrag_llm.completion import create_completion
149
+ from graphrag_llm.config import ModelConfig
150
+
151
+ from .completion_adapter import register_completion_adapter
152
+
153
+ api_base = getattr(llm_cfg, "effective_url", None) or getattr(llm_cfg, "base_url", None)
154
+ call_args = resolve_completion_call_args(llm_cfg)
155
+ register_completion_adapter()
156
+ model_config = ModelConfig(
157
+ type=COMPLETION_TYPE,
158
+ model_provider=resolve_completion_provider(llm_cfg),
159
+ model=resolve_completion_model(llm_cfg),
160
+ api_base=api_base,
161
+ api_version=getattr(llm_cfg, "api_version", None),
162
+ api_key=getattr(llm_cfg, "api_key", None) or "sk-no-key-required",
163
+ auth_method="api_key",
164
+ call_args=call_args,
165
+ )
166
+ return create_completion(model_config), CommunityReportResponse
167
+
168
+
169
+ def _create_configured_probe_completion(config: Any):
170
+ """Create a probe from a loaded settings snapshot used by real indexing."""
171
+ from graphrag.index.operations.summarize_communities.community_reports_extractor import (
172
+ CommunityReportResponse,
173
+ )
174
+ from graphrag_llm.completion import create_completion
175
+
176
+ return (
177
+ create_completion(config.completion_models[COMPLETION_MODEL_ID]),
178
+ CommunityReportResponse,
179
+ )
180
+
181
+
182
+ async def _validate_probe_completion(completion: Any, response_model: type) -> None:
183
+ """Request and validate one minimal GraphRAG community-report response."""
184
+ response = await completion.completion_async(
185
+ messages=(
186
+ "Return one concise community report for a graph containing one topic named "
187
+ "'compatibility test'. Include a title, summary, one finding with summary and "
188
+ "explanation, a numeric rating, and a rating explanation."
189
+ ),
190
+ response_format=response_model,
191
+ max_tokens=PROBE_MAX_TOKENS,
192
+ stream=False,
193
+ timeout=PROBE_TIMEOUT_SECONDS,
194
+ )
195
+ if not isinstance(getattr(response, "formatted_response", None), response_model):
196
+ raise GraphRagStructuredOutputError("GraphRAG structured response validation failed.")
197
+
198
+
199
+ async def _probe_completion_model_impl(llm_cfg: Any) -> None:
200
+ """Probe a resolved DeepTutor model through the GraphRAG adapter."""
201
+ await _validate_probe_completion(*_create_probe_completion(llm_cfg))
202
+
203
+
204
+ def _failed_probe_result(llm_cfg: Any, error: Exception) -> dict[str, Any]:
205
+ """Classify a probe failure without returning provider messages or credentials."""
206
+ classified = classify_model_error(error)
207
+ if isinstance(
208
+ classified,
209
+ (GraphRagModelIncompatibleError, GraphRagUnsupportedProviderError),
210
+ ):
211
+ status = "incompatible"
212
+ compatible: bool | None = False
213
+ else:
214
+ status = "unverifiable"
215
+ compatible = None
216
+ if classified is not None:
217
+ return {
218
+ "status": status,
219
+ "compatible": compatible,
220
+ "code": classified.code,
221
+ "message": str(classified),
222
+ "model": str(getattr(llm_cfg, "model", "") or ""),
223
+ "binding": str(getattr(llm_cfg, "binding", "") or ""),
224
+ "retryable": classified.retryable,
225
+ }
226
+ return {
227
+ "status": "unverifiable",
228
+ "compatible": None,
229
+ "code": "graphrag_model_probe_failed",
230
+ "message": "GraphRAG compatibility could not be verified with this model.",
231
+ "model": str(getattr(llm_cfg, "model", "") or ""),
232
+ "binding": str(getattr(llm_cfg, "binding", "") or ""),
233
+ "retryable": False,
234
+ }
235
+
236
+
237
+ async def probe_completion_model(llm_cfg: Any) -> dict[str, Any]:
238
+ """Test one completion config against GraphRAG's structured-output contract."""
239
+ try:
240
+ await _run_isolated(lambda: _probe_completion_model_impl(llm_cfg))
241
+ except Exception as error: # noqa: BLE001 - converted into a secret-free probe result
242
+ return _failed_probe_result(llm_cfg, error)
243
+ return {
244
+ "status": "compatible",
245
+ "compatible": True,
246
+ "code": "graphrag_model_compatible",
247
+ "message": "The model returned valid GraphRAG structured output.",
248
+ "model": str(getattr(llm_cfg, "model", "") or ""),
249
+ "binding": str(getattr(llm_cfg, "binding", "") or ""),
250
+ "retryable": False,
251
+ }
252
+
253
+
254
+ def _create_probe_embedding(config: Any) -> tuple[Any, int]:
255
+ """Create GraphRAG's configured embedding client and expected vector size."""
256
+ from graphrag_llm.embedding import create_embedding
257
+
258
+ model_id = config.embed_text.embedding_model_id
259
+ model_config = config.embedding_models[model_id]
260
+ expected_dimension = int(config.vector_store.vector_size or 0)
261
+ return create_embedding(model_config), expected_dimension
262
+
263
+
264
+ async def _probe_embedding_model_impl(config: Any) -> None:
265
+ """Run one bounded embedding request through GraphRAG's actual client."""
266
+ embedding, expected_dimension = _create_probe_embedding(config)
267
+ try:
268
+ response = await embedding.embedding_async(
269
+ input=[EMBEDDING_PROBE_TEXT],
270
+ timeout=PROBE_TIMEOUT_SECONDS,
271
+ )
272
+ except Exception as error: # noqa: BLE001 - classified into secret-free metadata
273
+ classified = classify_embedding_error(error)
274
+ if classified is not None:
275
+ raise classified from error
276
+ raise GraphRagEmbeddingProbeError() from error
277
+
278
+ vector = getattr(response, "first_embedding", None)
279
+ if not isinstance(vector, list) or not vector:
280
+ raise GraphRagEmbeddingResponseError(EMBEDDING_RESPONSE_MESSAGE)
281
+ if expected_dimension and len(vector) != expected_dimension:
282
+ raise GraphRagEmbeddingDimensionError(
283
+ configured=expected_dimension,
284
+ actual=len(vector),
285
+ )
286
+
287
+
288
+ async def preflight_embedding(root_dir: Path) -> None:
289
+ """Validate one settings snapshot through GraphRAG's real embedding client."""
290
+ await _run_isolated(lambda: _preflight_embedding_impl(root_dir))
291
+
292
+
293
+ async def preflight_completion(root_dir: Path) -> None:
294
+ """Validate the completion model from the exact persisted settings snapshot."""
295
+ try:
296
+ await _run_isolated(lambda: _preflight_completion_impl(root_dir))
297
+ except Exception as error:
298
+ classified = classify_model_error(error)
299
+ if classified is not None and classified is not error:
300
+ raise classified from error
301
+ raise
302
+
303
+
304
+ async def _preflight_completion_impl(root_dir: Path) -> None:
305
+ config = _load_config(root_dir)
306
+ logger.info("GraphRAG: validating the active completion model before indexing")
307
+ await _validate_probe_completion(*_create_configured_probe_completion(config))
308
+
309
+
310
+ async def _preflight_embedding_impl(root_dir: Path) -> None:
311
+ config = _load_config(root_dir)
312
+ logger.info("GraphRAG: validating the active embedding model before indexing")
313
+ await _probe_embedding_model_impl(config)
314
+
315
+
316
+ async def build(
317
+ root_dir: Path,
318
+ *,
319
+ is_update: bool = False,
320
+ preflight_embedding_model: bool = True,
321
+ ) -> None:
322
+ """Run the GraphRAG indexing pipeline rooted at ``root_dir``.
323
+
324
+ Raises on any failed workflow so the caller can surface an error and clean
325
+ up the (incomplete) version directory.
326
+ """
327
+ try:
328
+ await _run_isolated(
329
+ lambda: _build_impl(
330
+ root_dir,
331
+ is_update=is_update,
332
+ preflight_embedding_model=preflight_embedding_model,
333
+ )
334
+ )
335
+ except Exception as error:
336
+ classified = classify_model_error(error)
337
+ if classified is not None and classified is not error:
338
+ raise classified from error
339
+ raise
340
+
341
+
342
+ async def _build_impl(
343
+ root_dir: Path,
344
+ *,
345
+ is_update: bool,
346
+ preflight_embedding_model: bool,
347
+ ) -> None:
348
+ from graphrag.api import build_index
349
+ from graphrag.config.enums import IndexingMethod
350
+
351
+ config = _load_config(root_dir)
352
+ if preflight_embedding_model:
353
+ logger.info("GraphRAG: validating the active embedding model before indexing")
354
+ await _probe_embedding_model_impl(config)
355
+ logger.info("GraphRAG: building index at %s (update=%s)", root_dir, is_update)
356
+ results = await build_index(
357
+ config=config,
358
+ method=IndexingMethod.Standard,
359
+ is_update_run=is_update,
360
+ )
361
+ errors = [r for r in results if getattr(r, "error", None) is not None]
362
+ if errors:
363
+ for result in errors:
364
+ error = getattr(result, "error", None)
365
+ if isinstance(error, BaseException):
366
+ workflow = str(getattr(result, "workflow", "") or "").lower()
367
+ classified = (
368
+ classify_embedding_error(error)
369
+ if "embed" in workflow
370
+ else classify_model_error(error)
371
+ )
372
+ if classified is not None:
373
+ raise classified from error
374
+ detail = "; ".join(f"{r.workflow}: {r.error}" for r in errors[:3])
375
+ raise RuntimeError(f"GraphRAG indexing failed: {detail}")
376
+
377
+
378
+ async def _resolve_outputs(config, names: list[str], optional: list[str]) -> dict[str, Any]:
379
+ """Load the requested output parquet tables as DataFrames (mirrors the CLI)."""
380
+ from graphrag.data_model.data_reader import DataReader
381
+ from graphrag_storage import create_storage
382
+ from graphrag_storage.tables.table_provider_factory import create_table_provider
383
+
384
+ storage_obj = create_storage(config.output_storage)
385
+ table_provider = create_table_provider(config.table_provider, storage=storage_obj)
386
+ reader = DataReader(table_provider)
387
+
388
+ frames: dict[str, Any] = {}
389
+ for name in names:
390
+ frames[name] = await getattr(reader, name)()
391
+ for name in optional:
392
+ frames[name] = await getattr(reader, name)() if await table_provider.has(name) else None
393
+ return frames
394
+
395
+
396
+ async def search(root_dir: Path, query: str, mode: str | None = None) -> tuple[str, dict]:
397
+ """Run a GraphRAG query and return ``(response_text, context_data)``.
398
+
399
+ ``context_data`` is normalised to a dict of record lists
400
+ (reports/entities/relationships/claims/sources) via GraphRAG's own helper.
401
+ """
402
+ try:
403
+ return await _run_isolated(lambda: _search_impl(root_dir, query, mode))
404
+ except Exception as error:
405
+ classified = classify_model_error(error)
406
+ if classified is not None and classified is not error:
407
+ raise classified from error
408
+ raise
409
+
410
+
411
+ async def _search_impl(root_dir: Path, query: str, mode: str | None) -> tuple[str, dict]:
412
+ import graphrag.api as api
413
+ from graphrag.utils.api import reformat_context_data
414
+
415
+ resolved_mode = normalize_mode(mode)
416
+ cfg = query_config_from_settings()
417
+ config = _load_config(root_dir)
418
+ names, optional = _OUTPUTS_BY_MODE.get(resolved_mode, _OUTPUTS_BY_MODE[DEFAULT_MODE])
419
+ frames = await _resolve_outputs(config, names, optional)
420
+
421
+ if resolved_mode == "global":
422
+ response, context = await api.global_search(
423
+ config=config,
424
+ entities=frames["entities"],
425
+ communities=frames["communities"],
426
+ community_reports=frames["community_reports"],
427
+ community_level=None,
428
+ dynamic_community_selection=cfg.dynamic_community_selection,
429
+ response_type=cfg.response_type,
430
+ query=query,
431
+ )
432
+ elif resolved_mode == "drift":
433
+ response, context = await api.drift_search(
434
+ config=config,
435
+ entities=frames["entities"],
436
+ communities=frames["communities"],
437
+ community_reports=frames["community_reports"],
438
+ text_units=frames["text_units"],
439
+ relationships=frames["relationships"],
440
+ community_level=cfg.community_level,
441
+ response_type=cfg.response_type,
442
+ query=query,
443
+ )
444
+ elif resolved_mode == "basic":
445
+ response, context = await api.basic_search(
446
+ config=config,
447
+ text_units=frames["text_units"],
448
+ response_type=cfg.response_type,
449
+ query=query,
450
+ )
451
+ else: # local (default)
452
+ response, context = await api.local_search(
453
+ config=config,
454
+ entities=frames["entities"],
455
+ communities=frames["communities"],
456
+ community_reports=frames["community_reports"],
457
+ text_units=frames["text_units"],
458
+ relationships=frames["relationships"],
459
+ covariates=frames.get("covariates"),
460
+ community_level=cfg.community_level,
461
+ response_type=cfg.response_type,
462
+ query=query,
463
+ )
464
+
465
+ try:
466
+ context_data = reformat_context_data(context) if isinstance(context, dict) else {}
467
+ except Exception: # pragma: no cover - context shape is best-effort
468
+ context_data = {}
469
+ return str(response), context_data
470
+
471
+
472
+ __all__ = [
473
+ "build",
474
+ "search",
475
+ "probe_completion_model",
476
+ "preflight_completion",
477
+ "RESPONSE_TYPE",
478
+ "DEFAULT_COMMUNITY_LEVEL",
479
+ ]