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,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)