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,288 @@
1
+ """Vector-store backend selection for the LlamaIndex RAG pipeline.
2
+
3
+ This module is the single seam between DeepTutor's LlamaIndex pipeline and the
4
+ concrete vector store implementation. It exists so the rest of the pipeline
5
+ (ingestion, storage, retrieval) never has to know *how* vectors are stored.
6
+
7
+ Why it exists
8
+ -------------
9
+ LlamaIndex's built-in ``SimpleVectorStore`` keeps every embedding in a JSON
10
+ document and performs a pure-Python, O(N) brute-force scan on each query. On a
11
+ large knowledge base that is re-parsed and re-scanned per query, which pins a
12
+ CPU core and makes retrieval take minutes (issue #552).
13
+
14
+ This seam swaps in a FAISS index instead:
15
+
16
+ * New (and re-indexed) knowledge bases are persisted as a binary FAISS index
17
+ (exact ``IndexFlatIP`` by default, or opt-in HNSW for large corpora), loaded
18
+ once and searched without a Python-side scan.
19
+ * Legacy ``SimpleVectorStore`` knowledge bases stay fully readable, so upgrading
20
+ never breaks an existing index. Re-indexing one rebuilds it as FAISS for the
21
+ full speed-up.
22
+
23
+ FAISS is optional. When ``faiss`` / ``llama-index-vector-stores-faiss`` are not
24
+ importable, every path falls back to ``SimpleVectorStore`` so retrieval keeps
25
+ working (just without the speed-up).
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ import os
32
+ from pathlib import Path
33
+ from typing import Any, Iterable, Optional
34
+
35
+ from fsspec.implementations.local import LocalFileSystem
36
+ from llama_index.core import StorageContext, load_index_from_storage
37
+ from llama_index.core.vector_stores.simple import DEFAULT_VECTOR_STORE, NAMESPACE_SEP
38
+ import numpy as np
39
+
40
+ from .config import HNSW_VECTOR_INDEX, VectorIndexConfig
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ BACKEND_FAISS = "faiss"
45
+ BACKEND_SIMPLE = "simple"
46
+
47
+ # The default vector store is always persisted under this filename, whether it
48
+ # holds a SimpleVectorStore JSON document or a binary FAISS index. The first
49
+ # byte disambiguates the two: "{" => JSON/simple, anything else => FAISS.
50
+ DEFAULT_VECTOR_STORE_FILENAME = f"{DEFAULT_VECTOR_STORE}{NAMESPACE_SEP}vector_store.json"
51
+
52
+ _COSINE_FAISS_CLS: Optional[type] = None
53
+
54
+
55
+ def _faiss_modules() -> tuple[Any, Any]:
56
+ """Return ``(faiss, FaissVectorStore)`` or ``(None, None)`` when unavailable."""
57
+ try:
58
+ import faiss
59
+ from llama_index.vector_stores.faiss import FaissVectorStore
60
+ except Exception: # pragma: no cover - exercised only without faiss installed
61
+ return None, None
62
+ return faiss, FaissVectorStore
63
+
64
+
65
+ def faiss_available() -> bool:
66
+ """True when both the FAISS library and its LlamaIndex integration import."""
67
+ faiss, faiss_store_cls = _faiss_modules()
68
+ return faiss is not None and faiss_store_cls is not None
69
+
70
+
71
+ def _normalize(embedding: Any) -> list[float]:
72
+ """L2-normalize an embedding so inner product equals cosine similarity."""
73
+ vector = np.asarray(embedding, dtype="float32")
74
+ norm = float(np.linalg.norm(vector))
75
+ if norm > 0:
76
+ vector = vector / norm
77
+ return vector.tolist()
78
+
79
+
80
+ def faiss_write_index(index: Any, persist_path: str) -> None:
81
+ """Persist a FAISS index through a Python byte stream (Unicode-path safe).
82
+
83
+ Stock ``faiss.write_index`` is a SWIG passthrough that hands the path
84
+ straight to C++ ``fopen``. On Windows that is the *narrow* ANSI API, while
85
+ SWIG encodes the Python string as UTF-8 — so any non-ASCII path (a Chinese
86
+ knowledge-base name, or a ``C:\\Users\\张三`` home directory) fails to open
87
+ and index rebuilds crash. Serializing to bytes in memory and letting Python
88
+ write the file sidesteps this: CPython opens files via the wide ``_wfopen``
89
+ API on Windows, so Unicode paths work. The byte payload is identical to
90
+ ``write_index`` output, so indexes stay cross-readable with stock FAISS.
91
+ """
92
+ import faiss
93
+
94
+ payload = faiss.serialize_index(index)
95
+ with open(persist_path, "wb") as handle:
96
+ handle.write(payload.tobytes())
97
+
98
+
99
+ def faiss_read_index(persist_path: str) -> Any:
100
+ """Load a FAISS index written by :func:`faiss_write_index` (or stock FAISS).
101
+
102
+ The on-disk format is identical either way, so this also reads indexes
103
+ persisted by an older ``faiss.write_index`` call. Reading the bytes with
104
+ Python ``open`` keeps the load path Unicode-safe on Windows, mirroring
105
+ :func:`faiss_write_index`.
106
+ """
107
+ import faiss
108
+
109
+ with open(persist_path, "rb") as handle:
110
+ buffer = np.frombuffer(handle.read(), dtype="uint8")
111
+ return faiss.deserialize_index(buffer)
112
+
113
+
114
+ def _cosine_faiss_cls() -> Optional[type]:
115
+ """Return a memoized FaissVectorStore subclass that ranks by cosine.
116
+
117
+ Stock ``FaissVectorStore`` normalizes neither on add nor on query, so an
118
+ ``IndexFlatIP`` would rank by raw dot product. Normalizing both sides makes
119
+ inner-product ranking identical to the cosine similarity SimpleVectorStore
120
+ used, preserving retrieval behaviour after the backend swap.
121
+ """
122
+ global _COSINE_FAISS_CLS
123
+ if _COSINE_FAISS_CLS is not None:
124
+ return _COSINE_FAISS_CLS
125
+
126
+ _, faiss_store_cls = _faiss_modules()
127
+ if faiss_store_cls is None:
128
+ return None
129
+
130
+ class _CosineFaissVectorStore(faiss_store_cls): # type: ignore[valid-type, misc]
131
+ """FAISS store that L2-normalizes vectors for cosine ranking.
132
+
133
+ It also overrides persistence to route through :func:`faiss_write_index`
134
+ / :func:`faiss_read_index` instead of the stock path-based
135
+ ``faiss.write_index`` / ``read_index``, which cannot handle non-ASCII
136
+ paths on Windows. The two concerns are independent: cosine ranking
137
+ shapes *what* is stored, the IO override shapes *how* it reaches disk.
138
+ """
139
+
140
+ def add(self, nodes: list[Any], **kwargs: Any) -> list[str]:
141
+ for node in nodes:
142
+ node.embedding = _normalize(node.get_embedding())
143
+ return super().add(nodes, **kwargs)
144
+
145
+ def query(self, query: Any, **kwargs: Any) -> Any:
146
+ if getattr(query, "query_embedding", None) is not None:
147
+ query.query_embedding = _normalize(query.query_embedding)
148
+ return super().query(query, **kwargs)
149
+
150
+ def persist(self, persist_path: str, fs: Any = None) -> None:
151
+ if fs is not None and not isinstance(fs, LocalFileSystem):
152
+ raise NotImplementedError("FAISS only supports local storage for now.")
153
+ dirpath = os.path.dirname(persist_path)
154
+ if dirpath:
155
+ os.makedirs(dirpath, exist_ok=True)
156
+ faiss_write_index(self._faiss_index, persist_path)
157
+
158
+ @classmethod
159
+ def from_persist_path(cls, persist_path: str, fs: Any = None) -> Any:
160
+ if fs is not None and not isinstance(fs, LocalFileSystem):
161
+ raise NotImplementedError("FAISS only supports local storage for now.")
162
+ if not os.path.exists(persist_path):
163
+ raise ValueError(f"No existing FAISS index found at {persist_path}.")
164
+ return cls(faiss_index=faiss_read_index(persist_path))
165
+
166
+ _COSINE_FAISS_CLS = _CosineFaissVectorStore
167
+ return _COSINE_FAISS_CLS
168
+
169
+
170
+ def _uniform_dimension(embeddings: Iterable[Any]) -> Optional[int]:
171
+ """Return the shared embedding dimension, or None if missing/ragged.
172
+
173
+ Mixed dimensions (e.g. text + multimodal image vectors) cannot live in a
174
+ single fixed-width FAISS index, so callers fall back to SimpleVectorStore.
175
+ """
176
+ dimension: Optional[int] = None
177
+ for embedding in embeddings:
178
+ if embedding is None:
179
+ return None
180
+ length = len(embedding)
181
+ if dimension is None:
182
+ dimension = length
183
+ elif length != dimension:
184
+ return None
185
+ return dimension if dimension and dimension > 0 else None
186
+
187
+
188
+ def _new_faiss_index(faiss: Any, dimension: int, config: VectorIndexConfig) -> Any:
189
+ """Construct a cosine-compatible FAISS index for ``config``."""
190
+ if config.type == HNSW_VECTOR_INDEX:
191
+ index = faiss.IndexHNSWFlat(
192
+ dimension,
193
+ max(1, int(config.hnsw_m)),
194
+ faiss.METRIC_INNER_PRODUCT,
195
+ )
196
+ index.hnsw.efConstruction = max(1, int(config.hnsw_ef_construction))
197
+ index.hnsw.efSearch = max(1, int(config.hnsw_ef_search))
198
+ return index
199
+ return faiss.IndexFlatIP(dimension)
200
+
201
+
202
+ def new_faiss_storage_context(
203
+ dimension: int, index_config: VectorIndexConfig | None = None
204
+ ) -> Optional[StorageContext]:
205
+ """Return a StorageContext whose default store is a fresh cosine FAISS index.
206
+
207
+ Returns None when FAISS is unavailable or the dimension is invalid, so
208
+ callers fall back to the default SimpleVectorStore StorageContext.
209
+ """
210
+ faiss, _ = _faiss_modules()
211
+ cosine_cls = _cosine_faiss_cls()
212
+ if faiss is None or cosine_cls is None or dimension <= 0:
213
+ return None
214
+ store = cosine_cls(
215
+ faiss_index=_new_faiss_index(faiss, dimension, index_config or VectorIndexConfig())
216
+ )
217
+ return StorageContext.from_defaults(vector_store=store)
218
+
219
+
220
+ def storage_context_for_nodes(
221
+ nodes: list[Any], index_config: VectorIndexConfig | None = None
222
+ ) -> Optional[StorageContext]:
223
+ """Choose the write-time StorageContext for a set of embedded nodes.
224
+
225
+ Returns a FAISS-backed context when every node shares one embedding
226
+ dimension and FAISS is available; otherwise None (use the SimpleVectorStore
227
+ default). ``None`` keeps multimodal / mixed-dimension and faiss-less installs
228
+ on the original code path.
229
+ """
230
+ if not faiss_available():
231
+ return None
232
+ dimension = _uniform_dimension(getattr(node, "embedding", None) for node in nodes)
233
+ if dimension is None:
234
+ return None
235
+ return new_faiss_storage_context(dimension, index_config)
236
+
237
+
238
+ def detect_backend(storage_dir: Path) -> str:
239
+ """Detect a persisted index's vector backend from its default store file."""
240
+ path = Path(storage_dir) / DEFAULT_VECTOR_STORE_FILENAME
241
+ try:
242
+ with open(path, "rb") as handle:
243
+ head = handle.read(1)
244
+ except OSError:
245
+ return BACKEND_SIMPLE
246
+ return BACKEND_SIMPLE if head[:1] == b"{" else BACKEND_FAISS
247
+
248
+
249
+ def load_index(storage_dir: Path) -> Any:
250
+ """Load a persisted index for retrieval.
251
+
252
+ FAISS-persisted versions load their binary index directly. Legacy
253
+ SimpleVectorStore versions load unchanged and stay queryable; re-indexing
254
+ such a knowledge base rebuilds it as FAISS for the full speed-up.
255
+ """
256
+ storage_dir = Path(storage_dir)
257
+ from .embedding_adapter import current_embedding
258
+
259
+ if detect_backend(storage_dir) == BACKEND_FAISS:
260
+ cosine_cls = _cosine_faiss_cls()
261
+ if cosine_cls is None:
262
+ raise RuntimeError(
263
+ "This knowledge base was indexed with FAISS but the 'faiss-cpu' "
264
+ "package is not installed. Install it (pip install faiss-cpu) or "
265
+ "re-index the knowledge base to query it again."
266
+ )
267
+ vector_store = cosine_cls.from_persist_dir(str(storage_dir))
268
+ context = StorageContext.from_defaults(
269
+ persist_dir=str(storage_dir), vector_store=vector_store
270
+ )
271
+ return load_index_from_storage(context, embed_model=current_embedding())
272
+
273
+ context = StorageContext.from_defaults(persist_dir=str(storage_dir))
274
+ return load_index_from_storage(context, embed_model=current_embedding())
275
+
276
+
277
+ __all__ = [
278
+ "BACKEND_FAISS",
279
+ "BACKEND_SIMPLE",
280
+ "DEFAULT_VECTOR_STORE_FILENAME",
281
+ "detect_backend",
282
+ "faiss_available",
283
+ "faiss_read_index",
284
+ "faiss_write_index",
285
+ "load_index",
286
+ "new_faiss_storage_context",
287
+ "storage_context_for_nodes",
288
+ ]
@@ -0,0 +1,48 @@
1
+ """Shared retrieval-mode resolution for mode-aware pipelines (LightRAG, GraphRAG).
2
+
3
+ Resolution order, first valid wins:
4
+
5
+ 1. an explicit ``mode`` kwarg (a one-off override on the search call),
6
+ 2. the KB's own ``search_mode`` (``kb_config.json`` → ``knowledge_bases[kb]``),
7
+ 3. the engine's global default mode set from the engine card
8
+ (``kb_config.json`` → ``defaults.provider_modes[provider]``),
9
+ 4. the engine's built-in default.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ from pathlib import Path
16
+ from typing import Any, Optional, Sequence
17
+
18
+
19
+ def resolve_kb_mode(
20
+ kb_base_dir: str | Path,
21
+ kb_name: Optional[str],
22
+ provider: str,
23
+ *,
24
+ explicit: Any = None,
25
+ supported: Sequence[str],
26
+ default: str,
27
+ ) -> str:
28
+ candidates: list[Any] = [explicit]
29
+ try:
30
+ cfg_path = Path(kb_base_dir) / "kb_config.json"
31
+ if cfg_path.exists():
32
+ data = json.loads(cfg_path.read_text(encoding="utf-8"))
33
+ if kb_name:
34
+ entry = data.get("knowledge_bases", {}).get(kb_name, {})
35
+ candidates.append(entry.get("search_mode"))
36
+ candidates.append(data.get("defaults", {}).get("provider_modes", {}).get(provider))
37
+ except Exception: # pragma: no cover - defensive
38
+ pass
39
+
40
+ supported_set = {m.lower() for m in supported}
41
+ for candidate in candidates:
42
+ norm = (str(candidate) if candidate else "").strip().lower()
43
+ if norm in supported_set:
44
+ return norm
45
+ return default
46
+
47
+
48
+ __all__ = ["resolve_kb_mode"]
@@ -0,0 +1,47 @@
1
+ """PageIndex Cloud and OSS knowledge-base lifecycle.
2
+
3
+ Both providers use turn-scoped PageIndex SDK tools: Cloud delegates its transport
4
+ to the SDK, while OSS binds the same interface to the KB's Local Library. Neither
5
+ provider answers through ``RAGPipeline.search``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from .pipeline import PageIndexPipeline
11
+
12
+
13
+ def _bound_pageindex_provider(kb_name: str | None) -> str | None:
14
+ if not kb_name:
15
+ return None
16
+ try:
17
+ from deeptutor.multi_user.knowledge_access import resolve_kb
18
+ from deeptutor.services.rag.provider_binding import resolve_bound_provider
19
+
20
+ resource = resolve_kb(kb_name, require_write=False)
21
+ return resolve_bound_provider(resource.base_dir, resource.name)
22
+ except Exception:
23
+ return None
24
+
25
+
26
+ def is_pageindex_kb(kb_name: str | None) -> bool:
27
+ from deeptutor.services.rag.factory import PAGEINDEX_OSS_PROVIDER, PAGEINDEX_PROVIDER
28
+
29
+ return _bound_pageindex_provider(kb_name) in {PAGEINDEX_PROVIDER, PAGEINDEX_OSS_PROVIDER}
30
+
31
+
32
+ def validate_pageindex_oss_selection(knowledge_bases: list[str]) -> None:
33
+ from deeptutor.services.rag.factory import PAGEINDEX_OSS_PROVIDER
34
+
35
+ selected = [
36
+ name
37
+ for name in dict.fromkeys(str(item or "").strip() for item in knowledge_bases)
38
+ if name and _bound_pageindex_provider(name) == PAGEINDEX_OSS_PROVIDER
39
+ ]
40
+ if len(selected) > 1:
41
+ raise ValueError(
42
+ "Select at most one PageIndex OSS knowledge base per request. "
43
+ f"Selected: {', '.join(selected)}"
44
+ )
45
+
46
+
47
+ __all__ = ["PageIndexPipeline", "is_pageindex_kb", "validate_pageindex_oss_selection"]
@@ -0,0 +1,146 @@
1
+ """Async adapter around the synchronous PageIndex SDK clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from deeptutor.services.provider_registry import find_by_name, strip_provider_prefix
11
+
12
+ from .config import PageIndexConfig
13
+
14
+
15
+ def _sdk_types():
16
+ from pageindex import PageIndexCloudClient, PageIndexLocalClient
17
+
18
+ return PageIndexCloudClient, PageIndexLocalClient
19
+
20
+
21
+ @lru_cache(maxsize=1)
22
+ def _cloud_sdk_client(api_key: str):
23
+ """Reuse the SDK's MCP bridge until the global Cloud key changes."""
24
+ cloud_type, _ = _sdk_types()
25
+ return cloud_type(api_key)
26
+
27
+
28
+ def _prefixed_model(prefix: str, model: str) -> str:
29
+ return model if model.startswith(f"{prefix}/") else f"{prefix}/{model}"
30
+
31
+
32
+ def resolve_oss_sdk_config() -> tuple[str, dict[str, Any]]:
33
+ """Translate DeepTutor's active LLM into PageIndex's indexing lane."""
34
+ from deeptutor.services.config import resolve_llm_runtime_config
35
+
36
+ cfg = resolve_llm_runtime_config()
37
+ model = str(getattr(cfg, "model", "") or "").strip()
38
+ binding = str(
39
+ getattr(cfg, "binding", None) or getattr(cfg, "provider_name", None) or "openai"
40
+ ).strip()
41
+ spec = find_by_name(binding)
42
+ if not model:
43
+ raise RuntimeError(
44
+ "PageIndex OSS needs an active LLM. Configure one under Settings → Catalog."
45
+ )
46
+ if (
47
+ spec is None
48
+ or spec.is_oauth
49
+ or spec.backend
50
+ in {
51
+ "openai_codex",
52
+ "github_copilot",
53
+ "codebuddy",
54
+ }
55
+ ):
56
+ raise RuntimeError(
57
+ "PageIndex OSS indexing needs an API-key or local LLM profile; "
58
+ "the active OAuth-only provider cannot be used."
59
+ )
60
+
61
+ resolved_model = strip_provider_prefix(model, spec)
62
+ prefix = {
63
+ "anthropic": "anthropic",
64
+ "azure_openai": "azure",
65
+ "openai_compat": "openai",
66
+ }.get(spec.backend)
67
+ if prefix is None:
68
+ raise RuntimeError("The active LLM transport is not supported by PageIndex OSS indexing.")
69
+
70
+ sdk_model = _prefixed_model(prefix, resolved_model)
71
+ backend: dict[str, Any] = {}
72
+ api_key = str(getattr(cfg, "api_key", "") or "").strip()
73
+ base_url = str(getattr(cfg, "base_url", "") or "").strip()
74
+ api_version = str(getattr(cfg, "api_version", "") or "").strip()
75
+ headers = getattr(cfg, "extra_headers", None)
76
+
77
+ if spec.backend == "openai_compat":
78
+ # PageIndex treats ``openai/...`` as the OpenAI-compatible fast path;
79
+ # this also preserves gateway model ids such as anthropic/claude-*.
80
+ backend["api_key"] = api_key or "sk-no-key-required"
81
+ elif api_key:
82
+ backend["api_key"] = api_key
83
+
84
+ if base_url:
85
+ backend["api_base"] = base_url
86
+ if api_version and spec.backend != "openai_compat":
87
+ backend["api_version"] = api_version
88
+ if isinstance(headers, dict) and headers:
89
+ if spec.backend == "openai_compat":
90
+ backend["default_headers"] = dict(headers)
91
+ else:
92
+ backend["extra_headers"] = dict(headers)
93
+
94
+ return sdk_model, backend
95
+
96
+
97
+ class PageIndexClient:
98
+ """Small async facade used by the DeepTutor RAG lifecycle."""
99
+
100
+ def __init__(self, sdk_client: Any) -> None:
101
+ self.sdk_client = sdk_client
102
+
103
+ @classmethod
104
+ def cloud(cls, config: PageIndexConfig) -> "PageIndexClient":
105
+ return cls(_cloud_sdk_client(config.api_key))
106
+
107
+ @classmethod
108
+ def local(cls, storage_path: str | Path) -> "PageIndexClient":
109
+ _, local_type = _sdk_types()
110
+ model, backend = resolve_oss_sdk_config()
111
+ return cls(
112
+ local_type(
113
+ storage_path=str(storage_path),
114
+ index_model=model,
115
+ summary_model=model,
116
+ index_backend=backend,
117
+ )
118
+ )
119
+
120
+ @classmethod
121
+ def local_read(cls, storage_path: str | Path) -> "PageIndexClient":
122
+ """Open an existing Local Library without resolving indexing credentials."""
123
+ _, local_type = _sdk_types()
124
+ return cls(local_type(storage_path=str(storage_path)))
125
+
126
+ async def submit_document(self, file_path: str | Path, *, mode: str | None = None) -> str:
127
+ result = await asyncio.to_thread(
128
+ self.sdk_client.submit_document,
129
+ str(file_path),
130
+ mode=mode,
131
+ wait=True,
132
+ )
133
+ doc_id = result.get("doc_id") if isinstance(result, dict) else None
134
+ if not doc_id:
135
+ raise RuntimeError(f"PageIndex submit_document returned no doc_id: {result!r}")
136
+ return str(doc_id)
137
+
138
+ async def delete_document(self, doc_id: str) -> bool:
139
+ await asyncio.to_thread(self.sdk_client.delete_document, doc_id)
140
+ return True
141
+
142
+
143
+ __all__ = [
144
+ "PageIndexClient",
145
+ "resolve_oss_sdk_config",
146
+ ]
@@ -0,0 +1,49 @@
1
+ """Resolve PageIndex Cloud credentials and OSS readiness.
2
+
3
+ The Cloud key lives in ``data/.../settings/pageindex.json`` (managed by
4
+ ``RuntimeSettingsService``), surfaced to users under Knowledge → RAG pipeline
5
+ settings. A single deployment credential is shared by every ``pageindex`` KB;
6
+ the SDK owns the official Cloud endpoint.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class PageIndexConfig:
16
+ api_key: str
17
+
18
+
19
+ def get_pageindex_config(*, require_key: bool = True) -> PageIndexConfig:
20
+ """Load the active PageIndex credential.
21
+
22
+ Raises when ``require_key`` and the key is empty, so callers fail with a
23
+ clear, actionable message instead of an opaque 401 from the API.
24
+ """
25
+ from deeptutor.services.config import get_runtime_settings_service
26
+
27
+ settings = get_runtime_settings_service().load_pageindex()
28
+ api_key = str(settings.get("api_key") or "").strip()
29
+ if require_key and not api_key:
30
+ raise RuntimeError(
31
+ "PageIndex API key is not configured. Add it under "
32
+ "Knowledge → RAG pipeline settings before using a PageIndex knowledge base."
33
+ )
34
+ return PageIndexConfig(api_key=api_key)
35
+
36
+
37
+ def is_pageindex_configured() -> bool:
38
+ """Best-effort check used to flag the provider as ready in the UI."""
39
+ try:
40
+ return bool(get_pageindex_config(require_key=False).api_key)
41
+ except Exception:
42
+ return False
43
+
44
+
45
+ __all__ = [
46
+ "PageIndexConfig",
47
+ "get_pageindex_config",
48
+ "is_pageindex_configured",
49
+ ]