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,556 @@
|
|
|
1
|
+
"""LlamaIndex-backed RAG pipeline orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from contextvars import copy_context
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import traceback
|
|
13
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
from deeptutor.runtime.home import get_runtime_data_root
|
|
16
|
+
from deeptutor.services.embedding import get_embedding_config
|
|
17
|
+
from deeptutor.services.rag.embedding_signature import signature_from_embedding_config
|
|
18
|
+
from deeptutor.services.rag.index_versioning import (
|
|
19
|
+
EmbeddingSignature,
|
|
20
|
+
resolve_storage_dir_for_read,
|
|
21
|
+
resolve_storage_dir_for_rebuild,
|
|
22
|
+
write_version_meta,
|
|
23
|
+
)
|
|
24
|
+
from deeptutor.services.rag.kb_paths import resolve_kb_dir
|
|
25
|
+
from deeptutor.services.rag.visual_assets import VisualAssetStore
|
|
26
|
+
|
|
27
|
+
from . import storage
|
|
28
|
+
from .config import default_top_k, should_show_progress
|
|
29
|
+
from .document_loader import LlamaIndexDocumentLoader
|
|
30
|
+
from .embedding_adapter import (
|
|
31
|
+
configure_llamaindex_settings,
|
|
32
|
+
set_progress_callback,
|
|
33
|
+
verify_embedding_connectivity,
|
|
34
|
+
)
|
|
35
|
+
from .errors import search_error_result
|
|
36
|
+
|
|
37
|
+
DEFAULT_KB_BASE_DIR = str(get_runtime_data_root() / "knowledge_bases")
|
|
38
|
+
|
|
39
|
+
# How long an indexing step may run without reporting any progress before it
|
|
40
|
+
# is treated as stalled (see _run_with_stall_guard).
|
|
41
|
+
_INDEX_STALL_TIMEOUT_SECONDS = 600.0
|
|
42
|
+
# How often the stall guard checks the progress heartbeat.
|
|
43
|
+
_INDEX_STALL_POLL_SECONDS = 5.0
|
|
44
|
+
_INDEX_WORKERS: dict[str, threading.Event] = {}
|
|
45
|
+
_INDEX_WORKERS_LOCK = threading.Lock()
|
|
46
|
+
|
|
47
|
+
SignatureProvider = Callable[[], EmbeddingSignature | None]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class IndexingStallError(RuntimeError):
|
|
51
|
+
"""Raised when an indexing operation makes no progress for too long."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _worker_busy_error() -> IndexingStallError:
|
|
55
|
+
return IndexingStallError(
|
|
56
|
+
"A previous indexing worker for this knowledge base is still running. "
|
|
57
|
+
"Wait for it to finish or restart DeepTutor before retrying."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _ensure_index_worker_available(key: str) -> None:
|
|
62
|
+
with _INDEX_WORKERS_LOCK:
|
|
63
|
+
marker = _INDEX_WORKERS.get(key)
|
|
64
|
+
if marker is not None and not marker.is_set():
|
|
65
|
+
raise _worker_busy_error()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _claim_index_worker(key: str | None) -> threading.Event | None:
|
|
69
|
+
if key is None:
|
|
70
|
+
return None
|
|
71
|
+
with _INDEX_WORKERS_LOCK:
|
|
72
|
+
previous = _INDEX_WORKERS.get(key)
|
|
73
|
+
if previous is not None and not previous.is_set():
|
|
74
|
+
raise _worker_busy_error()
|
|
75
|
+
marker = threading.Event()
|
|
76
|
+
_INDEX_WORKERS[key] = marker
|
|
77
|
+
return marker
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _release_index_worker(key: str | None, marker: threading.Event | None) -> None:
|
|
81
|
+
if key is None or marker is None:
|
|
82
|
+
return
|
|
83
|
+
marker.set()
|
|
84
|
+
with _INDEX_WORKERS_LOCK:
|
|
85
|
+
if _INDEX_WORKERS.get(key) is marker:
|
|
86
|
+
_INDEX_WORKERS.pop(key, None)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def _run_with_stall_guard(
|
|
90
|
+
fn: Callable[[], Any],
|
|
91
|
+
*,
|
|
92
|
+
progress_callback: Optional[Callable[..., Any]] = None,
|
|
93
|
+
stall_timeout: Optional[float] = None,
|
|
94
|
+
worker_key: str | None = None,
|
|
95
|
+
) -> Any:
|
|
96
|
+
"""Run a synchronous indexing step in the executor, failing if it stalls.
|
|
97
|
+
|
|
98
|
+
Indexing steps (chunking + embedding) run in a worker thread via
|
|
99
|
+
``run_in_executor``. A provider that accepts a request but never
|
|
100
|
+
completes it (e.g. a blackholed keep-alive connection) can block that
|
|
101
|
+
thread indefinitely: per-request HTTP timeouts only bound a single
|
|
102
|
+
attempt, and provider retries extend the wait far beyond any reasonable
|
|
103
|
+
budget. Instead of hanging forever, watch the embedding progress
|
|
104
|
+
heartbeat and fail with a clear error once no progress has been reported
|
|
105
|
+
for ``stall_timeout`` seconds.
|
|
106
|
+
|
|
107
|
+
The sync function keeps running in its thread after a stall is raised —
|
|
108
|
+
Python cannot interrupt arbitrary synchronous code. Its callback becomes
|
|
109
|
+
silent and a same-KB retry is rejected until that thread actually exits.
|
|
110
|
+
"""
|
|
111
|
+
if stall_timeout is None:
|
|
112
|
+
stall_timeout = _INDEX_STALL_TIMEOUT_SECONDS
|
|
113
|
+
|
|
114
|
+
worker_marker = _claim_index_worker(worker_key)
|
|
115
|
+
progress_live = threading.Event()
|
|
116
|
+
progress_live.set()
|
|
117
|
+
last_progress = {"at": time.monotonic()}
|
|
118
|
+
|
|
119
|
+
def _heartbeat(*args: Any, **kwargs: Any) -> None:
|
|
120
|
+
if not progress_live.is_set():
|
|
121
|
+
return
|
|
122
|
+
last_progress["at"] = time.monotonic()
|
|
123
|
+
if progress_callback is not None:
|
|
124
|
+
progress_callback(*args, **kwargs)
|
|
125
|
+
|
|
126
|
+
def _work() -> Any:
|
|
127
|
+
try:
|
|
128
|
+
return fn()
|
|
129
|
+
finally:
|
|
130
|
+
_release_index_worker(worker_key, worker_marker)
|
|
131
|
+
|
|
132
|
+
set_progress_callback(_heartbeat)
|
|
133
|
+
try:
|
|
134
|
+
future = asyncio.get_running_loop().run_in_executor(None, copy_context().run, _work)
|
|
135
|
+
except BaseException:
|
|
136
|
+
progress_live.clear()
|
|
137
|
+
_release_index_worker(worker_key, worker_marker)
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
def _consume_terminal_exception(fut: "asyncio.Future[Any]") -> None:
|
|
141
|
+
# The stalled thread may finish after we raise; retrieve its exception
|
|
142
|
+
# so it is not reported as "exception was never retrieved".
|
|
143
|
+
if not fut.cancelled():
|
|
144
|
+
fut.exception()
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
while True:
|
|
148
|
+
done, _ = await asyncio.wait({future}, timeout=_INDEX_STALL_POLL_SECONDS)
|
|
149
|
+
if done:
|
|
150
|
+
return future.result()
|
|
151
|
+
stalled_for = time.monotonic() - last_progress["at"]
|
|
152
|
+
if stalled_for > stall_timeout:
|
|
153
|
+
raise IndexingStallError(
|
|
154
|
+
f"Indexing made no progress for {stalled_for:.0f}s while "
|
|
155
|
+
"embedding documents. Check the embedding endpoint; wait "
|
|
156
|
+
"for the worker to finish or restart DeepTutor before retrying."
|
|
157
|
+
)
|
|
158
|
+
finally:
|
|
159
|
+
progress_live.clear()
|
|
160
|
+
if not future.done():
|
|
161
|
+
future.add_done_callback(_consume_terminal_exception)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class LlamaIndexPipeline:
|
|
165
|
+
"""Pipeline that indexes and retrieves KB content via LlamaIndex."""
|
|
166
|
+
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
kb_base_dir: Optional[str] = None,
|
|
170
|
+
*,
|
|
171
|
+
signature_provider: SignatureProvider | None = None,
|
|
172
|
+
document_loader: LlamaIndexDocumentLoader | None = None,
|
|
173
|
+
):
|
|
174
|
+
self.logger = logging.getLogger(__name__)
|
|
175
|
+
self.kb_base_dir = kb_base_dir or DEFAULT_KB_BASE_DIR
|
|
176
|
+
self._signature_provider = signature_provider or signature_from_embedding_config
|
|
177
|
+
self.document_loader = document_loader or LlamaIndexDocumentLoader(self.logger)
|
|
178
|
+
|
|
179
|
+
def _configure_settings(self) -> None:
|
|
180
|
+
configure_llamaindex_settings(self.logger)
|
|
181
|
+
|
|
182
|
+
async def _verify_embedding_connectivity(self) -> None:
|
|
183
|
+
await verify_embedding_connectivity(self.logger)
|
|
184
|
+
|
|
185
|
+
def _current_signature(self) -> EmbeddingSignature | None:
|
|
186
|
+
return self._signature_provider()
|
|
187
|
+
|
|
188
|
+
def _cleanup_failed_version_dir(self, storage_dir: Path, signature: Optional[Any]) -> None:
|
|
189
|
+
_ = signature
|
|
190
|
+
try:
|
|
191
|
+
if storage.cleanup_failed_version_dir(storage_dir):
|
|
192
|
+
self.logger.info(
|
|
193
|
+
f"Removed empty version dir after failed pipeline run: {storage_dir}"
|
|
194
|
+
)
|
|
195
|
+
except Exception as cleanup_exc: # pragma: no cover - best-effort
|
|
196
|
+
self.logger.warning(
|
|
197
|
+
f"Could not clean up failed version dir for {storage_dir}: {cleanup_exc}"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _embedding_source_records(run: Any) -> dict[str, storage.EmbeddingSourceRecord]:
|
|
202
|
+
return {
|
|
203
|
+
key: storage.EmbeddingSourceRecord(key=key, path=path, source_hash=source_hash)
|
|
204
|
+
for key, (path, source_hash) in run.source_receipts().items()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async def initialize(self, kb_name: str, file_paths: List[str], **kwargs) -> bool:
|
|
208
|
+
from deeptutor.knowledge.indexing_run import current_run
|
|
209
|
+
|
|
210
|
+
progress_callback = kwargs.get("progress_callback")
|
|
211
|
+
image_progress_callback = kwargs.get("image_progress_callback")
|
|
212
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
213
|
+
_ensure_index_worker_available(str(kb_dir.resolve()))
|
|
214
|
+
self._configure_settings()
|
|
215
|
+
|
|
216
|
+
self.logger.info(
|
|
217
|
+
f"Initializing KB '{kb_name}' with {len(file_paths)} files using LlamaIndex"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
signature = self._current_signature()
|
|
221
|
+
run = current_run()
|
|
222
|
+
source_records = self._embedding_source_records(run) if run is not None else {}
|
|
223
|
+
storage_dir = storage.resolve_resumable_rebuild_dir(
|
|
224
|
+
kb_dir,
|
|
225
|
+
signature,
|
|
226
|
+
previous_run=run.previous_run if run is not None else None,
|
|
227
|
+
source_records=source_records,
|
|
228
|
+
)
|
|
229
|
+
resumed = storage_dir is not None
|
|
230
|
+
if storage_dir is None:
|
|
231
|
+
storage_dir = resolve_storage_dir_for_rebuild(kb_dir, signature, publication_guard=True)
|
|
232
|
+
if run is not None:
|
|
233
|
+
run.set_candidate_storage(storage_dir)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
await self._verify_embedding_connectivity()
|
|
237
|
+
visual_candidates = []
|
|
238
|
+
documents = await self.document_loader.load(
|
|
239
|
+
file_paths,
|
|
240
|
+
image_progress_callback=image_progress_callback,
|
|
241
|
+
kb_dir=kb_dir,
|
|
242
|
+
visual_candidates=visual_candidates,
|
|
243
|
+
)
|
|
244
|
+
if not documents:
|
|
245
|
+
self.logger.error("No valid documents found")
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
self.logger.info(
|
|
249
|
+
f"Creating VectorStoreIndex with {len(documents)} documents "
|
|
250
|
+
f"(chunking + embedding)..."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# #1802: validate and publish pixels before persisting nodes that
|
|
254
|
+
# reference them. Failed indexing can leave unused immutable assets;
|
|
255
|
+
# failed asset publication must not leave a partially accepted index.
|
|
256
|
+
VisualAssetStore(kb_dir).publish(visual_candidates, prune_missing=True)
|
|
257
|
+
|
|
258
|
+
def checkpoint_document(path: Path, embedding_reused: bool) -> None:
|
|
259
|
+
if run is not None:
|
|
260
|
+
run.document(
|
|
261
|
+
path,
|
|
262
|
+
"completed",
|
|
263
|
+
embedding_checkpoint=True,
|
|
264
|
+
embedding_reused=embedding_reused,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
await _run_with_stall_guard(
|
|
268
|
+
lambda: storage.create_resumable_index(
|
|
269
|
+
documents,
|
|
270
|
+
storage_dir,
|
|
271
|
+
source_records=source_records,
|
|
272
|
+
signature=signature,
|
|
273
|
+
show_progress=should_show_progress(),
|
|
274
|
+
checkpoint_callback=checkpoint_document if run is not None else None,
|
|
275
|
+
),
|
|
276
|
+
progress_callback=progress_callback,
|
|
277
|
+
worker_key=str(kb_dir.resolve()),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
self.logger.info(f"Index persisted to {storage_dir}")
|
|
281
|
+
if resumed:
|
|
282
|
+
self.logger.info(f"Resumed interrupted LlamaIndex candidate at {storage_dir}")
|
|
283
|
+
await self._publish_verified(kb_dir, storage_dir, signature, documents)
|
|
284
|
+
|
|
285
|
+
indexed_file_callback = kwargs.get("indexed_file_callback")
|
|
286
|
+
if indexed_file_callback is not None:
|
|
287
|
+
indexed_paths: set[str] = set()
|
|
288
|
+
for document in documents:
|
|
289
|
+
metadata = getattr(document, "metadata", None)
|
|
290
|
+
if isinstance(metadata, dict):
|
|
291
|
+
path = metadata.get("file_path")
|
|
292
|
+
if isinstance(path, str) and path:
|
|
293
|
+
indexed_paths.add(path)
|
|
294
|
+
indexed_file_callback(sorted(indexed_paths))
|
|
295
|
+
|
|
296
|
+
self.logger.info(f"KB '{kb_name}' initialized successfully with LlamaIndex")
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
self.logger.error(f"Failed to initialize KB: {exc}")
|
|
301
|
+
self.logger.error(traceback.format_exc())
|
|
302
|
+
# A timed-out executor can still be writing to this directory.
|
|
303
|
+
if not isinstance(exc, IndexingStallError):
|
|
304
|
+
self._cleanup_failed_version_dir(storage_dir, signature)
|
|
305
|
+
raise
|
|
306
|
+
finally:
|
|
307
|
+
set_progress_callback(None)
|
|
308
|
+
|
|
309
|
+
async def search(
|
|
310
|
+
self,
|
|
311
|
+
query: str,
|
|
312
|
+
kb_name: str,
|
|
313
|
+
**kwargs,
|
|
314
|
+
) -> Dict[str, Any]:
|
|
315
|
+
kwargs.pop("mode", None)
|
|
316
|
+
# Numbered exercises require structural lookup: tokenization discards
|
|
317
|
+
# chapter numbers, while top-k fragments omit tables and continuation.
|
|
318
|
+
# This uses completed parses only and leaves ordinary semantic queries
|
|
319
|
+
# on the existing embedding/retrieval path.
|
|
320
|
+
from .exercise_lookup import lookup_exercises, requested_exercises
|
|
321
|
+
|
|
322
|
+
if requested_exercises(query)[0]:
|
|
323
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
324
|
+
try:
|
|
325
|
+
exact = await asyncio.to_thread(
|
|
326
|
+
lookup_exercises, query, kb_dir, Path(self.kb_base_dir).parent / "parse_cache"
|
|
327
|
+
)
|
|
328
|
+
except Exception:
|
|
329
|
+
self.logger.exception("Exercise lookup unavailable; using normal retrieval")
|
|
330
|
+
else:
|
|
331
|
+
if exact is not None:
|
|
332
|
+
return exact
|
|
333
|
+
self._configure_settings()
|
|
334
|
+
self.logger.info(f"Searching KB '{kb_name}' with query: {query[:50]}...")
|
|
335
|
+
|
|
336
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
337
|
+
signature = self._current_signature()
|
|
338
|
+
storage_dir = resolve_storage_dir_for_read(kb_dir, signature)
|
|
339
|
+
|
|
340
|
+
if storage_dir is None or not (storage_dir / "docstore.json").exists():
|
|
341
|
+
self.logger.warning(
|
|
342
|
+
f"No matching index found for KB '{kb_name}' at signature "
|
|
343
|
+
f"{signature.hash() if signature else 'n/a'}"
|
|
344
|
+
)
|
|
345
|
+
return {
|
|
346
|
+
"query": query,
|
|
347
|
+
"answer": (
|
|
348
|
+
"This knowledge base has no index for its selected embedding "
|
|
349
|
+
"model. Re-index it (or switch back to a previously-used "
|
|
350
|
+
"embedding model) before querying."
|
|
351
|
+
),
|
|
352
|
+
"content": "",
|
|
353
|
+
"provider": "llamaindex",
|
|
354
|
+
"needs_reindex": True,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
embedding_mismatch_warning = self._embedding_mismatch_warning(kb_name)
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
loop = asyncio.get_running_loop()
|
|
361
|
+
top_k = kwargs.get("top_k") or default_top_k()
|
|
362
|
+
nodes = await loop.run_in_executor(
|
|
363
|
+
None,
|
|
364
|
+
copy_context().run,
|
|
365
|
+
lambda: storage.retrieve_nodes(
|
|
366
|
+
storage_dir, query, top_k=top_k, document_keys=kwargs.get("document_keys")
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
result = self._nodes_to_result(query, nodes)
|
|
371
|
+
if embedding_mismatch_warning:
|
|
372
|
+
result["warning"] = embedding_mismatch_warning
|
|
373
|
+
return result
|
|
374
|
+
|
|
375
|
+
except Exception as exc:
|
|
376
|
+
result = search_error_result(query, exc)
|
|
377
|
+
if result.get("error_type"):
|
|
378
|
+
log_message = result.get("log_message") or str(exc)
|
|
379
|
+
self.logger.warning(f"Search failed ({result['error_type']}): {log_message}")
|
|
380
|
+
else:
|
|
381
|
+
self.logger.error(f"Search failed: {exc}")
|
|
382
|
+
self.logger.error(traceback.format_exc())
|
|
383
|
+
return result
|
|
384
|
+
|
|
385
|
+
def _embedding_mismatch_warning(self, kb_name: str) -> str:
|
|
386
|
+
try:
|
|
387
|
+
cfg_path = Path(self.kb_base_dir) / "kb_config.json"
|
|
388
|
+
if not cfg_path.exists():
|
|
389
|
+
return ""
|
|
390
|
+
with open(cfg_path, encoding="utf-8") as handle:
|
|
391
|
+
kb_entry = json.load(handle).get("knowledge_bases", {}).get(kb_name, {})
|
|
392
|
+
if not kb_entry.get("embedding_mismatch"):
|
|
393
|
+
return ""
|
|
394
|
+
stored = kb_entry.get("embedding_model", "unknown")
|
|
395
|
+
current = get_embedding_config().model
|
|
396
|
+
warning = (
|
|
397
|
+
f"Warning: KB '{kb_name}' was indexed with '{stored}' "
|
|
398
|
+
f"but current model is '{current}'. Re-index recommended."
|
|
399
|
+
)
|
|
400
|
+
self.logger.warning(warning)
|
|
401
|
+
return warning
|
|
402
|
+
except Exception:
|
|
403
|
+
return ""
|
|
404
|
+
|
|
405
|
+
def _nodes_to_result(self, query: str, nodes: list[Any]) -> Dict[str, Any]:
|
|
406
|
+
context_parts: list[str] = []
|
|
407
|
+
sources: list[dict[str, Any]] = []
|
|
408
|
+
for i, node in enumerate(nodes):
|
|
409
|
+
context_parts.append(node.node.text)
|
|
410
|
+
meta = node.node.metadata or {}
|
|
411
|
+
sources.append(
|
|
412
|
+
{
|
|
413
|
+
"title": meta.get("file_name", meta.get("title", f"Document {i + 1}")),
|
|
414
|
+
"content": node.node.text[:200],
|
|
415
|
+
"source": meta.get("file_path", meta.get("file_name", "")),
|
|
416
|
+
"page": meta.get("page_label", meta.get("page", "")),
|
|
417
|
+
"chunk_id": node.node.node_id or str(i),
|
|
418
|
+
"score": round(node.score, 4) if node.score is not None else "",
|
|
419
|
+
**(
|
|
420
|
+
{
|
|
421
|
+
"document_key": meta["document_key"],
|
|
422
|
+
"source_digest": meta.get("source_digest", ""),
|
|
423
|
+
"source_schema_version": meta.get("source_schema_version", 0),
|
|
424
|
+
"page_index": meta.get("page_index"),
|
|
425
|
+
"page_label": meta.get("page_label", ""),
|
|
426
|
+
"parser_signature": meta.get("parser_signature", ""),
|
|
427
|
+
"page_status": meta.get("page_status", "unlocated"),
|
|
428
|
+
"visual_status": meta.get("visual_status", "unknown"),
|
|
429
|
+
"evidence_text": node.node.text[:6000],
|
|
430
|
+
}
|
|
431
|
+
if meta.get("document_key")
|
|
432
|
+
else {}
|
|
433
|
+
),
|
|
434
|
+
**(
|
|
435
|
+
{
|
|
436
|
+
"visual_asset_id": meta["visual_asset_id"],
|
|
437
|
+
"source_document_id": meta.get("source_document_id", ""),
|
|
438
|
+
"bbox": meta.get("bbox"),
|
|
439
|
+
"caption": meta.get("caption", ""),
|
|
440
|
+
"source_locator": meta.get("source_locator", ""),
|
|
441
|
+
}
|
|
442
|
+
if meta.get("visual_asset_id")
|
|
443
|
+
else {}
|
|
444
|
+
),
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
content = "\n\n".join(context_parts) if context_parts else ""
|
|
449
|
+
return {
|
|
450
|
+
"query": query,
|
|
451
|
+
"answer": content,
|
|
452
|
+
"content": content,
|
|
453
|
+
"sources": sources,
|
|
454
|
+
"provider": "llamaindex",
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async def add_documents(self, kb_name: str, file_paths: List[str], **kwargs) -> bool:
|
|
458
|
+
progress_callback = kwargs.get("progress_callback")
|
|
459
|
+
image_progress_callback = kwargs.get("image_progress_callback")
|
|
460
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
461
|
+
_ensure_index_worker_available(str(kb_dir.resolve()))
|
|
462
|
+
self._configure_settings()
|
|
463
|
+
|
|
464
|
+
self.logger.info(f"Adding {len(file_paths)} documents to KB '{kb_name}' using LlamaIndex")
|
|
465
|
+
|
|
466
|
+
signature = self._current_signature()
|
|
467
|
+
plan = storage.resolve_add_storage_plan(kb_dir, signature)
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
await self._verify_embedding_connectivity()
|
|
471
|
+
|
|
472
|
+
visual_candidates = []
|
|
473
|
+
documents = await self.document_loader.load(
|
|
474
|
+
file_paths,
|
|
475
|
+
image_progress_callback=image_progress_callback,
|
|
476
|
+
kb_dir=kb_dir,
|
|
477
|
+
visual_candidates=visual_candidates,
|
|
478
|
+
)
|
|
479
|
+
if not documents:
|
|
480
|
+
self.logger.warning("No valid documents to add")
|
|
481
|
+
return False
|
|
482
|
+
|
|
483
|
+
VisualAssetStore(kb_dir).publish(visual_candidates)
|
|
484
|
+
if plan.existing_storage is not None:
|
|
485
|
+
self.logger.info(f"Loading existing index from {plan.existing_storage}...")
|
|
486
|
+
num_added = await _run_with_stall_guard(
|
|
487
|
+
lambda: storage.insert_documents(
|
|
488
|
+
plan.existing_storage, plan.storage_dir, documents
|
|
489
|
+
),
|
|
490
|
+
progress_callback=progress_callback,
|
|
491
|
+
worker_key=str(kb_dir.resolve()),
|
|
492
|
+
)
|
|
493
|
+
self.logger.info(f"Added {num_added} documents to existing index")
|
|
494
|
+
|
|
495
|
+
else:
|
|
496
|
+
self.logger.info(f"Creating new index with {len(documents)} documents...")
|
|
497
|
+
plan.storage_dir.mkdir(parents=True, exist_ok=True)
|
|
498
|
+
num_added = await _run_with_stall_guard(
|
|
499
|
+
lambda: storage.create_index(
|
|
500
|
+
documents, plan.storage_dir, show_progress=should_show_progress()
|
|
501
|
+
),
|
|
502
|
+
progress_callback=progress_callback,
|
|
503
|
+
worker_key=str(kb_dir.resolve()),
|
|
504
|
+
)
|
|
505
|
+
self.logger.info(f"Created new index with {num_added} documents")
|
|
506
|
+
|
|
507
|
+
await self._publish_verified(kb_dir, plan.storage_dir, signature, documents)
|
|
508
|
+
self.logger.info(f"Successfully added documents to KB '{kb_name}'")
|
|
509
|
+
return True
|
|
510
|
+
|
|
511
|
+
except Exception as exc:
|
|
512
|
+
self.logger.error(f"Failed to add documents: {exc}")
|
|
513
|
+
self.logger.error(traceback.format_exc())
|
|
514
|
+
if not isinstance(exc, IndexingStallError) and (
|
|
515
|
+
plan.existing_storage is None or plan.storage_dir != plan.existing_storage
|
|
516
|
+
):
|
|
517
|
+
self._cleanup_failed_version_dir(plan.storage_dir, signature)
|
|
518
|
+
raise
|
|
519
|
+
finally:
|
|
520
|
+
set_progress_callback(None)
|
|
521
|
+
|
|
522
|
+
async def _publish_verified(self, kb_dir, storage_dir, signature, documents):
|
|
523
|
+
from deeptutor.knowledge.indexing_run import current_run
|
|
524
|
+
from deeptutor.services.file_io import atomic_write_json
|
|
525
|
+
|
|
526
|
+
run = current_run()
|
|
527
|
+
if run:
|
|
528
|
+
run.phase("verifying")
|
|
529
|
+
await asyncio.to_thread(storage.verify_persisted_index, storage_dir)
|
|
530
|
+
if run:
|
|
531
|
+
await asyncio.to_thread(run.verify_sources)
|
|
532
|
+
if signature is not None:
|
|
533
|
+
write_version_meta(kb_dir, signature, storage_dir=storage_dir)
|
|
534
|
+
else:
|
|
535
|
+
atomic_write_json(
|
|
536
|
+
storage_dir / "meta.json",
|
|
537
|
+
{
|
|
538
|
+
"version": storage_dir.name,
|
|
539
|
+
"provider": "llamaindex",
|
|
540
|
+
"state": "published",
|
|
541
|
+
"readiness_verified": True,
|
|
542
|
+
},
|
|
543
|
+
)
|
|
544
|
+
if run:
|
|
545
|
+
run.data["usable_version"] = storage_dir.name
|
|
546
|
+
for document in documents:
|
|
547
|
+
path = (getattr(document, "metadata", None) or {}).get("file_path")
|
|
548
|
+
if path:
|
|
549
|
+
run.document(Path(path), "completed")
|
|
550
|
+
|
|
551
|
+
async def delete(self, kb_name: str) -> bool:
|
|
552
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
553
|
+
deleted = storage.delete_kb_dir(kb_dir)
|
|
554
|
+
if deleted:
|
|
555
|
+
self.logger.info(f"Deleted KB '{kb_name}'")
|
|
556
|
+
return deleted
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Optional cross-encoder reranking for the LlamaIndex pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import OrderedDict
|
|
6
|
+
import logging
|
|
7
|
+
import math
|
|
8
|
+
from threading import Lock
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
from llama_index.core.schema import MetadataMode, NodeWithScore
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_RERANKER_CACHE: "OrderedDict[str, Any]" = OrderedDict()
|
|
16
|
+
_RERANKER_CACHE_LOCK = Lock()
|
|
17
|
+
_RERANKER_CACHE_MAXSIZE = 2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def clear_reranker_cache() -> None:
|
|
21
|
+
"""Drop cached reranker models (used by tests and settings changes)."""
|
|
22
|
+
with _RERANKER_CACHE_LOCK:
|
|
23
|
+
_RERANKER_CACHE.clear()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_cross_encoder(model_name: str) -> Any:
|
|
27
|
+
"""Load a SentenceTransformers cross-encoder lazily."""
|
|
28
|
+
from sentence_transformers import CrossEncoder
|
|
29
|
+
|
|
30
|
+
return CrossEncoder(model_name)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _cross_encoder(
|
|
34
|
+
model_name: str,
|
|
35
|
+
loader: Callable[[str], Any] | None = None,
|
|
36
|
+
) -> Any | None:
|
|
37
|
+
with _RERANKER_CACHE_LOCK:
|
|
38
|
+
cached = _RERANKER_CACHE.get(model_name)
|
|
39
|
+
if cached is not None:
|
|
40
|
+
_RERANKER_CACHE.move_to_end(model_name)
|
|
41
|
+
return cached
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
model = (loader or _load_cross_encoder)(model_name)
|
|
45
|
+
except ImportError:
|
|
46
|
+
logger.warning(
|
|
47
|
+
"Reranker model %r is configured, but sentence-transformers is not installed; "
|
|
48
|
+
"using embedding retrieval unchanged.",
|
|
49
|
+
model_name,
|
|
50
|
+
)
|
|
51
|
+
return None
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
logger.warning(
|
|
54
|
+
"Failed to load reranker model %r; using embedding retrieval unchanged: %s",
|
|
55
|
+
model_name,
|
|
56
|
+
exc,
|
|
57
|
+
)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
with _RERANKER_CACHE_LOCK:
|
|
61
|
+
_RERANKER_CACHE[model_name] = model
|
|
62
|
+
_RERANKER_CACHE.move_to_end(model_name)
|
|
63
|
+
while len(_RERANKER_CACHE) > _RERANKER_CACHE_MAXSIZE:
|
|
64
|
+
_RERANKER_CACHE.popitem(last=False)
|
|
65
|
+
return model
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _sigmoid(value: float) -> float:
|
|
69
|
+
"""Convert cross-encoder logits to a stable 0..1 source score."""
|
|
70
|
+
try:
|
|
71
|
+
if value >= 0:
|
|
72
|
+
return 1.0 / (1.0 + math.exp(-value))
|
|
73
|
+
exp_value = math.exp(value)
|
|
74
|
+
return exp_value / (1.0 + exp_value)
|
|
75
|
+
except (OverflowError, ValueError):
|
|
76
|
+
return 1.0 if value > 0 else 0.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _identity_logits(scores: Any) -> Any:
|
|
80
|
+
"""Request raw cross-encoder logits from SentenceTransformers."""
|
|
81
|
+
return scores
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def rerank_nodes(
|
|
85
|
+
query: str,
|
|
86
|
+
nodes: list[Any],
|
|
87
|
+
*,
|
|
88
|
+
top_k: int,
|
|
89
|
+
model_name: str,
|
|
90
|
+
loader: Callable[[str], Any] | None = None,
|
|
91
|
+
) -> list[Any]:
|
|
92
|
+
"""Rerank LlamaIndex results and return at most ``top_k`` nodes.
|
|
93
|
+
|
|
94
|
+
Missing optional dependencies and model-load failures are non-fatal: the
|
|
95
|
+
first-stage ordering is returned so saved knowledge remains searchable.
|
|
96
|
+
"""
|
|
97
|
+
requested = max(1, int(top_k))
|
|
98
|
+
if not query or not nodes or not model_name:
|
|
99
|
+
return nodes[:requested]
|
|
100
|
+
|
|
101
|
+
model = _cross_encoder(model_name, loader)
|
|
102
|
+
if model is None:
|
|
103
|
+
return nodes[:requested]
|
|
104
|
+
|
|
105
|
+
pairs = [(query, result.node.get_content(metadata_mode=MetadataMode.LLM)) for result in nodes]
|
|
106
|
+
try:
|
|
107
|
+
raw_scores = model.predict(pairs, activation_fct=_identity_logits)
|
|
108
|
+
ranked = sorted(
|
|
109
|
+
(
|
|
110
|
+
(index, float(score))
|
|
111
|
+
for index, score in enumerate(raw_scores)
|
|
112
|
+
if math.isfinite(float(score))
|
|
113
|
+
),
|
|
114
|
+
key=lambda item: item[1],
|
|
115
|
+
reverse=True,
|
|
116
|
+
)[:requested]
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
logger.warning(
|
|
119
|
+
"Reranker model %r failed while scoring %d candidates; "
|
|
120
|
+
"using embedding retrieval unchanged: %s",
|
|
121
|
+
model_name,
|
|
122
|
+
len(nodes),
|
|
123
|
+
exc,
|
|
124
|
+
)
|
|
125
|
+
return nodes[:requested]
|
|
126
|
+
|
|
127
|
+
return [NodeWithScore(node=nodes[index].node, score=_sigmoid(score)) for index, score in ranked]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = ["clear_reranker_cache", "rerank_nodes"]
|