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,274 @@
|
|
|
1
|
+
"""Per-engine environment preflight checks.
|
|
2
|
+
|
|
3
|
+
Powers the "check whether this engine can run right now" affordance on each
|
|
4
|
+
engine's detail page. Every check is best-effort and never raises — a failed
|
|
5
|
+
import or missing config becomes a failed/optional check, not an exception.
|
|
6
|
+
|
|
7
|
+
A check is ``{key, label, ok, detail, optional}``. Overall ``ok`` is true when
|
|
8
|
+
every *required* (non-optional) check passes.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .factory import (
|
|
16
|
+
DEFAULT_PROVIDER,
|
|
17
|
+
GRAPHRAG_PROVIDER,
|
|
18
|
+
IMA_PROVIDER,
|
|
19
|
+
LIGHTRAG_PROVIDER,
|
|
20
|
+
LIGHTRAG_SERVER_PROVIDER,
|
|
21
|
+
PAGEINDEX_OSS_PROVIDER,
|
|
22
|
+
PAGEINDEX_PROVIDER,
|
|
23
|
+
normalize_provider_name,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _check(key: str, label: str, ok: bool, detail: str = "", *, optional: bool = False) -> dict:
|
|
28
|
+
return {"key": key, "label": label, "ok": bool(ok), "detail": detail, "optional": optional}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _active_chat_model() -> tuple[str | None, str]:
|
|
32
|
+
"""Return ``(model, binding)`` for the active chat LLM, or ``(None, "")``."""
|
|
33
|
+
try:
|
|
34
|
+
from deeptutor.services.config import resolve_llm_runtime_config
|
|
35
|
+
|
|
36
|
+
cfg = resolve_llm_runtime_config()
|
|
37
|
+
return getattr(cfg, "model", None), str(getattr(cfg, "binding", "") or "")
|
|
38
|
+
except Exception:
|
|
39
|
+
return None, ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _active_embedding() -> tuple[str | None, int]:
|
|
43
|
+
"""Return ``(model, dim)`` for the active embedding model, or ``(None, 0)``."""
|
|
44
|
+
try:
|
|
45
|
+
from deeptutor.services.embedding import get_embedding_config
|
|
46
|
+
|
|
47
|
+
cfg = get_embedding_config()
|
|
48
|
+
return getattr(cfg, "model", None), int(getattr(cfg, "dim", 0) or 0)
|
|
49
|
+
except Exception:
|
|
50
|
+
return None, 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _llamaindex_preflight() -> dict:
|
|
54
|
+
emb_model, emb_dim = _active_embedding()
|
|
55
|
+
checks = [
|
|
56
|
+
_check(
|
|
57
|
+
"embedding",
|
|
58
|
+
"Active embedding model",
|
|
59
|
+
bool(emb_model) and emb_dim > 0,
|
|
60
|
+
f"{emb_model} · {emb_dim}d" if emb_model else "Configure one in the model catalog.",
|
|
61
|
+
)
|
|
62
|
+
]
|
|
63
|
+
try:
|
|
64
|
+
from .pipelines.llamaindex.retrievers import _import_bm25_retriever
|
|
65
|
+
|
|
66
|
+
bm25_ok = _import_bm25_retriever() is not None
|
|
67
|
+
except Exception:
|
|
68
|
+
bm25_ok = False
|
|
69
|
+
checks.append(
|
|
70
|
+
_check(
|
|
71
|
+
"bm25",
|
|
72
|
+
"BM25 hybrid retrieval",
|
|
73
|
+
bm25_ok,
|
|
74
|
+
"Installed." if bm25_ok else "Not installed — hybrid falls back to vector-only.",
|
|
75
|
+
optional=True,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
return _finalize(checks)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _pageindex_preflight() -> dict:
|
|
82
|
+
try:
|
|
83
|
+
from .pipelines.pageindex.config import get_pageindex_config
|
|
84
|
+
|
|
85
|
+
cfg = get_pageindex_config(require_key=False)
|
|
86
|
+
configured = bool(cfg.api_key)
|
|
87
|
+
except Exception:
|
|
88
|
+
configured = False
|
|
89
|
+
return _finalize(
|
|
90
|
+
[
|
|
91
|
+
_check(
|
|
92
|
+
"api_key",
|
|
93
|
+
"API key configured",
|
|
94
|
+
configured,
|
|
95
|
+
"PageIndex Cloud" if configured else "Add a PageIndex API key under Credentials.",
|
|
96
|
+
)
|
|
97
|
+
]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _pageindex_oss_preflight() -> dict:
|
|
102
|
+
try:
|
|
103
|
+
from .pipelines.pageindex.client import resolve_oss_sdk_config
|
|
104
|
+
|
|
105
|
+
model, _backend = resolve_oss_sdk_config()
|
|
106
|
+
llm_ok, detail = bool(model), model
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
llm_ok = False
|
|
109
|
+
detail = str(exc)
|
|
110
|
+
return _finalize(
|
|
111
|
+
[
|
|
112
|
+
_check("chat", "Active LLM for indexing", llm_ok, detail),
|
|
113
|
+
]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _graphrag_preflight() -> dict:
|
|
118
|
+
try:
|
|
119
|
+
from .pipelines.graphrag.config import is_graphrag_available
|
|
120
|
+
|
|
121
|
+
installed = is_graphrag_available()
|
|
122
|
+
except Exception:
|
|
123
|
+
installed = False
|
|
124
|
+
emb_model, emb_dim = _active_embedding()
|
|
125
|
+
chat_model, _ = _active_chat_model()
|
|
126
|
+
return _finalize(
|
|
127
|
+
[
|
|
128
|
+
_check(
|
|
129
|
+
"package",
|
|
130
|
+
"GraphRAG package installed",
|
|
131
|
+
installed,
|
|
132
|
+
"Installed." if installed else "pip install 'deeptutor[graphrag]'",
|
|
133
|
+
),
|
|
134
|
+
_check(
|
|
135
|
+
"chat",
|
|
136
|
+
"Active chat model",
|
|
137
|
+
bool(chat_model),
|
|
138
|
+
chat_model or "Configure one in the model catalog.",
|
|
139
|
+
),
|
|
140
|
+
_check(
|
|
141
|
+
"embedding",
|
|
142
|
+
"Active embedding model",
|
|
143
|
+
bool(emb_model) and emb_dim > 0,
|
|
144
|
+
f"{emb_model} · {emb_dim}d" if emb_model else "Configure one in the model catalog.",
|
|
145
|
+
),
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _lightrag_preflight() -> dict:
|
|
151
|
+
package_detail = "pip install 'deeptutor[rag-lightrag]'"
|
|
152
|
+
try:
|
|
153
|
+
from .pipelines.lightrag.config import is_lightrag_available
|
|
154
|
+
from .pipelines.lightrag.engine import LIGHTRAG_VERSION, installed_version
|
|
155
|
+
|
|
156
|
+
current_version = installed_version() if is_lightrag_available() else ""
|
|
157
|
+
installed = current_version == LIGHTRAG_VERSION
|
|
158
|
+
if current_version:
|
|
159
|
+
package_detail = (
|
|
160
|
+
f"Installed {current_version}."
|
|
161
|
+
if installed
|
|
162
|
+
else f"Found {current_version}; required {LIGHTRAG_VERSION}."
|
|
163
|
+
)
|
|
164
|
+
except Exception:
|
|
165
|
+
installed = False
|
|
166
|
+
emb_model, emb_dim = _active_embedding()
|
|
167
|
+
chat_model, binding = _active_chat_model()
|
|
168
|
+
vision_ok = False
|
|
169
|
+
if chat_model:
|
|
170
|
+
try:
|
|
171
|
+
from deeptutor.services.llm.capabilities import supports_vision
|
|
172
|
+
|
|
173
|
+
vision_ok = supports_vision(binding, chat_model)
|
|
174
|
+
except Exception:
|
|
175
|
+
vision_ok = False
|
|
176
|
+
return _finalize(
|
|
177
|
+
[
|
|
178
|
+
_check(
|
|
179
|
+
"package",
|
|
180
|
+
"LightRAG package installed",
|
|
181
|
+
installed,
|
|
182
|
+
package_detail,
|
|
183
|
+
),
|
|
184
|
+
_check(
|
|
185
|
+
"chat",
|
|
186
|
+
"Active chat model",
|
|
187
|
+
bool(chat_model),
|
|
188
|
+
chat_model or "Configure one in the model catalog.",
|
|
189
|
+
),
|
|
190
|
+
_check(
|
|
191
|
+
"embedding",
|
|
192
|
+
"Active embedding model",
|
|
193
|
+
bool(emb_model) and emb_dim > 0,
|
|
194
|
+
f"{emb_model} · {emb_dim}d" if emb_model else "Configure one in the model catalog.",
|
|
195
|
+
),
|
|
196
|
+
_check(
|
|
197
|
+
"vision",
|
|
198
|
+
"Vision model for multimodal",
|
|
199
|
+
vision_ok,
|
|
200
|
+
"Active chat model supports vision."
|
|
201
|
+
if vision_ok
|
|
202
|
+
else "Active chat model has no vision — image analysis is disabled.",
|
|
203
|
+
optional=True,
|
|
204
|
+
),
|
|
205
|
+
]
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _lightrag_server_preflight() -> dict:
|
|
210
|
+
try:
|
|
211
|
+
from deeptutor.services.config import load_lightrag_server_settings
|
|
212
|
+
|
|
213
|
+
settings = load_lightrag_server_settings()
|
|
214
|
+
server_url = str(settings.get("server_url") or "").strip()
|
|
215
|
+
except Exception:
|
|
216
|
+
server_url = ""
|
|
217
|
+
return _finalize(
|
|
218
|
+
[
|
|
219
|
+
_check(
|
|
220
|
+
"server_url",
|
|
221
|
+
"Default server configured",
|
|
222
|
+
bool(server_url),
|
|
223
|
+
server_url or "Add a server URL under Connection defaults.",
|
|
224
|
+
)
|
|
225
|
+
]
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _ima_preflight() -> dict:
|
|
230
|
+
try:
|
|
231
|
+
from .pipelines.ima.config import get_account_credentials
|
|
232
|
+
|
|
233
|
+
credentials = get_account_credentials()
|
|
234
|
+
except Exception:
|
|
235
|
+
from .pipelines.ima.config import ImaCredentials
|
|
236
|
+
|
|
237
|
+
credentials = ImaCredentials()
|
|
238
|
+
return _finalize(
|
|
239
|
+
[
|
|
240
|
+
_check(
|
|
241
|
+
"credentials",
|
|
242
|
+
"IMA Client ID and API key configured",
|
|
243
|
+
credentials.complete,
|
|
244
|
+
credentials.client_id
|
|
245
|
+
if credentials.complete
|
|
246
|
+
else "Add them under Credentials, or supply a pair per knowledge base "
|
|
247
|
+
"when connecting one.",
|
|
248
|
+
)
|
|
249
|
+
]
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _finalize(checks: list[dict]) -> dict:
|
|
254
|
+
ok = all(c["ok"] for c in checks if not c["optional"])
|
|
255
|
+
return {"ok": ok, "checks": checks}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
_PREFLIGHTS = {
|
|
259
|
+
DEFAULT_PROVIDER: _llamaindex_preflight,
|
|
260
|
+
PAGEINDEX_PROVIDER: _pageindex_preflight,
|
|
261
|
+
PAGEINDEX_OSS_PROVIDER: _pageindex_oss_preflight,
|
|
262
|
+
GRAPHRAG_PROVIDER: _graphrag_preflight,
|
|
263
|
+
LIGHTRAG_PROVIDER: _lightrag_preflight,
|
|
264
|
+
LIGHTRAG_SERVER_PROVIDER: _lightrag_server_preflight,
|
|
265
|
+
IMA_PROVIDER: _ima_preflight,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def engine_preflight(provider: str) -> dict[str, Any]:
|
|
270
|
+
"""Run the requirement checks for ``provider`` and return the report."""
|
|
271
|
+
return _PREFLIGHTS[normalize_provider_name(provider)]()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
__all__ = ["engine_preflight"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Resolve a knowledge base's bound RAG provider.
|
|
2
|
+
|
|
3
|
+
DeepTutor owns the knowledge-base lifecycle and stores the authoritative
|
|
4
|
+
provider binding in ``kb_config.json``. Older KBs may only have
|
|
5
|
+
``metadata.json``, so metadata remains a legacy fallback. Retrieval and
|
|
6
|
+
incremental indexing should use this helper instead of hand-reading either file
|
|
7
|
+
directly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from deeptutor.services.rag.factory import DEFAULT_PROVIDER, normalize_provider_name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_kb_config_entry(kb_base_dir: str | Path, kb_name: str) -> dict[str, Any]:
|
|
20
|
+
"""Return the raw ``kb_config.json`` entry for ``kb_name``, if present."""
|
|
21
|
+
config_path = Path(kb_base_dir) / "kb_config.json"
|
|
22
|
+
if not config_path.exists():
|
|
23
|
+
return {}
|
|
24
|
+
try:
|
|
25
|
+
with open(config_path, encoding="utf-8") as handle:
|
|
26
|
+
entry = json.load(handle).get("knowledge_bases", {}).get(kb_name, {})
|
|
27
|
+
return entry if isinstance(entry, dict) else {}
|
|
28
|
+
except Exception:
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_metadata_provider(kb_base_dir: str | Path, kb_name: str) -> str | None:
|
|
33
|
+
"""Return the provider stored in legacy ``metadata.json``, if present."""
|
|
34
|
+
metadata_path = Path(kb_base_dir) / kb_name / "metadata.json"
|
|
35
|
+
if not metadata_path.exists():
|
|
36
|
+
return None
|
|
37
|
+
try:
|
|
38
|
+
with open(metadata_path, encoding="utf-8") as handle:
|
|
39
|
+
provider = json.load(handle).get("rag_provider")
|
|
40
|
+
except Exception:
|
|
41
|
+
return None
|
|
42
|
+
return normalize_provider_name(provider) if provider else None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_bound_provider(kb_base_dir: str | Path, kb_name: str | None) -> str:
|
|
46
|
+
"""Resolve the provider DeepTutor has bound to a KB.
|
|
47
|
+
|
|
48
|
+
Order:
|
|
49
|
+
1. ``kb_config.json``: authoritative runtime state.
|
|
50
|
+
2. ``metadata.json``: legacy/import fallback.
|
|
51
|
+
3. default provider.
|
|
52
|
+
"""
|
|
53
|
+
if kb_name:
|
|
54
|
+
entry = load_kb_config_entry(kb_base_dir, kb_name)
|
|
55
|
+
provider = entry.get("rag_provider")
|
|
56
|
+
if provider:
|
|
57
|
+
return normalize_provider_name(provider)
|
|
58
|
+
|
|
59
|
+
metadata_provider = load_metadata_provider(kb_base_dir, kb_name)
|
|
60
|
+
if metadata_provider:
|
|
61
|
+
return metadata_provider
|
|
62
|
+
|
|
63
|
+
return DEFAULT_PROVIDER
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = [
|
|
67
|
+
"load_kb_config_entry",
|
|
68
|
+
"load_metadata_provider",
|
|
69
|
+
"resolve_bound_provider",
|
|
70
|
+
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Single-query rewrite for retrieval.
|
|
2
|
+
|
|
3
|
+
Rewrites a conversational or anaphoric user query into one self-contained,
|
|
4
|
+
keyword-leaning search query before it reaches the KB pipeline. The layer is
|
|
5
|
+
fail-open by design: any LLM error, timeout, or empty output falls back to
|
|
6
|
+
the original query so retrieval never fails because of a rewrite.
|
|
7
|
+
|
|
8
|
+
Only the ``standalone`` strategy exists in v1 (one LLM call, one query).
|
|
9
|
+
``multi_query`` is reserved for a follow-up PR; the settings schema already
|
|
10
|
+
carries the ``strategy`` field so no migration is needed later.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
from collections.abc import Awaitable, Callable
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
import logging
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any, Optional
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
#: Rewrite is disabled / was forced off.
|
|
25
|
+
STRATEGY_OFF = "off"
|
|
26
|
+
#: Single self-contained query produced by one LLM call.
|
|
27
|
+
STRATEGY_STANDALONE = "standalone"
|
|
28
|
+
|
|
29
|
+
#: Strategies ``maybe_rewrite`` accepts (``off`` forces the pass-through).
|
|
30
|
+
KNOWN_STRATEGIES = (STRATEGY_OFF, STRATEGY_STANDALONE)
|
|
31
|
+
|
|
32
|
+
_SYSTEM_PROMPT = (
|
|
33
|
+
"You rewrite a user question into a single self-contained search query "
|
|
34
|
+
"for a document retrieval engine. Keep the original language (never "
|
|
35
|
+
"translate). Resolve pronouns and conversational context into explicit "
|
|
36
|
+
"keywords. Output ONLY the rewritten query, no quotes, no explanation."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class RewriteOutcome:
|
|
42
|
+
"""What the rewrite layer did to one query."""
|
|
43
|
+
|
|
44
|
+
original: str
|
|
45
|
+
rewritten: str
|
|
46
|
+
strategy: str
|
|
47
|
+
latency_ms: float = 0.0
|
|
48
|
+
error: str = ""
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def applied(self) -> bool:
|
|
52
|
+
"""True when the query sent to the engine differs from the input."""
|
|
53
|
+
return bool(self.rewritten) and self.rewritten != self.original
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict[str, Any]:
|
|
56
|
+
"""Serializable form attached to search results and eval reports."""
|
|
57
|
+
return {
|
|
58
|
+
"original": self.original,
|
|
59
|
+
"rewritten": self.rewritten,
|
|
60
|
+
"strategy": self.strategy,
|
|
61
|
+
"latency_ms": round(self.latency_ms, 1),
|
|
62
|
+
"error": self.error,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _clean_rewritten(raw: str) -> str:
|
|
67
|
+
"""Take the first non-empty line of LLM output, minus wrapping quotes."""
|
|
68
|
+
for line in (raw or "").splitlines():
|
|
69
|
+
text = line.strip().strip("\"'“”‘’")
|
|
70
|
+
if text:
|
|
71
|
+
return text
|
|
72
|
+
return ""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def rewrite_query(
|
|
76
|
+
query: str,
|
|
77
|
+
*,
|
|
78
|
+
settings: dict[str, Any] | None = None,
|
|
79
|
+
complete_fn: Callable[..., Awaitable[str]] | None = None,
|
|
80
|
+
) -> RewriteOutcome:
|
|
81
|
+
"""Rewrite ``query`` per ``settings``; always fail open to the original."""
|
|
82
|
+
original = query if isinstance(query, str) else ""
|
|
83
|
+
started = time.monotonic()
|
|
84
|
+
strategy = STRATEGY_STANDALONE
|
|
85
|
+
try:
|
|
86
|
+
cfg = settings or {}
|
|
87
|
+
strategy = str(cfg.get("strategy") or STRATEGY_STANDALONE)
|
|
88
|
+
if not bool(cfg.get("enabled", False)) or strategy == STRATEGY_OFF:
|
|
89
|
+
return RewriteOutcome(original=original, rewritten=original, strategy=STRATEGY_OFF)
|
|
90
|
+
min_chars = int(cfg.get("min_query_chars") or 0)
|
|
91
|
+
if len(original.strip()) < max(0, min_chars):
|
|
92
|
+
return RewriteOutcome(original=original, rewritten=original, strategy=strategy)
|
|
93
|
+
timeout = float(cfg.get("timeout_seconds") or 5.0)
|
|
94
|
+
model = str(cfg.get("model") or "").strip() or None
|
|
95
|
+
complete = complete_fn
|
|
96
|
+
if complete is None:
|
|
97
|
+
from deeptutor.services.llm import complete as _default_complete
|
|
98
|
+
|
|
99
|
+
complete = _default_complete
|
|
100
|
+
if model is not None:
|
|
101
|
+
raw = await asyncio.wait_for(
|
|
102
|
+
complete(query, system_prompt=_SYSTEM_PROMPT, model=model),
|
|
103
|
+
timeout=timeout,
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
raw = await asyncio.wait_for(
|
|
107
|
+
complete(query, system_prompt=_SYSTEM_PROMPT),
|
|
108
|
+
timeout=timeout,
|
|
109
|
+
)
|
|
110
|
+
rewritten = _clean_rewritten(raw)
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
logger.warning("Query rewrite failed (%s); using the original query", type(exc).__name__)
|
|
113
|
+
return RewriteOutcome(
|
|
114
|
+
original=original,
|
|
115
|
+
rewritten=original,
|
|
116
|
+
strategy=strategy,
|
|
117
|
+
latency_ms=(time.monotonic() - started) * 1000.0,
|
|
118
|
+
error=f"{type(exc).__name__}: {exc}",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
latency_ms = (time.monotonic() - started) * 1000.0
|
|
122
|
+
if not rewritten:
|
|
123
|
+
return RewriteOutcome(
|
|
124
|
+
original=original,
|
|
125
|
+
rewritten=original,
|
|
126
|
+
strategy=strategy,
|
|
127
|
+
latency_ms=latency_ms,
|
|
128
|
+
error="empty_rewrite",
|
|
129
|
+
)
|
|
130
|
+
return RewriteOutcome(
|
|
131
|
+
original=original,
|
|
132
|
+
rewritten=rewritten,
|
|
133
|
+
strategy=strategy,
|
|
134
|
+
latency_ms=latency_ms,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def maybe_rewrite(
|
|
139
|
+
query: str,
|
|
140
|
+
*,
|
|
141
|
+
rewrite: bool | None = None,
|
|
142
|
+
settings: dict[str, Any] | None = None,
|
|
143
|
+
complete_fn: Callable[..., Awaitable[str]] | None = None,
|
|
144
|
+
) -> tuple[str, Optional[RewriteOutcome]]:
|
|
145
|
+
"""Return ``(effective_query, outcome)`` for :meth:`RAGService.search`.
|
|
146
|
+
|
|
147
|
+
``rewrite`` is the per-call override: True forces the rewrite on, False
|
|
148
|
+
forces it off, None follows the runtime settings. Returns ``(query, None)``
|
|
149
|
+
when the layer is off so callers only attach provenance when active.
|
|
150
|
+
"""
|
|
151
|
+
if rewrite is False:
|
|
152
|
+
return query, None
|
|
153
|
+
try:
|
|
154
|
+
cfg = settings
|
|
155
|
+
if cfg is None:
|
|
156
|
+
from deeptutor.services.config import get_runtime_settings_service
|
|
157
|
+
|
|
158
|
+
cfg = get_runtime_settings_service().load_query_rewrite()
|
|
159
|
+
cfg = dict(cfg)
|
|
160
|
+
if rewrite is not None:
|
|
161
|
+
cfg["enabled"] = bool(rewrite)
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
logger.warning(
|
|
164
|
+
"Query rewrite settings failed (%s); using the original query", type(exc).__name__
|
|
165
|
+
)
|
|
166
|
+
return query, RewriteOutcome(
|
|
167
|
+
original=query,
|
|
168
|
+
rewritten=query,
|
|
169
|
+
strategy=STRATEGY_OFF,
|
|
170
|
+
error=f"{type(exc).__name__}: {exc}",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if not bool(cfg.get("enabled", False)):
|
|
174
|
+
return query, None
|
|
175
|
+
outcome = await rewrite_query(query, settings=cfg, complete_fn=complete_fn)
|
|
176
|
+
return outcome.rewritten, outcome
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
__all__ = [
|
|
180
|
+
"KNOWN_STRATEGIES",
|
|
181
|
+
"STRATEGY_OFF",
|
|
182
|
+
"STRATEGY_STANDALONE",
|
|
183
|
+
"RewriteOutcome",
|
|
184
|
+
"maybe_rewrite",
|
|
185
|
+
"rewrite_query",
|
|
186
|
+
]
|