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,313 @@
|
|
|
1
|
+
"""PageIndex Cloud and OSS knowledge-base lifecycle orchestration.
|
|
2
|
+
|
|
3
|
+
Implements the same contract as :class:`LlamaIndexPipeline` (see
|
|
4
|
+
``..base.RAGPipeline``) but delegates document lifecycle to the PageIndex SDK.
|
|
5
|
+
Answering is intentionally unavailable through ``search()``: PageIndex
|
|
6
|
+
knowledge bases are read inside an agent loop with their provider tools.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import traceback
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from deeptutor.runtime.home import get_runtime_data_root
|
|
17
|
+
from deeptutor.services.rag.index_versioning import (
|
|
18
|
+
resolve_storage_dir_for_read,
|
|
19
|
+
resolve_storage_dir_for_rebuild,
|
|
20
|
+
)
|
|
21
|
+
from deeptutor.services.rag.kb_paths import resolve_kb_dir
|
|
22
|
+
|
|
23
|
+
from . import storage
|
|
24
|
+
from .client import PageIndexClient
|
|
25
|
+
from .config import get_pageindex_config
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
DEFAULT_KB_BASE_DIR = str(get_runtime_data_root() / "knowledge_bases")
|
|
30
|
+
|
|
31
|
+
# Mirrors what PageIndex ``POST /doc/`` accepts (ZIP is handled upstream as a
|
|
32
|
+
# container: members are extracted and validated individually). Other formats
|
|
33
|
+
# are rejected upstream and skipped defensively here.
|
|
34
|
+
SUPPORTED_EXTENSIONS = {
|
|
35
|
+
".pdf",
|
|
36
|
+
".md",
|
|
37
|
+
".markdown",
|
|
38
|
+
".txt",
|
|
39
|
+
".docx",
|
|
40
|
+
".doc",
|
|
41
|
+
".pptx",
|
|
42
|
+
".ppt",
|
|
43
|
+
".xlsx",
|
|
44
|
+
".xls",
|
|
45
|
+
".csv",
|
|
46
|
+
}
|
|
47
|
+
OSS_SUPPORTED_EXTENSIONS = {".pdf"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_supported_file(path: str | Path, provider: str = storage.CLOUD_PROVIDER) -> bool:
|
|
51
|
+
extensions = (
|
|
52
|
+
OSS_SUPPORTED_EXTENSIONS if provider == storage.OSS_PROVIDER else SUPPORTED_EXTENSIONS
|
|
53
|
+
)
|
|
54
|
+
return Path(path).suffix.lower() in extensions
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PageIndexPipeline:
|
|
58
|
+
"""Manage one PageIndex provider while preserving DeepTutor's RAG contract."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
kb_base_dir: Optional[str] = None,
|
|
63
|
+
*,
|
|
64
|
+
client: Optional[PageIndexClient] = None,
|
|
65
|
+
config_provider=None,
|
|
66
|
+
provider: str = storage.CLOUD_PROVIDER,
|
|
67
|
+
) -> None:
|
|
68
|
+
self.logger = logging.getLogger(__name__)
|
|
69
|
+
self.kb_base_dir = kb_base_dir or DEFAULT_KB_BASE_DIR
|
|
70
|
+
self._client = client
|
|
71
|
+
self._config_provider = config_provider or get_pageindex_config
|
|
72
|
+
self.provider = (
|
|
73
|
+
storage.OSS_PROVIDER if provider == storage.OSS_PROVIDER else storage.CLOUD_PROVIDER
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def _get_client(self, storage_dir: Path | None = None) -> PageIndexClient:
|
|
77
|
+
if self._client is not None:
|
|
78
|
+
return self._client
|
|
79
|
+
if self.provider == storage.OSS_PROVIDER:
|
|
80
|
+
if storage_dir is None:
|
|
81
|
+
raise RuntimeError("PageIndex OSS requires a resolved Local Library path")
|
|
82
|
+
return PageIndexClient.local(storage.sdk_storage_path(storage_dir))
|
|
83
|
+
return PageIndexClient.cloud(self._config_provider())
|
|
84
|
+
|
|
85
|
+
def _processing_mode(self, kb_name: str) -> str | None:
|
|
86
|
+
if self.provider != storage.OSS_PROVIDER:
|
|
87
|
+
return None
|
|
88
|
+
try:
|
|
89
|
+
from deeptutor.services.config.knowledge_base_config import KnowledgeBaseConfigService
|
|
90
|
+
|
|
91
|
+
mode = (
|
|
92
|
+
str(
|
|
93
|
+
KnowledgeBaseConfigService.get_instance(
|
|
94
|
+
Path(self.kb_base_dir) / "kb_config.json"
|
|
95
|
+
)
|
|
96
|
+
.get_kb_config(kb_name)
|
|
97
|
+
.get("pageindex_mode")
|
|
98
|
+
or ""
|
|
99
|
+
)
|
|
100
|
+
.strip()
|
|
101
|
+
.lower()
|
|
102
|
+
)
|
|
103
|
+
except Exception:
|
|
104
|
+
mode = ""
|
|
105
|
+
return mode if mode in {"flash", "standard"} else None
|
|
106
|
+
|
|
107
|
+
# ----- indexing -------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
async def initialize(self, kb_name: str, file_paths: List[str], **kwargs) -> bool:
|
|
110
|
+
progress_callback = kwargs.get("progress_callback")
|
|
111
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
112
|
+
storage_dir = resolve_storage_dir_for_rebuild(kb_dir, None)
|
|
113
|
+
self.logger.info(
|
|
114
|
+
"Initializing KB '%s' with %d file(s) using PageIndex", kb_name, len(file_paths)
|
|
115
|
+
)
|
|
116
|
+
try:
|
|
117
|
+
manifest = storage._empty_manifest(self.provider)
|
|
118
|
+
submitted_by_name: dict[str, str] = {}
|
|
119
|
+
count = await self._ingest(
|
|
120
|
+
file_paths,
|
|
121
|
+
manifest,
|
|
122
|
+
progress_callback,
|
|
123
|
+
storage_dir=storage_dir,
|
|
124
|
+
mode=self._processing_mode(kb_name),
|
|
125
|
+
submitted_by_name=submitted_by_name,
|
|
126
|
+
)
|
|
127
|
+
if count == 0:
|
|
128
|
+
self.logger.error("PageIndex: no supported documents to index for '%s'", kb_name)
|
|
129
|
+
self._cleanup_failed_version_dir(storage_dir)
|
|
130
|
+
return False
|
|
131
|
+
storage.write_manifest(storage_dir, manifest)
|
|
132
|
+
storage.write_meta(storage_dir, provider=self.provider)
|
|
133
|
+
if indexed_file_callback := kwargs.get("indexed_file_callback"):
|
|
134
|
+
# submit_document waits for processing, and the published
|
|
135
|
+
# manifest retains only the last file for each basename.
|
|
136
|
+
indexed_file_callback(sorted(submitted_by_name.values()))
|
|
137
|
+
self.logger.info("KB '%s' initialized with PageIndex (%d docs)", kb_name, count)
|
|
138
|
+
return True
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
self.logger.error("Failed to initialize PageIndex KB: %s", exc)
|
|
141
|
+
self.logger.error(traceback.format_exc())
|
|
142
|
+
self._cleanup_failed_version_dir(storage_dir)
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
async def add_documents(self, kb_name: str, file_paths: List[str], **kwargs) -> bool:
|
|
146
|
+
progress_callback = kwargs.get("progress_callback")
|
|
147
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
148
|
+
existing = resolve_storage_dir_for_read(kb_dir, None)
|
|
149
|
+
if existing is not None:
|
|
150
|
+
storage_dir = existing
|
|
151
|
+
manifest = storage.read_manifest(existing, provider=self.provider)
|
|
152
|
+
else:
|
|
153
|
+
storage_dir = resolve_storage_dir_for_rebuild(kb_dir, None)
|
|
154
|
+
manifest = storage._empty_manifest(self.provider)
|
|
155
|
+
|
|
156
|
+
self.logger.info("Adding %d document(s) to PageIndex KB '%s'", len(file_paths), kb_name)
|
|
157
|
+
try:
|
|
158
|
+
count = await self._ingest(
|
|
159
|
+
file_paths,
|
|
160
|
+
manifest,
|
|
161
|
+
progress_callback,
|
|
162
|
+
storage_dir=storage_dir,
|
|
163
|
+
mode=self._processing_mode(kb_name),
|
|
164
|
+
)
|
|
165
|
+
if count == 0:
|
|
166
|
+
self.logger.warning("PageIndex: no supported documents to add for '%s'", kb_name)
|
|
167
|
+
return False
|
|
168
|
+
storage_dir.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
storage.write_manifest(storage_dir, manifest)
|
|
170
|
+
storage.write_meta(storage_dir, provider=self.provider)
|
|
171
|
+
self.logger.info("Added %d doc(s) to PageIndex KB '%s'", count, kb_name)
|
|
172
|
+
return True
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
self.logger.error("Failed to add documents to PageIndex KB: %s", exc)
|
|
175
|
+
self.logger.error(traceback.format_exc())
|
|
176
|
+
raise
|
|
177
|
+
|
|
178
|
+
async def _ingest(
|
|
179
|
+
self,
|
|
180
|
+
file_paths: List[str],
|
|
181
|
+
manifest: dict[str, Any],
|
|
182
|
+
progress_callback,
|
|
183
|
+
*,
|
|
184
|
+
storage_dir: Path,
|
|
185
|
+
mode: str | None,
|
|
186
|
+
submitted_by_name: dict[str, str] | None = None,
|
|
187
|
+
) -> int:
|
|
188
|
+
supported = [fp for fp in file_paths if is_supported_file(fp, self.provider)]
|
|
189
|
+
skipped = [fp for fp in file_paths if not is_supported_file(fp, self.provider)]
|
|
190
|
+
for fp in skipped:
|
|
191
|
+
self.logger.warning("PageIndex skips unsupported file type: %s", Path(fp).name)
|
|
192
|
+
if not supported:
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
client = self._get_client(storage_dir)
|
|
196
|
+
total = len(supported)
|
|
197
|
+
for idx, fp in enumerate(supported, 1):
|
|
198
|
+
path = Path(fp)
|
|
199
|
+
self.logger.info("PageIndex: submitting %s (%d/%d)", path.name, idx, total)
|
|
200
|
+
doc_id = await client.submit_document(path, mode=mode)
|
|
201
|
+
size = path.stat().st_size if path.exists() else None
|
|
202
|
+
storage.upsert_doc(manifest, path.name, doc_id, size=size)
|
|
203
|
+
if submitted_by_name is not None:
|
|
204
|
+
submitted_by_name[path.name] = str(path)
|
|
205
|
+
if progress_callback:
|
|
206
|
+
progress_callback(idx, total)
|
|
207
|
+
return total
|
|
208
|
+
|
|
209
|
+
# ----- retrieval ------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
async def search(self, query: str, kb_name: str, **_kwargs) -> Dict[str, Any]:
|
|
212
|
+
"""Fail closed: PageIndex answering must happen inside an agent loop."""
|
|
213
|
+
message = (
|
|
214
|
+
"PageIndex uses Reasoning as Retrieval. Read this knowledge base with "
|
|
215
|
+
"its PageIndex tools inside an agent loop instead of calling rag search."
|
|
216
|
+
)
|
|
217
|
+
return {
|
|
218
|
+
"query": query,
|
|
219
|
+
"answer": message,
|
|
220
|
+
"content": "",
|
|
221
|
+
"sources": [],
|
|
222
|
+
"provider": self.provider,
|
|
223
|
+
"error_type": "reasoning_as_retrieval_required",
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
def document_map(self, kb_name: str) -> dict[str, str]:
|
|
227
|
+
"""file name -> cloud doc_id for the KB's current manifest.
|
|
228
|
+
|
|
229
|
+
Used by the chat layer to inject the doc list into the system prompt.
|
|
230
|
+
"""
|
|
231
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
232
|
+
manifest = storage.read_manifest(
|
|
233
|
+
resolve_storage_dir_for_read(kb_dir, None), provider=self.provider
|
|
234
|
+
)
|
|
235
|
+
return {
|
|
236
|
+
name: str(entry["doc_id"])
|
|
237
|
+
for name, entry in storage.doc_entries(manifest).items()
|
|
238
|
+
if isinstance(entry, dict) and entry.get("doc_id")
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# ----- lifecycle ------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
async def delete(self, kb_name: str, **_kwargs) -> bool:
|
|
244
|
+
import shutil
|
|
245
|
+
|
|
246
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
247
|
+
# Preserve the existing Cloud behavior. OSS data is already inside kb_dir.
|
|
248
|
+
if self.provider == storage.CLOUD_PROVIDER:
|
|
249
|
+
try:
|
|
250
|
+
storage_dir = resolve_storage_dir_for_read(kb_dir, None)
|
|
251
|
+
ids = storage.doc_ids(storage.read_manifest(storage_dir, provider=self.provider))
|
|
252
|
+
if ids:
|
|
253
|
+
client = self._get_client(storage_dir)
|
|
254
|
+
for doc_id in ids:
|
|
255
|
+
await client.delete_document(doc_id)
|
|
256
|
+
except Exception as exc: # pragma: no cover - best-effort
|
|
257
|
+
self.logger.warning("PageIndex cloud cleanup skipped for '%s': %s", kb_name, exc)
|
|
258
|
+
|
|
259
|
+
if kb_dir.exists():
|
|
260
|
+
shutil.rmtree(kb_dir)
|
|
261
|
+
self.logger.info("Deleted PageIndex KB '%s'", kb_name)
|
|
262
|
+
return True
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
async def remove_document(self, kb_name: str, file_name: str) -> bool:
|
|
266
|
+
"""Delete one indexed document and update the active manifest."""
|
|
267
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
268
|
+
storage_dir = resolve_storage_dir_for_read(kb_dir, None)
|
|
269
|
+
if storage_dir is None:
|
|
270
|
+
return False
|
|
271
|
+
manifest = storage.read_manifest(storage_dir, provider=self.provider)
|
|
272
|
+
docs = storage.doc_entries(manifest)
|
|
273
|
+
key = file_name if file_name in docs else Path(file_name).name
|
|
274
|
+
entry = docs.get(key)
|
|
275
|
+
if not isinstance(entry, dict) or not entry.get("doc_id"):
|
|
276
|
+
return False
|
|
277
|
+
client = (
|
|
278
|
+
PageIndexClient.local_read(storage.sdk_storage_path(storage_dir))
|
|
279
|
+
if self.provider == storage.OSS_PROVIDER and self._client is None
|
|
280
|
+
else self._get_client(storage_dir)
|
|
281
|
+
)
|
|
282
|
+
await client.delete_document(str(entry["doc_id"]))
|
|
283
|
+
storage.remove_doc(manifest, key)
|
|
284
|
+
storage.write_manifest(storage_dir, manifest)
|
|
285
|
+
storage.write_meta(storage_dir, provider=self.provider)
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
def sdk_client_for_read(self, kb_name: str) -> Any:
|
|
289
|
+
"""Return the SDK client bound to this KB's active Local Library."""
|
|
290
|
+
kb_dir = resolve_kb_dir(self.kb_base_dir, kb_name)
|
|
291
|
+
storage_dir = resolve_storage_dir_for_read(kb_dir, None)
|
|
292
|
+
if storage_dir is None:
|
|
293
|
+
raise RuntimeError(f"PageIndex knowledge base '{kb_name}' has no ready index")
|
|
294
|
+
if self.provider == storage.OSS_PROVIDER and self._client is None:
|
|
295
|
+
return PageIndexClient.local_read(storage.sdk_storage_path(storage_dir)).sdk_client
|
|
296
|
+
return self._get_client(storage_dir).sdk_client
|
|
297
|
+
|
|
298
|
+
def _cleanup_failed_version_dir(self, storage_dir: Path) -> None:
|
|
299
|
+
try:
|
|
300
|
+
if storage_dir.is_dir() and not (storage_dir / storage.META_FILENAME).exists():
|
|
301
|
+
import shutil
|
|
302
|
+
|
|
303
|
+
shutil.rmtree(storage_dir)
|
|
304
|
+
except Exception as exc: # pragma: no cover - best-effort
|
|
305
|
+
self.logger.warning("Could not clean up failed version dir %s: %s", storage_dir, exc)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
__all__ = [
|
|
309
|
+
"OSS_SUPPORTED_EXTENSIONS",
|
|
310
|
+
"PageIndexPipeline",
|
|
311
|
+
"SUPPORTED_EXTENSIONS",
|
|
312
|
+
"is_supported_file",
|
|
313
|
+
]
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""A small DeepTutor agent loop for workflow-owned PageIndex reading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from deeptutor.core.context import UnifiedContext
|
|
9
|
+
from deeptutor.core.trace import build_trace_metadata, new_call_id
|
|
10
|
+
from deeptutor.runtime.agentic import (
|
|
11
|
+
DispatchOutcome,
|
|
12
|
+
LabelProtocol,
|
|
13
|
+
LLMClientConfig,
|
|
14
|
+
UsageTracker,
|
|
15
|
+
build_completion_kwargs,
|
|
16
|
+
build_openai_client,
|
|
17
|
+
can_use_native_tool_calling,
|
|
18
|
+
dispatch_tool_calls,
|
|
19
|
+
run_agentic_loop,
|
|
20
|
+
)
|
|
21
|
+
from deeptutor.runtime.agentic.labeled_step import run_labeled_step
|
|
22
|
+
from deeptutor.runtime.registry.tool_registry import get_tool_registry
|
|
23
|
+
from deeptutor.runtime.stream_bus import StreamBus
|
|
24
|
+
from deeptutor.services.llm import get_llm_config
|
|
25
|
+
|
|
26
|
+
from .tools import PageIndexToolContext, build_pageindex_tool_context
|
|
27
|
+
|
|
28
|
+
_PROTOCOL = LabelProtocol(
|
|
29
|
+
allowed=("THINK", "TOOL", "FINISH"),
|
|
30
|
+
terminal=frozenset({"FINISH"}),
|
|
31
|
+
intermediate=frozenset({"THINK"}),
|
|
32
|
+
final=frozenset({"FINISH"}),
|
|
33
|
+
tool_label="TOOL",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class PageIndexReadingResult:
|
|
39
|
+
text: str
|
|
40
|
+
sources: list[dict[str, Any]]
|
|
41
|
+
tool_context: PageIndexToolContext
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _ReadingHost:
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
tool_context: PageIndexToolContext,
|
|
49
|
+
context: UnifiedContext,
|
|
50
|
+
stream: StreamBus,
|
|
51
|
+
client: Any,
|
|
52
|
+
model: str | None,
|
|
53
|
+
binding: str,
|
|
54
|
+
reasoning_effort: str | None,
|
|
55
|
+
source: str,
|
|
56
|
+
stage: str,
|
|
57
|
+
) -> None:
|
|
58
|
+
self.tool_context = tool_context
|
|
59
|
+
self.context = context
|
|
60
|
+
self.stream = stream
|
|
61
|
+
self.client = client
|
|
62
|
+
self.model = model
|
|
63
|
+
self.binding = binding
|
|
64
|
+
self.reasoning_effort = reasoning_effort
|
|
65
|
+
self.source = source
|
|
66
|
+
self.stage = stage
|
|
67
|
+
|
|
68
|
+
async def guard_context_window(self, messages: list[dict[str, Any]]) -> None:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
def build_iteration_trace_meta(self, iteration: int) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
72
|
+
call_id = new_call_id(f"{self.source}-pageindex-{iteration}")
|
|
73
|
+
meta = build_trace_metadata(
|
|
74
|
+
call_id=call_id,
|
|
75
|
+
phase=self.stage,
|
|
76
|
+
label="PageIndex reading",
|
|
77
|
+
call_kind="llm_reasoning",
|
|
78
|
+
trace_id=call_id,
|
|
79
|
+
trace_role="thought",
|
|
80
|
+
trace_group="stage",
|
|
81
|
+
)
|
|
82
|
+
return meta, meta
|
|
83
|
+
|
|
84
|
+
async def dispatch_tools(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
iteration: int,
|
|
88
|
+
tool_calls: list[dict[str, Any]],
|
|
89
|
+
) -> DispatchOutcome:
|
|
90
|
+
return await dispatch_tool_calls(
|
|
91
|
+
tool_calls=tool_calls,
|
|
92
|
+
context=self.context,
|
|
93
|
+
stream=self.stream,
|
|
94
|
+
source=self.source,
|
|
95
|
+
stage=self.stage,
|
|
96
|
+
iteration_index=iteration,
|
|
97
|
+
registry=self.tool_context.registry,
|
|
98
|
+
tool_call_label="PageIndex tool",
|
|
99
|
+
retrieve_label="Read document",
|
|
100
|
+
empty_tool_result_message="PageIndex returned no content.",
|
|
101
|
+
start_retrieval_message="Reading PageIndex documents",
|
|
102
|
+
too_many_tool_calls_message="Too many tool calls in one round.",
|
|
103
|
+
trace_id_prefix=f"{self.source}-pageindex",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def resolve_pause(self, dispatch: DispatchOutcome) -> bool:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
async def emit_terminator(self, payload: dict[str, Any] | None) -> None:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
async def emit_final(self, text: str, final_meta: dict[str, Any]) -> None:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
def protocol_retry_notice(self) -> str:
|
|
116
|
+
return "The PageIndex reading loop used an invalid action label; retrying."
|
|
117
|
+
|
|
118
|
+
def protocol_repair_message(self, violation: str) -> str:
|
|
119
|
+
return (
|
|
120
|
+
f"Protocol violation: {violation}. Start with exactly THINK, TOOL, or FINISH. "
|
|
121
|
+
"Use TOOL only with tool calls; FINISH must contain the requested result."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def force_finalize(
|
|
125
|
+
self,
|
|
126
|
+
*,
|
|
127
|
+
messages: list[dict[str, Any]],
|
|
128
|
+
start_iteration: int,
|
|
129
|
+
) -> tuple[str, bool, int]:
|
|
130
|
+
messages.append(
|
|
131
|
+
{
|
|
132
|
+
"role": "user",
|
|
133
|
+
"content": "Tool budget exhausted. Return FINISH followed by the best grounded result now.",
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
step = await run_labeled_step(
|
|
137
|
+
client=self.client,
|
|
138
|
+
model=self.model,
|
|
139
|
+
messages=messages,
|
|
140
|
+
completion_kwargs=build_completion_kwargs(
|
|
141
|
+
temperature=0.2,
|
|
142
|
+
model=self.model,
|
|
143
|
+
max_tokens=5000,
|
|
144
|
+
binding=self.binding,
|
|
145
|
+
reasoning_effort=self.reasoning_effort,
|
|
146
|
+
),
|
|
147
|
+
tool_schemas=None,
|
|
148
|
+
allowed_labels=("FINISH",),
|
|
149
|
+
final_labels=frozenset({"FINISH"}),
|
|
150
|
+
tool_label=None,
|
|
151
|
+
stream=self.stream,
|
|
152
|
+
source=self.source,
|
|
153
|
+
stage=self.stage,
|
|
154
|
+
iter_meta=self.build_iteration_trace_meta(start_iteration)[0],
|
|
155
|
+
binding=self.binding,
|
|
156
|
+
)
|
|
157
|
+
return step.text, step.label == "FINISH", 1
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def read_pageindex_with_agent(
|
|
161
|
+
*,
|
|
162
|
+
kb_name: str,
|
|
163
|
+
system_prompt: str,
|
|
164
|
+
user_prompt: str,
|
|
165
|
+
context: UnifiedContext | None = None,
|
|
166
|
+
stream: StreamBus | None = None,
|
|
167
|
+
source: str,
|
|
168
|
+
stage: str,
|
|
169
|
+
max_iterations: int = 8,
|
|
170
|
+
) -> PageIndexReadingResult:
|
|
171
|
+
"""Run the caller's reasoning stage with the selected PageIndex tools."""
|
|
172
|
+
llm = get_llm_config()
|
|
173
|
+
binding = str(getattr(llm, "binding", None) or "openai")
|
|
174
|
+
model = getattr(llm, "model", None)
|
|
175
|
+
if not can_use_native_tool_calling(binding=binding, model=model):
|
|
176
|
+
raise RuntimeError("The active LLM must support tool calling to read a PageIndex KB.")
|
|
177
|
+
|
|
178
|
+
tool_context = await build_pageindex_tool_context(
|
|
179
|
+
kb_name,
|
|
180
|
+
base_registry=get_tool_registry(),
|
|
181
|
+
)
|
|
182
|
+
if tool_context is None:
|
|
183
|
+
raise ValueError(f"Knowledge base '{kb_name}' is not a PageIndex knowledge base")
|
|
184
|
+
|
|
185
|
+
bus = stream or StreamBus(max_history=0)
|
|
186
|
+
turn_context = context or UnifiedContext(user_message=user_prompt, knowledge_bases=[kb_name])
|
|
187
|
+
client = build_openai_client(
|
|
188
|
+
LLMClientConfig(
|
|
189
|
+
binding=binding,
|
|
190
|
+
model=model,
|
|
191
|
+
api_key=getattr(llm, "api_key", None),
|
|
192
|
+
base_url=getattr(llm, "base_url", None),
|
|
193
|
+
api_version=getattr(llm, "api_version", None),
|
|
194
|
+
extra_headers=getattr(llm, "extra_headers", None) or None,
|
|
195
|
+
reasoning_effort=getattr(llm, "reasoning_effort", None),
|
|
196
|
+
wire_api=getattr(llm, "wire_api", None) or "auto",
|
|
197
|
+
api_format=getattr(llm, "api_format", None) or "auto",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
docs = (
|
|
201
|
+
"; ".join(
|
|
202
|
+
f"{name} (doc_id: {doc_id})" for name, doc_id in sorted(tool_context.documents.items())
|
|
203
|
+
)
|
|
204
|
+
or "(no indexed documents)"
|
|
205
|
+
)
|
|
206
|
+
tool_names = ", ".join(tool.name for tool in tool_context.tools)
|
|
207
|
+
managed = (
|
|
208
|
+
"You are reading a PageIndex knowledge base inside the caller's existing workflow.\n"
|
|
209
|
+
"Use Reasoning as Retrieval: inspect document structure and then read only relevant pages.\n"
|
|
210
|
+
"Never call a generic RAG search and never answer document claims from general knowledge.\n"
|
|
211
|
+
"Each response must start with exactly one label on its own first line: THINK, TOOL, or "
|
|
212
|
+
"FINISH. TOOL must include native tool calls. FINISH must contain the caller's requested "
|
|
213
|
+
"grounded result.\n"
|
|
214
|
+
f"Available tools: {tool_names}\nDocuments: {docs}\n"
|
|
215
|
+
f"{tool_context.instructions}\n\n{system_prompt}"
|
|
216
|
+
)
|
|
217
|
+
host = _ReadingHost(
|
|
218
|
+
tool_context=tool_context,
|
|
219
|
+
context=turn_context,
|
|
220
|
+
stream=bus,
|
|
221
|
+
client=client,
|
|
222
|
+
model=model,
|
|
223
|
+
binding=binding,
|
|
224
|
+
reasoning_effort=getattr(llm, "reasoning_effort", None),
|
|
225
|
+
source=source,
|
|
226
|
+
stage=stage,
|
|
227
|
+
)
|
|
228
|
+
outcome = await run_agentic_loop(
|
|
229
|
+
initial_messages=[
|
|
230
|
+
{"role": "system", "content": managed},
|
|
231
|
+
{"role": "user", "content": user_prompt},
|
|
232
|
+
],
|
|
233
|
+
protocol=_PROTOCOL,
|
|
234
|
+
client=client,
|
|
235
|
+
model=model,
|
|
236
|
+
completion_kwargs=build_completion_kwargs(
|
|
237
|
+
temperature=0.2,
|
|
238
|
+
model=model,
|
|
239
|
+
max_tokens=5000,
|
|
240
|
+
binding=binding,
|
|
241
|
+
reasoning_effort=getattr(llm, "reasoning_effort", None),
|
|
242
|
+
),
|
|
243
|
+
binding=binding,
|
|
244
|
+
tool_schemas=[tool.get_definition().to_openai_schema() for tool in tool_context.tools],
|
|
245
|
+
stream=bus,
|
|
246
|
+
source=source,
|
|
247
|
+
stage=stage,
|
|
248
|
+
max_iterations=max_iterations,
|
|
249
|
+
host=host,
|
|
250
|
+
usage=UsageTracker(model=model),
|
|
251
|
+
eager_sub_trace=True,
|
|
252
|
+
)
|
|
253
|
+
if outcome.sources:
|
|
254
|
+
await bus.sources(outcome.sources, source=source, stage=stage)
|
|
255
|
+
return PageIndexReadingResult(
|
|
256
|
+
text=(outcome.final_text or "").strip(),
|
|
257
|
+
sources=list(outcome.sources),
|
|
258
|
+
tool_context=tool_context,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
__all__ = ["PageIndexReadingResult", "read_pageindex_with_agent"]
|