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,309 @@
1
+ """Run an evaluation set against a knowledge base and score its retrieval.
2
+
3
+ The runner drives the same search path a chat turn uses (``rag_search`` →
4
+ :class:`~deeptutor.services.rag.service.RAGService` → the KB's bound pipeline),
5
+ so a score reflects production retrieval rather than a re-implementation of it.
6
+ One case is one search; the ranked citations it returns are matched against the
7
+ case's gold passages and scored at the configured cutoff.
8
+
9
+ A case that cannot be scored — the engine raised, or it answered with an error
10
+ envelope (a missing index, a credentials failure) — is recorded as failed with
11
+ its message. Failed cases are excluded from the aggregate so an unreachable
12
+ engine reports "0 of 8 scored" instead of a misleadingly low quality score.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Awaitable, Callable, Mapping
18
+ from dataclasses import dataclass
19
+ import logging
20
+ import time
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ from .dataset import EvalCase, EvalDataset
24
+ from .matching import MatchPolicy, RelevanceMatcher, source_text
25
+ from .metrics import QueryMetrics, evaluate_hits
26
+
27
+ if TYPE_CHECKING: # ``report`` imports this module's result type.
28
+ from .report import EvalReport
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ #: Search callable shaped like :func:`deeptutor.tools.rag_tool.rag_search`.
33
+ SearchFn = Callable[..., Awaitable[dict[str, Any]]]
34
+ #: Progress hook: ``(index, total, evaluation)`` after each case.
35
+ ProgressFn = Callable[[int, int, "QueryEvaluation"], None]
36
+
37
+ #: The engine returned ranked citations.
38
+ SOURCE_MODE_CITATIONS = "sources"
39
+ #: The engine returned context but no citations; the context is scored as one span.
40
+ SOURCE_MODE_CONTEXT = "content"
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class QueryEvaluation:
45
+ """One case's retrieval result, its ranked hits, and its metrics."""
46
+
47
+ case_id: str
48
+ query: str
49
+ k: int
50
+ gold_count: int
51
+ hits: tuple[frozenset[int], ...]
52
+ metrics: QueryMetrics
53
+ source_mode: str = SOURCE_MODE_CITATIONS
54
+ provider: str = ""
55
+ error: str = ""
56
+ sources: tuple[dict[str, Any], ...] = ()
57
+ query_rewrite: dict[str, Any] | None = None
58
+
59
+ @property
60
+ def failed(self) -> bool:
61
+ """True when the case could not be scored (error or failed engine)."""
62
+ return bool(self.error)
63
+
64
+ def to_dict(self) -> dict[str, Any]:
65
+ """Serializable form, including which gold passages were covered."""
66
+ return {
67
+ "case_id": self.case_id,
68
+ "query": self.query,
69
+ "error": self.error,
70
+ "provider": self.provider,
71
+ "source_mode": self.source_mode,
72
+ "matched_gold": [sorted(indices) for indices in self.hits],
73
+ "query_rewrite": self.query_rewrite,
74
+ **self.metrics.to_dict(),
75
+ }
76
+
77
+
78
+ def _extract_sources(result: Mapping[str, Any]) -> tuple[list[dict[str, Any]], str]:
79
+ """Pull the ranked citations out of a search result.
80
+
81
+ Engines that offload retrieval (LightRAG server, WeKnora, IMA) and the local
82
+ ones all normalize to ``sources``. An engine that answers with context but no
83
+ citations is scored on that context as a single span, so the case still says
84
+ whether the material came back at all.
85
+ """
86
+ raw = result.get("sources")
87
+ if isinstance(raw, list):
88
+ records = [item for item in raw if isinstance(item, Mapping)]
89
+ if records:
90
+ return [dict(record) for record in records], SOURCE_MODE_CITATIONS
91
+
92
+ content = result.get("content") or result.get("answer") or ""
93
+ if isinstance(content, str) and content.strip():
94
+ return [{"content": content}], SOURCE_MODE_CONTEXT
95
+ return [], SOURCE_MODE_CITATIONS
96
+
97
+
98
+ def _error_message(result: Mapping[str, Any]) -> str:
99
+ """The engine's own failure text, or an empty string when the search worked."""
100
+ if result.get("needs_reindex"):
101
+ message = (
102
+ result.get("answer") or result.get("content") or "Knowledge base needs re-indexing."
103
+ )
104
+ return str(message)
105
+ if result.get("error_type"):
106
+ message = result.get("answer") or result.get("content") or result["error_type"]
107
+ return str(message)
108
+ if result.get("error"):
109
+ return str(result["error"])
110
+ return ""
111
+
112
+
113
+ async def _default_search(
114
+ query: str,
115
+ *,
116
+ kb_name: str,
117
+ kb_base_dir: str | None,
118
+ mode: str | None,
119
+ top_k: int,
120
+ rewrite: bool | None = None,
121
+ ) -> dict[str, Any]:
122
+ """Retrieve through the production path (``rag_search`` → the bound pipeline)."""
123
+ from deeptutor.tools.rag_tool import rag_search
124
+
125
+ kwargs: dict[str, Any] = {"top_k": top_k}
126
+ if mode:
127
+ kwargs["mode"] = mode
128
+ if rewrite is not None:
129
+ kwargs["rewrite"] = rewrite
130
+ return await rag_search(query=query, kb_name=kb_name, kb_base_dir=kb_base_dir, **kwargs)
131
+
132
+
133
+ class RetrievalEvaluator:
134
+ """Score a knowledge base against an evaluation set.
135
+
136
+ Args:
137
+ kb_name: The knowledge base to search.
138
+ kb_base_dir: KB root to search; ``None`` resolves the caller's workspace.
139
+ k: Rank cutoff for every metric.
140
+ mode: Search mode for engines that support one (e.g. ``hybrid``);
141
+ ``None`` leaves each engine on its own default.
142
+ query_limit: Evaluate only the first N cases.
143
+ policy: Gold-matching thresholds; defaults to :class:`MatchPolicy`.
144
+ search_fn: Search callable override, for tests and embedders.
145
+ provider: Provider label for the report; a search result's own
146
+ ``provider`` still wins per case.
147
+ rewrite: Force the query-rewrite layer on (``True``), off (``False``),
148
+ or defer to the runtime ``query_rewrite`` setting (``None``, the
149
+ default). Passed through to :func:`deeptutor.tools.rag_tool.rag_search`.
150
+
151
+ Raises:
152
+ ValueError: ``kb_name`` is empty or ``k`` is below 1.
153
+ """
154
+
155
+ def __init__(
156
+ self,
157
+ *,
158
+ kb_name: str,
159
+ kb_base_dir: str | None = None,
160
+ k: int = 5,
161
+ mode: str | None = None,
162
+ query_limit: int | None = None,
163
+ policy: MatchPolicy | None = None,
164
+ search_fn: SearchFn | None = None,
165
+ provider: str = "",
166
+ rewrite: bool | None = None,
167
+ ) -> None:
168
+ self.kb_name = (kb_name or "").strip()
169
+ if not self.kb_name:
170
+ raise ValueError("kb_name must be a non-empty string")
171
+ self.k = int(k)
172
+ if self.k < 1:
173
+ raise ValueError("k must be >= 1")
174
+ self.kb_base_dir = kb_base_dir
175
+ self.mode = mode or None
176
+ self.query_limit = query_limit if query_limit and query_limit > 0 else None
177
+ self.policy = policy or MatchPolicy()
178
+ self.search_fn: SearchFn = search_fn or _default_search
179
+ self.provider = provider
180
+ self.rewrite = rewrite
181
+
182
+ async def evaluate(
183
+ self,
184
+ dataset: EvalDataset,
185
+ *,
186
+ progress: ProgressFn | None = None,
187
+ ) -> EvalReport:
188
+ """Score every case and return the report defined in ``...eval.report``."""
189
+ from .report import EvalReport
190
+
191
+ cases = dataset.limited(self.query_limit).cases
192
+ started = time.monotonic()
193
+ evaluations: list[QueryEvaluation] = []
194
+ for index, case in enumerate(cases, start=1):
195
+ evaluation = await self.evaluate_case(case)
196
+ evaluations.append(evaluation)
197
+ if evaluation.failed:
198
+ logger.warning(
199
+ "Evaluation case %s could not be scored: %s", case.id, evaluation.error
200
+ )
201
+ if progress is not None:
202
+ progress(index, len(cases), evaluation)
203
+
204
+ return EvalReport(
205
+ dataset_name=dataset.name,
206
+ kb_name=self.kb_name,
207
+ provider=self.provider,
208
+ k=self.k,
209
+ mode=self.mode,
210
+ policy=self.policy,
211
+ evaluations=tuple(evaluations),
212
+ duration_seconds=time.monotonic() - started,
213
+ )
214
+
215
+ async def evaluate_case(self, case: EvalCase) -> QueryEvaluation:
216
+ """Search one case and score the citations it came back with."""
217
+ matcher = RelevanceMatcher(case.gold, self.policy)
218
+ try:
219
+ search_kwargs: dict[str, Any] = {
220
+ "query": case.query,
221
+ "kb_name": self.kb_name,
222
+ "kb_base_dir": self.kb_base_dir,
223
+ "mode": self.mode,
224
+ "top_k": self.k,
225
+ }
226
+ if self.rewrite is not None:
227
+ search_kwargs["rewrite"] = self.rewrite
228
+ result = await self.search_fn(**search_kwargs)
229
+ except Exception as exc:
230
+ return self._failure(case, matcher, f"{type(exc).__name__}: {exc}")
231
+
232
+ if not isinstance(result, Mapping):
233
+ kind = type(result).__name__
234
+ return self._failure(case, matcher, f"unexpected search result {kind}")
235
+
236
+ error = _error_message(result)
237
+ if error:
238
+ return self._failure(case, matcher, error)
239
+
240
+ sources, source_mode = _extract_sources(result)
241
+ hits = tuple(
242
+ frozenset(matcher.match(source_text(source)).indices) for source in sources[: self.k]
243
+ )
244
+ provenance = result.get("query_rewrite")
245
+ return QueryEvaluation(
246
+ case_id=case.id,
247
+ query=case.query,
248
+ k=self.k,
249
+ gold_count=matcher.gold_count,
250
+ hits=hits,
251
+ metrics=evaluate_hits(case.id, hits, matcher.gold_count, self.k),
252
+ source_mode=source_mode,
253
+ provider=str(result.get("provider") or self.provider),
254
+ sources=tuple(sources),
255
+ query_rewrite=dict(provenance) if isinstance(provenance, Mapping) else None,
256
+ )
257
+
258
+ def _failure(self, case: EvalCase, matcher: RelevanceMatcher, message: str) -> QueryEvaluation:
259
+ """Record a case that could not be scored, with zeroed metrics."""
260
+ return QueryEvaluation(
261
+ case_id=case.id,
262
+ query=case.query,
263
+ k=self.k,
264
+ gold_count=matcher.gold_count,
265
+ hits=(),
266
+ metrics=evaluate_hits(case.id, (), matcher.gold_count, self.k),
267
+ provider=self.provider,
268
+ error=message,
269
+ )
270
+
271
+
272
+ async def evaluate_dataset(
273
+ dataset: EvalDataset,
274
+ *,
275
+ kb_name: str,
276
+ kb_base_dir: str | None = None,
277
+ k: int = 5,
278
+ mode: str | None = None,
279
+ query_limit: int | None = None,
280
+ policy: MatchPolicy | None = None,
281
+ search_fn: SearchFn | None = None,
282
+ provider: str = "",
283
+ rewrite: bool | None = None,
284
+ progress: ProgressFn | None = None,
285
+ ) -> EvalReport:
286
+ """Score one dataset against one knowledge base (see :class:`RetrievalEvaluator`)."""
287
+ evaluator = RetrievalEvaluator(
288
+ kb_name=kb_name,
289
+ kb_base_dir=kb_base_dir,
290
+ k=k,
291
+ mode=mode,
292
+ query_limit=query_limit,
293
+ policy=policy,
294
+ search_fn=search_fn,
295
+ provider=provider,
296
+ rewrite=rewrite,
297
+ )
298
+ return await evaluator.evaluate(dataset, progress=progress)
299
+
300
+
301
+ __all__ = [
302
+ "ProgressFn",
303
+ "QueryEvaluation",
304
+ "RetrievalEvaluator",
305
+ "SearchFn",
306
+ "SOURCE_MODE_CITATIONS",
307
+ "SOURCE_MODE_CONTEXT",
308
+ "evaluate_dataset",
309
+ ]
@@ -0,0 +1,369 @@
1
+ """RAG pipeline factory.
2
+
3
+ Selects a KB's index/retrieve engine by provider name. Three pipelines ship
4
+ today:
5
+
6
+ * ``llamaindex`` (default) — local vector retrieval with hybrid BM25 fusion.
7
+ * ``pageindex`` — PageIndex Cloud (deployment credential + MCP tools).
8
+ * ``pageindex-oss`` — local PageIndex library using the active chat LLM.
9
+ * ``graphrag`` — local knowledge-graph retrieval (microsoft/graphrag);
10
+ optional dependency, ``pip install 'deeptutor[graphrag]'``.
11
+ * ``lightrag`` — graph + vector retrieval (HKUDS/LightRAG, multimodal
12
+ via the LightRAG native pipeline); optional dependency,
13
+ ``pip install 'deeptutor[rag-lightrag]'``.
14
+ * ``lightrag-server`` — retrieval offloaded to an external, standalone
15
+ LightRAG server the user runs. No local index: each
16
+ KB is a connection pointer queried over HTTP.
17
+ * ``ima`` — retrieval offloaded to a Tencent IMA knowledge base
18
+ the user curates in IMA. No local index: each KB is
19
+ a connection pointer queried over IMA's OpenAPI.
20
+ * ``weknora`` — retrieval offloaded to a knowledge base in a
21
+ self-hosted Tencent WeKnora deployment. No local
22
+ index or document copy; each KB is a connection
23
+ pointer queried over WeKnora's REST API.
24
+ * ``kiwix`` — retrieval from one ZIM already served by
25
+ kiwix-serve. Only matched articles are fetched;
26
+ no local archive or index copy is made.
27
+
28
+ A KB is bound to one provider at creation time; later adds and retrieval always
29
+ go through that same pipeline (enforced upstream in the knowledge router).
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from pathlib import Path
35
+ from typing import Any, Dict, List, Optional, Tuple
36
+
37
+ DEFAULT_PROVIDER = "llamaindex"
38
+ PAGEINDEX_PROVIDER = "pageindex"
39
+ PAGEINDEX_OSS_PROVIDER = "pageindex-oss"
40
+ GRAPHRAG_PROVIDER = "graphrag"
41
+ LIGHTRAG_PROVIDER = "lightrag"
42
+ LIGHTRAG_SERVER_PROVIDER = "lightrag-server"
43
+ IMA_PROVIDER = "ima"
44
+ WEKNORA_PROVIDER = "weknora"
45
+ KIWIX_PROVIDER = "kiwix"
46
+
47
+ # Providers the factory can instantiate. Unknown / legacy strings fall back to
48
+ # the default with a re-index hint upstream.
49
+ KNOWN_PROVIDERS = frozenset(
50
+ {
51
+ DEFAULT_PROVIDER,
52
+ PAGEINDEX_PROVIDER,
53
+ PAGEINDEX_OSS_PROVIDER,
54
+ GRAPHRAG_PROVIDER,
55
+ LIGHTRAG_PROVIDER,
56
+ LIGHTRAG_SERVER_PROVIDER,
57
+ IMA_PROVIDER,
58
+ WEKNORA_PROVIDER,
59
+ KIWIX_PROVIDER,
60
+ }
61
+ )
62
+
63
+ # Cached pipeline instances keyed by (kb_base_dir, provider).
64
+ _PIPELINE_CACHE: Dict[Tuple[Optional[str], str], Any] = {}
65
+
66
+
67
+ def normalize_provider_name(name: Optional[str] = None) -> str:
68
+ """Return a known provider name, falling back to the default.
69
+
70
+ Unknown / removed provider strings collapse to the default so a stale config
71
+ never selects a pipeline that no longer exists.
72
+ """
73
+ candidate = (name or "").strip().lower()
74
+ return candidate if candidate in KNOWN_PROVIDERS else DEFAULT_PROVIDER
75
+
76
+
77
+ def provider_uses_embedding_versions(provider: Optional[str]) -> bool:
78
+ """Whether this provider's index versions are keyed by embedding signature.
79
+
80
+ Today only the LlamaIndex pipeline uses DeepTutor's active embedding
81
+ signature to select/read index versions. PageIndex, GraphRAG and LightRAG
82
+ write synthetic provider signatures (``pageindex``/``graphrag``/``lightrag``)
83
+ and should not be marked stale merely because the active embedding profile
84
+ changed.
85
+ """
86
+ return normalize_provider_name(provider) == DEFAULT_PROVIDER
87
+
88
+
89
+ def version_matches_provider(entry: dict[str, Any], provider: Optional[str]) -> bool:
90
+ """Return True when a version-list entry belongs to ``provider``."""
91
+ resolved = normalize_provider_name(provider)
92
+ entry_provider = str(entry.get("provider") or "").strip().lower()
93
+ signature = str(entry.get("signature") or "").strip().lower()
94
+
95
+ if resolved == DEFAULT_PROVIDER:
96
+ return entry_provider in {"", DEFAULT_PROVIDER} and signature not in {
97
+ PAGEINDEX_PROVIDER,
98
+ PAGEINDEX_OSS_PROVIDER,
99
+ GRAPHRAG_PROVIDER,
100
+ LIGHTRAG_PROVIDER,
101
+ LIGHTRAG_SERVER_PROVIDER,
102
+ IMA_PROVIDER,
103
+ WEKNORA_PROVIDER,
104
+ KIWIX_PROVIDER,
105
+ }
106
+
107
+ return entry_provider == resolved or signature == resolved
108
+
109
+
110
+ def has_ready_provider_index(kb_dir: str | Path, provider: Optional[str]) -> bool:
111
+ """Return whether ``kb_dir`` has a ready index for ``provider``."""
112
+ from .index_probe import has_ready_provider_index as _has_ready_provider_index
113
+
114
+ return _has_ready_provider_index(kb_dir, provider)
115
+
116
+
117
+ def version_has_provider_output(entry: dict[str, Any], provider: Optional[str]) -> bool:
118
+ """Return True when a version entry is ready and has real provider output."""
119
+ from .index_probe import inspect_provider_version
120
+
121
+ return inspect_provider_version(entry, provider).ready
122
+
123
+
124
+ def provider_failure_summary(
125
+ kb_dir: str | Path,
126
+ provider: Optional[str],
127
+ *,
128
+ limit: int = 3,
129
+ ) -> str:
130
+ """Return a short provider-specific failure summary, when available."""
131
+ from .index_probe import provider_failure_summary as _provider_failure_summary
132
+
133
+ return _provider_failure_summary(kb_dir, provider, limit=limit)
134
+
135
+
136
+ def _build_pipeline(provider: str, kb_base_dir: Optional[str], **kwargs: Any):
137
+ if provider in {PAGEINDEX_PROVIDER, PAGEINDEX_OSS_PROVIDER}:
138
+ from .pipelines.pageindex.pipeline import PageIndexPipeline
139
+
140
+ if kb_base_dir is not None:
141
+ kwargs.setdefault("kb_base_dir", kb_base_dir)
142
+ return PageIndexPipeline(provider=provider, **kwargs)
143
+
144
+ if provider == GRAPHRAG_PROVIDER:
145
+ from .pipelines.graphrag.pipeline import GraphRagPipeline
146
+
147
+ if kb_base_dir is not None:
148
+ kwargs.setdefault("kb_base_dir", kb_base_dir)
149
+ return GraphRagPipeline(**kwargs)
150
+
151
+ if provider == LIGHTRAG_PROVIDER:
152
+ from .pipelines.lightrag.pipeline import LightRagPipeline
153
+
154
+ if kb_base_dir is not None:
155
+ kwargs.setdefault("kb_base_dir", kb_base_dir)
156
+ return LightRagPipeline(**kwargs)
157
+
158
+ if provider == LIGHTRAG_SERVER_PROVIDER:
159
+ from .pipelines.lightrag_server.pipeline import LightRagServerPipeline
160
+
161
+ if kb_base_dir is not None:
162
+ kwargs.setdefault("kb_base_dir", kb_base_dir)
163
+ return LightRagServerPipeline(**kwargs)
164
+
165
+ if provider == IMA_PROVIDER:
166
+ from .pipelines.ima.pipeline import ImaPipeline
167
+
168
+ if kb_base_dir is not None:
169
+ kwargs.setdefault("kb_base_dir", kb_base_dir)
170
+ return ImaPipeline(**kwargs)
171
+
172
+ if provider == WEKNORA_PROVIDER:
173
+ from .pipelines.weknora.pipeline import WeKnoraPipeline
174
+
175
+ if kb_base_dir is not None:
176
+ kwargs.setdefault("kb_base_dir", kb_base_dir)
177
+ return WeKnoraPipeline(**kwargs)
178
+
179
+ if provider == KIWIX_PROVIDER:
180
+ from .pipelines.kiwix.pipeline import KiwixPipeline
181
+
182
+ if kb_base_dir is not None:
183
+ kwargs.setdefault("kb_base_dir", kb_base_dir)
184
+ return KiwixPipeline(**kwargs)
185
+
186
+ from .pipelines.llamaindex.pipeline import LlamaIndexPipeline
187
+
188
+ if kb_base_dir is not None:
189
+ kwargs.setdefault("kb_base_dir", kb_base_dir)
190
+ return LlamaIndexPipeline(**kwargs)
191
+
192
+
193
+ def get_pipeline(
194
+ name: str = DEFAULT_PROVIDER,
195
+ kb_base_dir: Optional[str] = None,
196
+ **kwargs: Any,
197
+ ):
198
+ """Return a pipeline instance for ``name`` (cached when no custom kwargs)."""
199
+ provider = normalize_provider_name(name)
200
+
201
+ if kwargs:
202
+ # Custom kwargs (e.g. an injected client/loader): build a fresh instance
203
+ # and skip the cache so overrides are honoured.
204
+ return _build_pipeline(provider, kb_base_dir, **kwargs)
205
+
206
+ cache_key = (kb_base_dir, provider)
207
+ if cache_key not in _PIPELINE_CACHE:
208
+ _PIPELINE_CACHE[cache_key] = _build_pipeline(provider, kb_base_dir)
209
+ return _PIPELINE_CACHE[cache_key]
210
+
211
+
212
+ def list_pipelines() -> List[Dict[str, Any]]:
213
+ """Describe the available pipelines for the UI provider picker."""
214
+ try:
215
+ from .pipelines.pageindex.config import is_pageindex_configured
216
+
217
+ pageindex_ready = is_pageindex_configured()
218
+ except Exception:
219
+ pageindex_ready = False
220
+
221
+ try:
222
+ from .pipelines.pageindex.client import resolve_oss_sdk_config
223
+
224
+ resolve_oss_sdk_config()
225
+ pageindex_oss_ready, pageindex_oss_reason = True, ""
226
+ except Exception as exc:
227
+ pageindex_oss_ready = False
228
+ pageindex_oss_reason = str(exc)
229
+
230
+ try:
231
+ from .pipelines.ima.config import is_ima_configured
232
+
233
+ ima_ready = is_ima_configured()
234
+ except Exception:
235
+ ima_ready = False
236
+
237
+ try:
238
+ from .pipelines.graphrag import config as graphrag_config
239
+
240
+ graphrag_ready = graphrag_config.is_graphrag_available()
241
+ graphrag_modes = list(graphrag_config.SUPPORTED_MODES)
242
+ graphrag_default_mode = graphrag_config.DEFAULT_MODE
243
+ except Exception:
244
+ graphrag_ready, graphrag_modes, graphrag_default_mode = False, [], ""
245
+
246
+ try:
247
+ from .pipelines.lightrag import config as lightrag_config
248
+
249
+ lightrag_ready = lightrag_config.is_lightrag_available()
250
+ lightrag_modes = list(lightrag_config.SUPPORTED_MODES)
251
+ lightrag_default_mode = lightrag_config.DEFAULT_MODE
252
+ except Exception:
253
+ lightrag_ready, lightrag_modes, lightrag_default_mode = False, [], ""
254
+
255
+ try:
256
+ from deeptutor.services.config import load_lightrag_server_settings
257
+
258
+ from .pipelines.lightrag_server import config as lightrag_server_config
259
+
260
+ lightrag_server_modes = list(lightrag_server_config.SUPPORTED_MODES)
261
+ lightrag_server_default_mode = lightrag_server_config.DEFAULT_MODE
262
+ lightrag_server_defaults_ready = bool(load_lightrag_server_settings().get("server_url"))
263
+ except Exception:
264
+ lightrag_server_modes, lightrag_server_default_mode = [], ""
265
+ lightrag_server_defaults_ready = False
266
+
267
+ return [
268
+ {
269
+ "id": DEFAULT_PROVIDER,
270
+ "name": "LlamaIndex",
271
+ "description": "Local vector retrieval with hybrid BM25/vector fusion. Works out of the box.",
272
+ "configured": True,
273
+ "requires_api_key": False,
274
+ },
275
+ {
276
+ "id": PAGEINDEX_PROVIDER,
277
+ "name": "PageIndex Cloud",
278
+ "description": "Hosted, vectorless engine: the chat agent reads documents through PageIndex SDK tools. Requires an API key; PDF, Office, text and Markdown formats.",
279
+ "configured": pageindex_ready,
280
+ "requires_api_key": True,
281
+ },
282
+ {
283
+ "id": PAGEINDEX_OSS_PROVIDER,
284
+ "name": "PageIndex OSS",
285
+ "description": "Local, chunkless and vectorless document indexing. Uses the active LLM and accepts PDF files.",
286
+ "configured": pageindex_oss_ready,
287
+ "requires_api_key": False,
288
+ "readiness_reason": pageindex_oss_reason,
289
+ },
290
+ {
291
+ "id": GRAPHRAG_PROVIDER,
292
+ "name": "GraphRAG",
293
+ "description": "Local knowledge-graph retrieval (global/local/drift/basic). Needs `pip install 'deeptutor[graphrag]'`; indexing is LLM-heavy.",
294
+ "configured": graphrag_ready,
295
+ "requires_api_key": False,
296
+ "modes": graphrag_modes,
297
+ "default_mode": graphrag_default_mode,
298
+ },
299
+ {
300
+ "id": LIGHTRAG_PROVIDER,
301
+ "name": "LightRAG",
302
+ "description": "Graph + vector retrieval with multimodal parsing (naive/local/global/hybrid/mix). Needs `pip install 'deeptutor[rag-lightrag]'`; indexing is LLM-heavy.",
303
+ "configured": lightrag_ready,
304
+ "requires_api_key": False,
305
+ "modes": lightrag_modes,
306
+ "default_mode": lightrag_default_mode,
307
+ },
308
+ {
309
+ "id": LIGHTRAG_SERVER_PROVIDER,
310
+ "name": "LightRAG Server",
311
+ "description": "Retrieval offloaded to an external, standalone LightRAG server you run. No local index — connect a KB to its URL and query it over HTTP (naive/local/global/hybrid/mix).",
312
+ # Always available: it's a thin HTTP client with no install or global
313
+ # credential. The endpoint is configured per-KB at connect time.
314
+ "configured": True,
315
+ "requires_api_key": False,
316
+ "setup_required": not lightrag_server_defaults_ready,
317
+ "modes": lightrag_server_modes,
318
+ "default_mode": lightrag_server_default_mode,
319
+ },
320
+ {
321
+ "id": IMA_PROVIDER,
322
+ "name": "Tencent IMA",
323
+ "description": (
324
+ "Retrieval offloaded to a knowledge base you keep in Tencent IMA. "
325
+ "No local index and no copy — connect a KB to its IMA library and "
326
+ "query it over IMA's OpenAPI. Chat can also browse the library's "
327
+ "documents, read a full source, search your IMA notes, and (when "
328
+ "you ask) collect a web page or save a note. Uploading files still "
329
+ "happens in IMA itself. Requires an IMA Client ID and API key."
330
+ ),
331
+ # A thin HTTPS client with no install; readiness is only about the
332
+ # account credentials. The library id stays per-KB, set at connect
333
+ # time, and a KB may pin its own credentials to reach another account.
334
+ "configured": ima_ready,
335
+ "requires_api_key": True,
336
+ },
337
+ {
338
+ "id": WEKNORA_PROVIDER,
339
+ "name": "WeKnora",
340
+ "description": (
341
+ "Retrieval offloaded to a knowledge base in your self-hosted "
342
+ "WeKnora deployment. No local index or document copy; connect "
343
+ "to its API URL, API key, and knowledge-base ID."
344
+ ),
345
+ "configured": True,
346
+ "requires_api_key": True,
347
+ },
348
+ ]
349
+
350
+
351
+ __all__ = [
352
+ "DEFAULT_PROVIDER",
353
+ "PAGEINDEX_PROVIDER",
354
+ "PAGEINDEX_OSS_PROVIDER",
355
+ "GRAPHRAG_PROVIDER",
356
+ "LIGHTRAG_PROVIDER",
357
+ "LIGHTRAG_SERVER_PROVIDER",
358
+ "IMA_PROVIDER",
359
+ "WEKNORA_PROVIDER",
360
+ "KNOWN_PROVIDERS",
361
+ "get_pipeline",
362
+ "has_ready_provider_index",
363
+ "list_pipelines",
364
+ "normalize_provider_name",
365
+ "provider_failure_summary",
366
+ "provider_uses_embedding_versions",
367
+ "version_has_provider_output",
368
+ "version_matches_provider",
369
+ ]
@@ -0,0 +1,5 @@
1
+ """Compatibility import for shared document classification."""
2
+
3
+ from deeptutor.utils.file_types import DocumentType, FileClassification, FileTypeRouter
4
+
5
+ __all__ = ["DocumentType", "FileClassification", "FileTypeRouter"]