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