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,360 @@
|
|
|
1
|
+
"""Retriever composition for the LlamaIndex RAG pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import shutil
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from llama_index.core.llms.mock import MockLLM
|
|
12
|
+
from llama_index.core.retrievers import QueryFusionRetriever
|
|
13
|
+
from llama_index.core.retrievers.fusion_retriever import FUSION_MODES
|
|
14
|
+
|
|
15
|
+
from .config import (
|
|
16
|
+
HYBRID_PROFILE,
|
|
17
|
+
VECTOR_PROFILE,
|
|
18
|
+
RetrievalConfig,
|
|
19
|
+
normalize_retrieval_profile,
|
|
20
|
+
retrieval_config_from_settings,
|
|
21
|
+
)
|
|
22
|
+
from .rerank import rerank_nodes
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
BM25_PERSIST_DIRNAME = "bm25_retriever"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _import_bm25_retriever():
|
|
30
|
+
try:
|
|
31
|
+
from llama_index.retrievers.bm25 import BM25Retriever
|
|
32
|
+
|
|
33
|
+
return BM25Retriever
|
|
34
|
+
except ImportError:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _bm25_persist_dir(storage_dir: Path) -> Path:
|
|
39
|
+
return storage_dir / BM25_PERSIST_DIRNAME
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _set_similarity_top_k(retriever: Any, top_k: int) -> Any:
|
|
43
|
+
if hasattr(retriever, "similarity_top_k"):
|
|
44
|
+
retriever.similarity_top_k = top_k
|
|
45
|
+
return retriever
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _corpus_size(index: Any) -> int | None:
|
|
49
|
+
"""Best-effort count of indexed nodes (the BM25 corpus size)."""
|
|
50
|
+
docstore = getattr(index, "docstore", None)
|
|
51
|
+
docs = getattr(docstore, "docs", None)
|
|
52
|
+
if isinstance(docs, dict):
|
|
53
|
+
return len(docs)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def bm25_available() -> bool:
|
|
58
|
+
"""Whether LlamaIndex's BM25 retriever package is importable.
|
|
59
|
+
|
|
60
|
+
Exposed so status surfaces can distinguish the *configured* retrieval
|
|
61
|
+
profile from the *effective* one: on Python 3.14 the BM25 dependency is
|
|
62
|
+
marker-excluded, and a deployment configured for hybrid then runs
|
|
63
|
+
vector-only without any signal otherwise (#1792).
|
|
64
|
+
"""
|
|
65
|
+
return _import_bm25_retriever() is not None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def effective_retrieval_profile(profile: str | None) -> str:
|
|
69
|
+
"""The retrieval profile that would actually run, given BM25 availability.
|
|
70
|
+
|
|
71
|
+
A hybrid configuration degrades to vector-only when the BM25 package is
|
|
72
|
+
missing; vector-only configurations are unaffected.
|
|
73
|
+
"""
|
|
74
|
+
normalized = normalize_retrieval_profile(profile)
|
|
75
|
+
if normalized == HYBRID_PROFILE and not bm25_available():
|
|
76
|
+
return VECTOR_PROFILE
|
|
77
|
+
return normalized
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_bm25_retriever(index: Any, storage_dir: Path, *, top_k: int) -> Any | None:
|
|
81
|
+
"""Build or load LlamaIndex's official BM25 retriever if available."""
|
|
82
|
+
top_k = max(1, int(top_k))
|
|
83
|
+
# BM25 raises ("k of N is larger than the number of available scores") when
|
|
84
|
+
# similarity_top_k exceeds the corpus size — so a small knowledge base (e.g. a
|
|
85
|
+
# single short document) would crash hybrid retrieval at query time. Clamp to
|
|
86
|
+
# the node count so it returns what it has instead of erroring.
|
|
87
|
+
corpus_size = _corpus_size(index)
|
|
88
|
+
if corpus_size:
|
|
89
|
+
top_k = min(top_k, corpus_size)
|
|
90
|
+
bm25_cls = _import_bm25_retriever()
|
|
91
|
+
if bm25_cls is None:
|
|
92
|
+
# #1792: an INFO line was invisible in normal runs, so deployments on
|
|
93
|
+
# Python 3.14 (where the BM25 dependency is marker-excluded) ran
|
|
94
|
+
# vector-only without ever knowing. The configured profile is not the
|
|
95
|
+
# effective one — say so at warning level.
|
|
96
|
+
logger.warning(
|
|
97
|
+
"LlamaIndex BM25 retriever package is not installed; configured "
|
|
98
|
+
"retrieval profile will run vector-only. Install "
|
|
99
|
+
"'llama-index-retrievers-bm25' to restore hybrid retrieval."
|
|
100
|
+
)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
persist_dir = _bm25_persist_dir(storage_dir)
|
|
104
|
+
if persist_dir.exists():
|
|
105
|
+
try:
|
|
106
|
+
retriever = bm25_cls.from_persist_dir(str(persist_dir))
|
|
107
|
+
return _set_similarity_top_k(retriever, top_k)
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
logger.warning("Failed to load persisted BM25 retriever from %s: %s", persist_dir, exc)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
return bm25_cls.from_defaults(index=index, similarity_top_k=top_k)
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
logger.warning("Failed to build BM25 retriever; falling back to vector retrieval: %s", exc)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def persist_bm25_retriever(index: Any, storage_dir: Path, *, top_k: int) -> bool:
|
|
119
|
+
"""Persist BM25 sidecar index for faster hybrid retrieval.
|
|
120
|
+
|
|
121
|
+
Missing optional dependencies are non-fatal because hybrid retrieval can
|
|
122
|
+
still be enabled in deployments that install ``llama-index-retrievers-bm25``.
|
|
123
|
+
"""
|
|
124
|
+
top_k = max(1, int(top_k))
|
|
125
|
+
bm25_cls = _import_bm25_retriever()
|
|
126
|
+
if bm25_cls is None:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
persist_dir = _bm25_persist_dir(storage_dir)
|
|
130
|
+
if persist_dir.exists():
|
|
131
|
+
shutil.rmtree(persist_dir, ignore_errors=True)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
retriever = bm25_cls.from_defaults(index=index, similarity_top_k=top_k)
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
logger.warning("Failed to build BM25 retriever for persistence: %s", exc)
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
if not hasattr(retriever, "persist"):
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
persist_dir.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
try:
|
|
144
|
+
retriever.persist(str(persist_dir))
|
|
145
|
+
return True
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
logger.warning("Failed to persist BM25 retriever to %s: %s", persist_dir, exc)
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def build_retriever(
|
|
152
|
+
index: Any,
|
|
153
|
+
storage_dir: Path,
|
|
154
|
+
*,
|
|
155
|
+
top_k: int = 5,
|
|
156
|
+
config: RetrievalConfig | None = None,
|
|
157
|
+
) -> Any:
|
|
158
|
+
"""Compose the retrieval stack from official LlamaIndex retrievers."""
|
|
159
|
+
top_k = max(1, int(top_k))
|
|
160
|
+
retrieval_config = config or retrieval_config_from_settings()
|
|
161
|
+
if retrieval_config.profile == VECTOR_PROFILE:
|
|
162
|
+
return index.as_retriever(similarity_top_k=top_k)
|
|
163
|
+
|
|
164
|
+
bm25_top_k = retrieval_config.candidate_top_k(top_k, retrieval_config.bm25_top_k_multiplier)
|
|
165
|
+
bm25_retriever = build_bm25_retriever(index, storage_dir, top_k=bm25_top_k)
|
|
166
|
+
if bm25_retriever is None:
|
|
167
|
+
# Only reachable for non-vector profiles: the configured stack
|
|
168
|
+
# degrades to vector-only here, and the operator must know (#1792).
|
|
169
|
+
logger.warning(
|
|
170
|
+
"Retrieval profile %r requires BM25 but it is unavailable; "
|
|
171
|
+
"running vector-only. Install 'llama-index-retrievers-bm25' "
|
|
172
|
+
"or switch the profile to 'vector'.",
|
|
173
|
+
retrieval_config.profile,
|
|
174
|
+
)
|
|
175
|
+
return index.as_retriever(similarity_top_k=top_k)
|
|
176
|
+
|
|
177
|
+
if retrieval_config.profile == HYBRID_PROFILE:
|
|
178
|
+
vector_top_k = retrieval_config.candidate_top_k(
|
|
179
|
+
top_k, retrieval_config.vector_top_k_multiplier
|
|
180
|
+
)
|
|
181
|
+
vector_retriever = index.as_retriever(similarity_top_k=vector_top_k)
|
|
182
|
+
return QueryFusionRetriever(
|
|
183
|
+
[vector_retriever, bm25_retriever],
|
|
184
|
+
llm=MockLLM(),
|
|
185
|
+
mode=FUSION_MODES.RECIPROCAL_RANK,
|
|
186
|
+
similarity_top_k=top_k,
|
|
187
|
+
num_queries=retrieval_config.fusion_num_queries,
|
|
188
|
+
use_async=False,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return index.as_retriever(similarity_top_k=top_k)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def retrieve_nodes(
|
|
195
|
+
index: Any,
|
|
196
|
+
storage_dir: Path,
|
|
197
|
+
query: str,
|
|
198
|
+
*,
|
|
199
|
+
top_k: int = 5,
|
|
200
|
+
) -> list[Any]:
|
|
201
|
+
"""Run first-stage retrieval and the optional cross-encoder reranker."""
|
|
202
|
+
config = retrieval_config_from_settings()
|
|
203
|
+
candidate_top_k = config.rerank_candidate_top_k(top_k)
|
|
204
|
+
retriever = build_retriever(
|
|
205
|
+
index,
|
|
206
|
+
storage_dir,
|
|
207
|
+
top_k=candidate_top_k,
|
|
208
|
+
config=config,
|
|
209
|
+
)
|
|
210
|
+
candidates = retriever.retrieve(query)
|
|
211
|
+
if not config.reranker_model:
|
|
212
|
+
return candidates[: max(1, int(top_k))]
|
|
213
|
+
|
|
214
|
+
return rerank_nodes(
|
|
215
|
+
query,
|
|
216
|
+
candidates,
|
|
217
|
+
top_k=top_k,
|
|
218
|
+
model_name=config.reranker_model,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@lru_cache(maxsize=4)
|
|
223
|
+
def _scoped_index(index: Any, document_keys: tuple[str, ...]) -> Any:
|
|
224
|
+
"""Reuse stored vectors in an isolated corpus; FAISS ignores node_ids filters.
|
|
225
|
+
|
|
226
|
+
This never embeds the documents again. The small LRU dies with the loaded
|
|
227
|
+
index identity, so a newly published index cannot reuse an old corpus.
|
|
228
|
+
"""
|
|
229
|
+
from llama_index.core import VectorStoreIndex
|
|
230
|
+
|
|
231
|
+
from .vector_store import storage_context_for_nodes
|
|
232
|
+
|
|
233
|
+
allowed = set(document_keys)
|
|
234
|
+
# .docs reconstructs all nodes on every access; never call it per vector.
|
|
235
|
+
documents = index.docstore.docs
|
|
236
|
+
store = index.vector_store
|
|
237
|
+
data = getattr(getattr(store, "data", None), "embedding_dict", None)
|
|
238
|
+
faiss_index = getattr(store, "_faiss_index", None)
|
|
239
|
+
if isinstance(data, dict):
|
|
240
|
+
by_node = data
|
|
241
|
+
elif faiss_index is not None:
|
|
242
|
+
by_node = {
|
|
243
|
+
node_id: faiss_index.reconstruct(int(vector_id)).tolist()
|
|
244
|
+
for vector_id, node_id in index.index_struct.nodes_dict.items()
|
|
245
|
+
if documents[node_id].metadata.get("document_key") in allowed
|
|
246
|
+
}
|
|
247
|
+
else:
|
|
248
|
+
raise ValueError("This vector backend cannot enforce selected-document retrieval scope.")
|
|
249
|
+
nodes = []
|
|
250
|
+
for node in documents.values():
|
|
251
|
+
if (
|
|
252
|
+
node.metadata.get("document_key") not in allowed
|
|
253
|
+
or node.metadata.get("page_status") != "searchable"
|
|
254
|
+
or node.node_id not in by_node
|
|
255
|
+
):
|
|
256
|
+
continue
|
|
257
|
+
copy = node.model_copy(deep=True)
|
|
258
|
+
copy.embedding = by_node[node.node_id]
|
|
259
|
+
nodes.append(copy)
|
|
260
|
+
if not nodes:
|
|
261
|
+
return None
|
|
262
|
+
return VectorStoreIndex(
|
|
263
|
+
nodes=nodes,
|
|
264
|
+
embed_model=index._embed_model,
|
|
265
|
+
storage_context=storage_context_for_nodes(nodes),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def retrieve_scoped_nodes(
|
|
270
|
+
index: Any, query: str, document_keys: list[str], *, top_k: int = 8
|
|
271
|
+
) -> list[Any]:
|
|
272
|
+
"""Restrict the corpus before retrieval, including the BM25 component."""
|
|
273
|
+
index = _scoped_index(index, tuple(sorted(set(document_keys))))
|
|
274
|
+
if index is None:
|
|
275
|
+
return []
|
|
276
|
+
nodes = list(index.docstore.docs.values())
|
|
277
|
+
ids = {node.node_id for node in nodes}
|
|
278
|
+
count = min(top_k, len(nodes))
|
|
279
|
+
vector = index.as_retriever(similarity_top_k=count)
|
|
280
|
+
bm25_cls = _import_bm25_retriever()
|
|
281
|
+
if bm25_cls is not None:
|
|
282
|
+
lexical = bm25_cls.from_defaults(nodes=nodes, similarity_top_k=count)
|
|
283
|
+
retriever = QueryFusionRetriever(
|
|
284
|
+
[vector, lexical],
|
|
285
|
+
llm=MockLLM(),
|
|
286
|
+
mode=FUSION_MODES.RECIPROCAL_RANK,
|
|
287
|
+
similarity_top_k=count,
|
|
288
|
+
num_queries=1,
|
|
289
|
+
use_async=False,
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
retriever = vector
|
|
293
|
+
return [hit for hit in retriever.retrieve(query) if hit.node.node_id in ids][:count]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class _UnstemmedPageRetriever:
|
|
297
|
+
"""Use BM25S directly when the optional stemming integration is unavailable.
|
|
298
|
+
|
|
299
|
+
Python 3.14 cannot install the PyStemmer version pinned by LlamaIndex's
|
|
300
|
+
adapter. Legacy page lookup still needs lexical retrieval without building
|
|
301
|
+
new embeddings; retain the same engine and skip optional stemming only.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
def __init__(self, nodes: list[Any]) -> None:
|
|
305
|
+
import bm25s
|
|
306
|
+
from llama_index.core.schema import MetadataMode
|
|
307
|
+
|
|
308
|
+
self.nodes = nodes
|
|
309
|
+
self.engine = bm25s.BM25()
|
|
310
|
+
corpus = [node.get_content(metadata_mode=MetadataMode.EMBED) for node in nodes]
|
|
311
|
+
self.engine.index(self._tokenize(corpus), show_progress=False)
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def _tokenize(text):
|
|
315
|
+
import bm25s
|
|
316
|
+
|
|
317
|
+
return bm25s.tokenize(text, stopwords="en", stemmer=None, show_progress=False)
|
|
318
|
+
|
|
319
|
+
def retrieve(self, query: str) -> list[Any]:
|
|
320
|
+
from llama_index.core.schema import NodeWithScore
|
|
321
|
+
|
|
322
|
+
if not query.strip():
|
|
323
|
+
return []
|
|
324
|
+
indexes, scores = self.engine.retrieve(
|
|
325
|
+
self._tokenize(query), k=min(8, len(self.nodes)), show_progress=False
|
|
326
|
+
)
|
|
327
|
+
return [
|
|
328
|
+
NodeWithScore(node=self.nodes[int(index)], score=float(score))
|
|
329
|
+
for index, score in zip(indexes[0], scores[0])
|
|
330
|
+
]
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@lru_cache(maxsize=4)
|
|
334
|
+
def _legacy_page_retriever(directory: Path) -> Any:
|
|
335
|
+
from llama_index.core.storage.docstore import SimpleDocumentStore
|
|
336
|
+
|
|
337
|
+
nodes = list(
|
|
338
|
+
SimpleDocumentStore.from_persist_path(str(directory / "docstore.json")).docs.values()
|
|
339
|
+
)
|
|
340
|
+
cls = _import_bm25_retriever()
|
|
341
|
+
if cls is None:
|
|
342
|
+
return _UnstemmedPageRetriever(nodes)
|
|
343
|
+
return cls.from_defaults(nodes=nodes, similarity_top_k=min(8, len(nodes)))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def retrieve_legacy_pages(directory: Path, query: str, *, top_k: int = 8) -> list[Any]:
|
|
347
|
+
# The sidecar directory's content-addressed identity already includes the
|
|
348
|
+
# selected files. Never merge in nodes from another source or workspace.
|
|
349
|
+
return _legacy_page_retriever(directory).retrieve(query)[:top_k]
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
__all__ = [
|
|
353
|
+
"BM25_PERSIST_DIRNAME",
|
|
354
|
+
"bm25_available",
|
|
355
|
+
"build_bm25_retriever",
|
|
356
|
+
"build_retriever",
|
|
357
|
+
"effective_retrieval_profile",
|
|
358
|
+
"persist_bm25_retriever",
|
|
359
|
+
"retrieve_nodes",
|
|
360
|
+
]
|