deeptutor-knowledge 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deeptutor/api/routers/knowledge.py +5225 -0
- deeptutor/api/routers/marginnote4.py +278 -0
- deeptutor/capabilities/ima/__init__.py +6 -0
- deeptutor/capabilities/ima/binding.py +118 -0
- deeptutor/capabilities/ima/capability.py +92 -0
- deeptutor/capabilities/ima/prompts/en/system.md +33 -0
- deeptutor/capabilities/ima/prompts/zh/system.md +24 -0
- deeptutor/capabilities/ima/tools.py +476 -0
- deeptutor/capabilities/marginnote4/__init__.py +13 -0
- deeptutor/capabilities/marginnote4/binding.py +66 -0
- deeptutor/capabilities/marginnote4/capability.py +80 -0
- deeptutor/capabilities/marginnote4/models.py +173 -0
- deeptutor/capabilities/marginnote4/prompts/en/system.md +54 -0
- deeptutor/capabilities/marginnote4/prompts/zh/system.md +44 -0
- deeptutor/capabilities/marginnote4/store.py +562 -0
- deeptutor/capabilities/marginnote4/tools.py +329 -0
- deeptutor/capabilities/obsidian/__init__.py +6 -0
- deeptutor/capabilities/obsidian/binding.py +75 -0
- deeptutor/capabilities/obsidian/capability.py +82 -0
- deeptutor/capabilities/obsidian/prompts/en/system.md +32 -0
- deeptutor/capabilities/obsidian/prompts/zh/system.md +28 -0
- deeptutor/capabilities/obsidian/tools.py +399 -0
- deeptutor/capabilities/obsidian/vault.py +341 -0
- deeptutor/knowledge/__init__.py +28 -0
- deeptutor/knowledge/add_documents.py +734 -0
- deeptutor/knowledge/indexing_run.py +432 -0
- deeptutor/knowledge/initializer.py +448 -0
- deeptutor/knowledge/manager.py +2570 -0
- deeptutor/knowledge/module_service.py +29 -0
- deeptutor/knowledge/naming.py +40 -0
- deeptutor/knowledge/progress_events.py +45 -0
- deeptutor/knowledge/progress_tracker.py +355 -0
- deeptutor/knowledge/staging.py +69 -0
- deeptutor/services/config/knowledge_base_config.py +239 -0
- deeptutor/services/embedding/__init__.py +46 -0
- deeptutor/services/embedding/adapters/__init__.py +40 -0
- deeptutor/services/embedding/adapters/base.py +196 -0
- deeptutor/services/embedding/adapters/cohere.py +176 -0
- deeptutor/services/embedding/adapters/dashscope_native.py +242 -0
- deeptutor/services/embedding/adapters/gemini.py +462 -0
- deeptutor/services/embedding/adapters/jina.py +161 -0
- deeptutor/services/embedding/adapters/ollama.py +145 -0
- deeptutor/services/embedding/adapters/openai_compatible.py +406 -0
- deeptutor/services/embedding/adapters/openai_sdk.py +150 -0
- deeptutor/services/embedding/client.py +323 -0
- deeptutor/services/embedding/config.py +114 -0
- deeptutor/services/embedding/request_options.py +46 -0
- deeptutor/services/embedding/validation.py +128 -0
- deeptutor/services/github_source/__init__.py +5 -0
- deeptutor/services/github_source/client.py +164 -0
- deeptutor/services/github_source/sync.py +384 -0
- deeptutor/services/github_source/sync_service.py +72 -0
- deeptutor/services/rag/__init__.py +21 -0
- deeptutor/services/rag/embedding_binding.py +331 -0
- deeptutor/services/rag/embedding_signature.py +63 -0
- deeptutor/services/rag/eval/__init__.py +92 -0
- deeptutor/services/rag/eval/dataset.py +244 -0
- deeptutor/services/rag/eval/diff.py +312 -0
- deeptutor/services/rag/eval/matching.py +218 -0
- deeptutor/services/rag/eval/metrics.py +213 -0
- deeptutor/services/rag/eval/report.py +111 -0
- deeptutor/services/rag/eval/runner.py +309 -0
- deeptutor/services/rag/factory.py +369 -0
- deeptutor/services/rag/file_routing.py +5 -0
- deeptutor/services/rag/index_probe.py +327 -0
- deeptutor/services/rag/index_versioning.py +387 -0
- deeptutor/services/rag/kb_paths.py +50 -0
- deeptutor/services/rag/linked_kb.py +237 -0
- deeptutor/services/rag/pipelines/__init__.py +7 -0
- deeptutor/services/rag/pipelines/base.py +41 -0
- deeptutor/services/rag/pipelines/graphrag/__init__.py +18 -0
- deeptutor/services/rag/pipelines/graphrag/compatibility.py +42 -0
- deeptutor/services/rag/pipelines/graphrag/completion_adapter.py +251 -0
- deeptutor/services/rag/pipelines/graphrag/config.py +357 -0
- deeptutor/services/rag/pipelines/graphrag/engine.py +479 -0
- deeptutor/services/rag/pipelines/graphrag/errors.py +328 -0
- deeptutor/services/rag/pipelines/graphrag/ingestion.py +135 -0
- deeptutor/services/rag/pipelines/graphrag/pandas_compat.py +68 -0
- deeptutor/services/rag/pipelines/graphrag/pipeline.py +284 -0
- deeptutor/services/rag/pipelines/graphrag/provider.py +134 -0
- deeptutor/services/rag/pipelines/graphrag/storage.py +127 -0
- deeptutor/services/rag/pipelines/ima/__init__.py +28 -0
- deeptutor/services/rag/pipelines/ima/client.py +364 -0
- deeptutor/services/rag/pipelines/ima/config.py +140 -0
- deeptutor/services/rag/pipelines/ima/envelope.py +109 -0
- deeptutor/services/rag/pipelines/ima/inventory.py +209 -0
- deeptutor/services/rag/pipelines/ima/media.py +190 -0
- deeptutor/services/rag/pipelines/ima/models.py +348 -0
- deeptutor/services/rag/pipelines/ima/notes.py +240 -0
- deeptutor/services/rag/pipelines/ima/pipeline.py +238 -0
- deeptutor/services/rag/pipelines/ima/probe.py +105 -0
- deeptutor/services/rag/pipelines/ima/sources.py +105 -0
- deeptutor/services/rag/pipelines/ima/transport.py +130 -0
- deeptutor/services/rag/pipelines/kiwix/__init__.py +1 -0
- deeptutor/services/rag/pipelines/kiwix/client.py +369 -0
- deeptutor/services/rag/pipelines/kiwix/pipeline.py +75 -0
- deeptutor/services/rag/pipelines/lightrag/README.md +78 -0
- deeptutor/services/rag/pipelines/lightrag/__init__.py +15 -0
- deeptutor/services/rag/pipelines/lightrag/block_policy.py +266 -0
- deeptutor/services/rag/pipelines/lightrag/cache_reuse.py +137 -0
- deeptutor/services/rag/pipelines/lightrag/config.py +453 -0
- deeptutor/services/rag/pipelines/lightrag/engine.py +536 -0
- deeptutor/services/rag/pipelines/lightrag/indexing_policy.py +696 -0
- deeptutor/services/rag/pipelines/lightrag/ingress.py +364 -0
- deeptutor/services/rag/pipelines/lightrag/parser.py +99 -0
- deeptutor/services/rag/pipelines/lightrag/pipeline.py +692 -0
- deeptutor/services/rag/pipelines/lightrag/roles.py +132 -0
- deeptutor/services/rag/pipelines/lightrag/sidecar.py +320 -0
- deeptutor/services/rag/pipelines/lightrag/storage.py +338 -0
- deeptutor/services/rag/pipelines/lightrag/worker.py +259 -0
- deeptutor/services/rag/pipelines/lightrag/write_lock.py +55 -0
- deeptutor/services/rag/pipelines/lightrag_server/__init__.py +15 -0
- deeptutor/services/rag/pipelines/lightrag_server/client.py +149 -0
- deeptutor/services/rag/pipelines/lightrag_server/config.py +68 -0
- deeptutor/services/rag/pipelines/lightrag_server/pipeline.py +120 -0
- deeptutor/services/rag/pipelines/lightrag_server/probe.py +110 -0
- deeptutor/services/rag/pipelines/llamaindex/__init__.py +3 -0
- deeptutor/services/rag/pipelines/llamaindex/config.py +199 -0
- deeptutor/services/rag/pipelines/llamaindex/document_loader.py +648 -0
- deeptutor/services/rag/pipelines/llamaindex/embedding_adapter.py +264 -0
- deeptutor/services/rag/pipelines/llamaindex/errors.py +76 -0
- deeptutor/services/rag/pipelines/llamaindex/exercise_lookup.py +373 -0
- deeptutor/services/rag/pipelines/llamaindex/ingestion.py +129 -0
- deeptutor/services/rag/pipelines/llamaindex/pipeline.py +556 -0
- deeptutor/services/rag/pipelines/llamaindex/rerank.py +130 -0
- deeptutor/services/rag/pipelines/llamaindex/retrievers.py +360 -0
- deeptutor/services/rag/pipelines/llamaindex/storage.py +655 -0
- deeptutor/services/rag/pipelines/llamaindex/vector_store.py +288 -0
- deeptutor/services/rag/pipelines/modes.py +48 -0
- deeptutor/services/rag/pipelines/pageindex/__init__.py +47 -0
- deeptutor/services/rag/pipelines/pageindex/client.py +146 -0
- deeptutor/services/rag/pipelines/pageindex/config.py +49 -0
- deeptutor/services/rag/pipelines/pageindex/pipeline.py +313 -0
- deeptutor/services/rag/pipelines/pageindex/reasoning.py +262 -0
- deeptutor/services/rag/pipelines/pageindex/storage.py +133 -0
- deeptutor/services/rag/pipelines/pageindex/tools.py +221 -0
- deeptutor/services/rag/pipelines/weknora/__init__.py +13 -0
- deeptutor/services/rag/pipelines/weknora/client.py +106 -0
- deeptutor/services/rag/pipelines/weknora/config.py +56 -0
- deeptutor/services/rag/pipelines/weknora/pipeline.py +74 -0
- deeptutor/services/rag/pipelines/weknora/probe.py +80 -0
- deeptutor/services/rag/preflight.py +274 -0
- deeptutor/services/rag/provider_binding.py +70 -0
- deeptutor/services/rag/query_rewrite.py +186 -0
- deeptutor/services/rag/service.py +379 -0
- deeptutor/services/rag/smart_retriever.py +81 -0
- deeptutor/services/rag/source_pages.py +292 -0
- deeptutor/services/rag/source_visuals.py +434 -0
- deeptutor/services/rag/visual_assets.py +430 -0
- deeptutor/services/rag/visual_coverage.py +165 -0
- deeptutor/services/web_source/__init__.py +1 -0
- deeptutor/services/web_source/bilingual.py +255 -0
- deeptutor/services/web_source/crawler.py +681 -0
- deeptutor/services/web_source/html_extractor.py +643 -0
- deeptutor/services/web_source/markdown.py +28 -0
- deeptutor/services/web_source/navigation.py +72 -0
- deeptutor/services/web_source/repository.py +640 -0
- deeptutor/services/web_source/robots.py +182 -0
- deeptutor/services/web_source/scheduler.py +336 -0
- deeptutor/services/web_source/snapshot_assets.py +171 -0
- deeptutor/services/web_source/sync.py +253 -0
- deeptutor/services/workspace/kb_move.py +446 -0
- deeptutor/tools/rag_tool.py +110 -0
- deeptutor_cli/kb.py +756 -0
- deeptutor_knowledge-1.7.0.dist-info/METADATA +15 -0
- deeptutor_knowledge-1.7.0.dist-info/RECORD +168 -0
- deeptutor_knowledge-1.7.0.dist-info/WHEEL +5 -0
- deeptutor_knowledge-1.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,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
|
+
]
|