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,379 @@
|
|
|
1
|
+
"""Unified RAG service entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import importlib
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import shutil
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from deeptutor.runtime.home import get_runtime_data_root
|
|
14
|
+
|
|
15
|
+
from .embedding_binding import with_kb_embedding
|
|
16
|
+
from .factory import DEFAULT_PROVIDER, get_pipeline, list_pipelines, normalize_provider_name
|
|
17
|
+
from .provider_binding import resolve_bound_provider
|
|
18
|
+
|
|
19
|
+
DEFAULT_KB_BASE_DIR = str(get_runtime_data_root() / "knowledge_bases")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RAGService:
|
|
23
|
+
"""Unified RAG service that routes to a KB's bound pipeline.
|
|
24
|
+
|
|
25
|
+
The provider is resolved per knowledge base: an explicit ``provider`` passed
|
|
26
|
+
to the constructor wins (used at create time); otherwise it is read from
|
|
27
|
+
DeepTutor's authoritative KB config, with metadata as a legacy fallback.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
kb_base_dir: Optional[str] = None,
|
|
33
|
+
provider: Optional[str] = None,
|
|
34
|
+
):
|
|
35
|
+
self.logger = logging.getLogger(__name__)
|
|
36
|
+
if kb_base_dir is None:
|
|
37
|
+
try:
|
|
38
|
+
from deeptutor.services.path_service import get_path_service
|
|
39
|
+
|
|
40
|
+
kb_base_dir = str(get_path_service().get_knowledge_bases_root())
|
|
41
|
+
except Exception:
|
|
42
|
+
self.logger.warning(
|
|
43
|
+
"RAGService falling back to DEFAULT_KB_BASE_DIR (%s); "
|
|
44
|
+
"this should only happen in single-user / CLI mode. "
|
|
45
|
+
"Multi-user requests must reach this path with an explicit kb_base_dir.",
|
|
46
|
+
DEFAULT_KB_BASE_DIR,
|
|
47
|
+
)
|
|
48
|
+
kb_base_dir = DEFAULT_KB_BASE_DIR
|
|
49
|
+
self.kb_base_dir = kb_base_dir
|
|
50
|
+
self._provider_override = normalize_provider_name(provider) if provider else None
|
|
51
|
+
# ``self.provider`` kept for callers that read it directly; the real
|
|
52
|
+
# selection happens per kb_name in ``_resolve_provider``.
|
|
53
|
+
self.provider = self._provider_override or DEFAULT_PROVIDER
|
|
54
|
+
self._pipelines: dict[str, Any] = {}
|
|
55
|
+
|
|
56
|
+
def _resolve_provider(self, kb_name: Optional[str]) -> str:
|
|
57
|
+
"""Pick the provider for ``kb_name`` from DeepTutor's binding."""
|
|
58
|
+
if self._provider_override:
|
|
59
|
+
return self._provider_override
|
|
60
|
+
return resolve_bound_provider(self.kb_base_dir, kb_name)
|
|
61
|
+
|
|
62
|
+
def _get_pipeline(self, provider: str):
|
|
63
|
+
if provider not in self._pipelines:
|
|
64
|
+
self._pipelines[provider] = get_pipeline(name=provider, kb_base_dir=self.kb_base_dir)
|
|
65
|
+
return self._pipelines[provider]
|
|
66
|
+
|
|
67
|
+
@with_kb_embedding
|
|
68
|
+
async def initialize(self, kb_name: str, file_paths: List[str], **kwargs) -> bool:
|
|
69
|
+
return await self._index(kb_name, file_paths, "initialize", kwargs)
|
|
70
|
+
|
|
71
|
+
@with_kb_embedding
|
|
72
|
+
async def add_documents(self, kb_name: str, file_paths: List[str], **kwargs) -> bool:
|
|
73
|
+
return await self._index(kb_name, file_paths, "add_documents", kwargs)
|
|
74
|
+
|
|
75
|
+
async def _index(self, kb_name, file_paths, method, kwargs):
|
|
76
|
+
from deeptutor.knowledge.indexing_run import IndexingRun, current_run
|
|
77
|
+
|
|
78
|
+
from .index_probe import latest_ready_provider_version
|
|
79
|
+
from .kb_paths import resolve_kb_dir
|
|
80
|
+
|
|
81
|
+
provider = self._resolve_provider(kb_name)
|
|
82
|
+
pipeline = self._get_pipeline(provider)
|
|
83
|
+
operation = getattr(pipeline, method, None) or pipeline.initialize
|
|
84
|
+
if current_run() is not None:
|
|
85
|
+
return await operation(kb_name=kb_name, file_paths=file_paths, **kwargs)
|
|
86
|
+
task_id = kwargs.pop("task_id", "")
|
|
87
|
+
async with IndexingRun(
|
|
88
|
+
resolve_kb_dir(self.kb_base_dir, kb_name),
|
|
89
|
+
file_paths,
|
|
90
|
+
task_id=task_id,
|
|
91
|
+
action="upload" if method == "add_documents" else "rebuild",
|
|
92
|
+
provider=provider,
|
|
93
|
+
) as run:
|
|
94
|
+
receipt_callback = kwargs.get("indexed_file_callback")
|
|
95
|
+
|
|
96
|
+
def indexed(paths):
|
|
97
|
+
for path in paths:
|
|
98
|
+
run.document(Path(path), "completed")
|
|
99
|
+
if receipt_callback:
|
|
100
|
+
receipt_callback(paths)
|
|
101
|
+
|
|
102
|
+
kwargs["indexed_file_callback"] = indexed
|
|
103
|
+
callback = kwargs.get("progress_callback")
|
|
104
|
+
|
|
105
|
+
def progress(current, total):
|
|
106
|
+
run.phase("embedding", current=current, total=total)
|
|
107
|
+
if callback:
|
|
108
|
+
callback(current, total)
|
|
109
|
+
|
|
110
|
+
kwargs["progress_callback"] = progress
|
|
111
|
+
success = await operation(kb_name=kb_name, file_paths=file_paths, **kwargs)
|
|
112
|
+
if not run.data.get("usable_version"):
|
|
113
|
+
run.check()
|
|
114
|
+
if not success:
|
|
115
|
+
return False
|
|
116
|
+
version = latest_ready_provider_version(run.kb_dir, provider)
|
|
117
|
+
if version is None:
|
|
118
|
+
raise RuntimeError("Index output did not pass the provider readiness check.")
|
|
119
|
+
partial = any(
|
|
120
|
+
doc.get("status") in {"failed", "unknown", "pending", "parsing"}
|
|
121
|
+
for doc in run.data["documents"].values()
|
|
122
|
+
)
|
|
123
|
+
run.finish("partial" if partial else "completed", version=version.get("version"))
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
@with_kb_embedding
|
|
127
|
+
async def search(
|
|
128
|
+
self,
|
|
129
|
+
query: str,
|
|
130
|
+
kb_name: str,
|
|
131
|
+
event_sink=None,
|
|
132
|
+
rewrite: bool | None = None,
|
|
133
|
+
**kwargs,
|
|
134
|
+
) -> Dict[str, Any]:
|
|
135
|
+
provider = self._resolve_provider(kb_name)
|
|
136
|
+
from .factory import PAGEINDEX_OSS_PROVIDER, PAGEINDEX_PROVIDER
|
|
137
|
+
|
|
138
|
+
if provider in {PAGEINDEX_PROVIDER, PAGEINDEX_OSS_PROVIDER}:
|
|
139
|
+
message = (
|
|
140
|
+
"PageIndex uses Reasoning as Retrieval. Read this knowledge base with "
|
|
141
|
+
"its PageIndex tools inside an agent loop instead of RAGService.search()."
|
|
142
|
+
)
|
|
143
|
+
return {
|
|
144
|
+
"query": query,
|
|
145
|
+
"answer": message,
|
|
146
|
+
"content": "",
|
|
147
|
+
"sources": [],
|
|
148
|
+
"provider": provider,
|
|
149
|
+
"error_type": "reasoning_as_retrieval_required",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Query rewrite layer (fail-open). ``maybe_rewrite`` reads runtime
|
|
153
|
+
# settings when ``rewrite`` is None, honors an explicit True/False
|
|
154
|
+
# override, and returns ``(original, None)`` when the layer is off, so
|
|
155
|
+
# provenance is only attached when a rewrite actually ran.
|
|
156
|
+
from deeptutor.services.rag.query_rewrite import maybe_rewrite
|
|
157
|
+
|
|
158
|
+
effective_query, rewrite_outcome = await maybe_rewrite(query, rewrite=rewrite)
|
|
159
|
+
if rewrite_outcome is not None:
|
|
160
|
+
await self._emit_tool_event(
|
|
161
|
+
event_sink,
|
|
162
|
+
"status",
|
|
163
|
+
f"Rewritten query: {rewrite_outcome.rewritten}",
|
|
164
|
+
{
|
|
165
|
+
"query": query,
|
|
166
|
+
"rewrite_outcome": rewrite_outcome.to_dict(),
|
|
167
|
+
"trace_layer": "summary",
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
with self._capture_raw_logs(event_sink):
|
|
172
|
+
await self._emit_tool_event(
|
|
173
|
+
event_sink,
|
|
174
|
+
"status",
|
|
175
|
+
f"Query: {query}",
|
|
176
|
+
{"query": query, "kb_name": kb_name, "trace_layer": "summary"},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
self.logger.info(f"Searching KB '{kb_name}' with query: {query[:50]}...")
|
|
180
|
+
pipeline = self._get_pipeline(provider)
|
|
181
|
+
|
|
182
|
+
await self._emit_tool_event(
|
|
183
|
+
event_sink,
|
|
184
|
+
"status",
|
|
185
|
+
f"Retrieving from knowledge base '{kb_name}'...",
|
|
186
|
+
{"provider": provider, "trace_layer": "summary"},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
result = await pipeline.search(query=effective_query, kb_name=kb_name, **kwargs)
|
|
190
|
+
|
|
191
|
+
if "query" not in result:
|
|
192
|
+
result["query"] = query
|
|
193
|
+
if "answer" not in result and "content" in result:
|
|
194
|
+
result["answer"] = result["content"]
|
|
195
|
+
if "content" not in result and "answer" in result:
|
|
196
|
+
result["content"] = result["answer"]
|
|
197
|
+
# The service is authoritative about which engine ran (resolved from
|
|
198
|
+
# the KB's binding), so it overwrites whatever the pipeline reports.
|
|
199
|
+
result["provider"] = provider
|
|
200
|
+
if rewrite_outcome is not None:
|
|
201
|
+
result["query_rewrite"] = rewrite_outcome.to_dict()
|
|
202
|
+
|
|
203
|
+
if result.get("error_type") or result.get("needs_reindex"):
|
|
204
|
+
await self._emit_tool_event(
|
|
205
|
+
event_sink,
|
|
206
|
+
"status",
|
|
207
|
+
result.get("answer") or result.get("content") or "RAG search failed.",
|
|
208
|
+
{
|
|
209
|
+
"provider": provider,
|
|
210
|
+
"kb_name": kb_name,
|
|
211
|
+
"trace_layer": "summary",
|
|
212
|
+
"call_state": "error",
|
|
213
|
+
"error_type": result.get("error_type"),
|
|
214
|
+
"needs_reindex": bool(result.get("needs_reindex")),
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
answer = result.get("answer") or result.get("content") or ""
|
|
220
|
+
await self._emit_tool_event(
|
|
221
|
+
event_sink,
|
|
222
|
+
"status",
|
|
223
|
+
f"Retrieved {len(answer)} characters of grounded context.",
|
|
224
|
+
{
|
|
225
|
+
"provider": provider,
|
|
226
|
+
"kb_name": kb_name,
|
|
227
|
+
"trace_layer": "summary",
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# L1 memory trace — best-effort, never blocks the search path.
|
|
232
|
+
try:
|
|
233
|
+
from deeptutor.services.memory import get_memory_store
|
|
234
|
+
from deeptutor.services.memory.trace import TraceEvent
|
|
235
|
+
|
|
236
|
+
await get_memory_store().emit(
|
|
237
|
+
TraceEvent.new(
|
|
238
|
+
"kb",
|
|
239
|
+
"query",
|
|
240
|
+
{
|
|
241
|
+
"query": query,
|
|
242
|
+
"kb_name": kb_name,
|
|
243
|
+
"answer_chars": len(answer),
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
async def _emit_tool_event(
|
|
253
|
+
self,
|
|
254
|
+
event_sink,
|
|
255
|
+
event_type: str,
|
|
256
|
+
message: str,
|
|
257
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
258
|
+
) -> None:
|
|
259
|
+
if event_sink is None:
|
|
260
|
+
return
|
|
261
|
+
await event_sink(event_type, message, metadata or {})
|
|
262
|
+
|
|
263
|
+
def _capture_raw_logs(self, event_sink):
|
|
264
|
+
if event_sink is None:
|
|
265
|
+
return contextlib.nullcontext()
|
|
266
|
+
|
|
267
|
+
from deeptutor.logging import ProcessLogEvent, capture_process_logs
|
|
268
|
+
from deeptutor.logging.process_stream import ProcessLogHandler
|
|
269
|
+
|
|
270
|
+
def should_skip_noisy_retrieve_log(event: ProcessLogEvent) -> bool:
|
|
271
|
+
if event.level != "INFO":
|
|
272
|
+
return False
|
|
273
|
+
message = event.message.strip()
|
|
274
|
+
logger_name = event.logger
|
|
275
|
+
if logger_name == "nano-vectordb" and (
|
|
276
|
+
message.startswith("Load ") or message.startswith("Init ")
|
|
277
|
+
):
|
|
278
|
+
return True
|
|
279
|
+
if (
|
|
280
|
+
logger_name.startswith("deeptutor.services.embedding.")
|
|
281
|
+
and (
|
|
282
|
+
message.startswith("Successfully generated ")
|
|
283
|
+
or message.startswith("Generated ")
|
|
284
|
+
)
|
|
285
|
+
and "embedding" in message.lower()
|
|
286
|
+
):
|
|
287
|
+
return True
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
def emit(event):
|
|
291
|
+
if should_skip_noisy_retrieve_log(event):
|
|
292
|
+
return None
|
|
293
|
+
return self._emit_tool_event(
|
|
294
|
+
event_sink,
|
|
295
|
+
"raw_log",
|
|
296
|
+
event.message,
|
|
297
|
+
{
|
|
298
|
+
"level": event.level,
|
|
299
|
+
"logger": event.logger,
|
|
300
|
+
"timestamp": event.timestamp,
|
|
301
|
+
"trace_layer": "raw",
|
|
302
|
+
**event.context,
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
@contextlib.contextmanager
|
|
307
|
+
def capture_non_propagating_logs():
|
|
308
|
+
handlers: list[tuple[logging.Logger, logging.Handler]] = []
|
|
309
|
+
for logger_name in ("lightrag", "graphrag", "graphrag_llm"):
|
|
310
|
+
if logger_name == "lightrag":
|
|
311
|
+
with contextlib.suppress(Exception):
|
|
312
|
+
importlib.import_module("lightrag.utils")
|
|
313
|
+
source_logger = logging.getLogger(logger_name)
|
|
314
|
+
if source_logger.propagate:
|
|
315
|
+
continue
|
|
316
|
+
handler = ProcessLogHandler(emit, min_level=logging.INFO)
|
|
317
|
+
source_logger.addHandler(handler)
|
|
318
|
+
handlers.append((source_logger, handler))
|
|
319
|
+
try:
|
|
320
|
+
yield
|
|
321
|
+
finally:
|
|
322
|
+
for source_logger, handler in handlers:
|
|
323
|
+
if handler in source_logger.handlers:
|
|
324
|
+
source_logger.removeHandler(handler)
|
|
325
|
+
handler.close()
|
|
326
|
+
|
|
327
|
+
@contextlib.contextmanager
|
|
328
|
+
def capture_all_raw_logs():
|
|
329
|
+
with capture_process_logs(emit, min_level=logging.INFO):
|
|
330
|
+
with capture_non_propagating_logs():
|
|
331
|
+
yield
|
|
332
|
+
|
|
333
|
+
return capture_all_raw_logs()
|
|
334
|
+
|
|
335
|
+
async def delete(self, kb_name: str) -> bool:
|
|
336
|
+
provider = self._resolve_provider(kb_name)
|
|
337
|
+
self.logger.info(f"Deleting KB '{kb_name}' (provider={provider})")
|
|
338
|
+
pipeline = self._get_pipeline(provider)
|
|
339
|
+
|
|
340
|
+
if hasattr(pipeline, "delete"):
|
|
341
|
+
return await pipeline.delete(kb_name=kb_name)
|
|
342
|
+
|
|
343
|
+
kb_dir = Path(self.kb_base_dir) / kb_name
|
|
344
|
+
if kb_dir.exists():
|
|
345
|
+
shutil.rmtree(kb_dir)
|
|
346
|
+
self.logger.info(f"Deleted KB directory: {kb_dir}")
|
|
347
|
+
return True
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
async def smart_retrieve(
|
|
351
|
+
self,
|
|
352
|
+
context: str,
|
|
353
|
+
kb_name: str,
|
|
354
|
+
query_hints: Optional[List[str]] = None,
|
|
355
|
+
max_queries: int = 3,
|
|
356
|
+
) -> Dict[str, Any]:
|
|
357
|
+
from .smart_retriever import SmartRetriever
|
|
358
|
+
|
|
359
|
+
return await SmartRetriever(self.search).retrieve(
|
|
360
|
+
context=context,
|
|
361
|
+
kb_name=kb_name,
|
|
362
|
+
query_hints=query_hints,
|
|
363
|
+
max_queries=max_queries,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def list_providers() -> List[Dict[str, str]]:
|
|
368
|
+
return list_pipelines()
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def get_current_provider() -> str:
|
|
372
|
+
# Global default; per-KB selection happens in ``_resolve_provider``.
|
|
373
|
+
return normalize_provider_name(os.getenv("RAG_PROVIDER"))
|
|
374
|
+
|
|
375
|
+
@staticmethod
|
|
376
|
+
def has_provider(name: str) -> bool:
|
|
377
|
+
from .factory import KNOWN_PROVIDERS
|
|
378
|
+
|
|
379
|
+
return (name or "").strip().lower() in KNOWN_PROVIDERS
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Higher-level multi-query retrieval helpers built on top of RAGService."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
SearchFunc = Callable[..., Awaitable[Dict[str, Any]]]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SmartRetriever:
|
|
13
|
+
"""Generate query variants, retrieve passages, and aggregate them."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, search: SearchFunc):
|
|
16
|
+
self._search = search
|
|
17
|
+
|
|
18
|
+
async def retrieve(
|
|
19
|
+
self,
|
|
20
|
+
context: str,
|
|
21
|
+
kb_name: str,
|
|
22
|
+
query_hints: Optional[List[str]] = None,
|
|
23
|
+
max_queries: int = 3,
|
|
24
|
+
) -> Dict[str, Any]:
|
|
25
|
+
queries = query_hints if query_hints else await self._generate_queries(context, max_queries)
|
|
26
|
+
results = await asyncio.gather(
|
|
27
|
+
*(self._search(query=q, kb_name=kb_name) for q in queries),
|
|
28
|
+
return_exceptions=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
passages: list[str] = []
|
|
32
|
+
all_sources: list[dict] = []
|
|
33
|
+
for result in results:
|
|
34
|
+
if isinstance(result, Exception):
|
|
35
|
+
continue
|
|
36
|
+
content = result.get("content") or result.get("answer") or ""
|
|
37
|
+
if content:
|
|
38
|
+
passages.append(content)
|
|
39
|
+
all_sources.append(
|
|
40
|
+
{"query": result.get("query", ""), "provider": result.get("provider", "")}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if not passages:
|
|
44
|
+
return {"answer": "", "sources": []}
|
|
45
|
+
|
|
46
|
+
aggregated = await self._aggregate(context, passages)
|
|
47
|
+
return {"answer": aggregated, "sources": all_sources}
|
|
48
|
+
|
|
49
|
+
async def _generate_queries(self, context: str, n: int) -> list[str]:
|
|
50
|
+
try:
|
|
51
|
+
from deeptutor.services.llm import complete
|
|
52
|
+
|
|
53
|
+
prompt = (
|
|
54
|
+
f"Generate {n} diverse search queries to retrieve information relevant "
|
|
55
|
+
f"to the following context. Return ONLY the queries, one per line.\n\n"
|
|
56
|
+
f"Context:\n{context[:2000]}"
|
|
57
|
+
)
|
|
58
|
+
raw = await complete(prompt, system_prompt="You are a search query generator.")
|
|
59
|
+
lines = [
|
|
60
|
+
line.strip().lstrip("0123456789.-) ")
|
|
61
|
+
for line in raw.strip().split("\n")
|
|
62
|
+
if line.strip()
|
|
63
|
+
]
|
|
64
|
+
return lines[:n] if lines else [context[:200]]
|
|
65
|
+
except Exception:
|
|
66
|
+
return [context[:200]]
|
|
67
|
+
|
|
68
|
+
async def _aggregate(self, context: str, passages: list[str]) -> str:
|
|
69
|
+
try:
|
|
70
|
+
from deeptutor.services.llm import complete
|
|
71
|
+
|
|
72
|
+
combined = "\n---\n".join(passages)
|
|
73
|
+
prompt = (
|
|
74
|
+
"Synthesise the following retrieved passages into a concise, "
|
|
75
|
+
"relevant summary for the given context.\n\n"
|
|
76
|
+
f"Context:\n{context[:1000]}\n\n"
|
|
77
|
+
f"Passages:\n{combined[:6000]}"
|
|
78
|
+
)
|
|
79
|
+
return await complete(prompt, system_prompt="You are a knowledge synthesiser.")
|
|
80
|
+
except Exception:
|
|
81
|
+
return "\n\n".join(passages)
|