deeptutor-knowledge 1.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. deeptutor/api/routers/knowledge.py +5225 -0
  2. deeptutor/api/routers/marginnote4.py +278 -0
  3. deeptutor/capabilities/ima/__init__.py +6 -0
  4. deeptutor/capabilities/ima/binding.py +118 -0
  5. deeptutor/capabilities/ima/capability.py +92 -0
  6. deeptutor/capabilities/ima/prompts/en/system.md +33 -0
  7. deeptutor/capabilities/ima/prompts/zh/system.md +24 -0
  8. deeptutor/capabilities/ima/tools.py +476 -0
  9. deeptutor/capabilities/marginnote4/__init__.py +13 -0
  10. deeptutor/capabilities/marginnote4/binding.py +66 -0
  11. deeptutor/capabilities/marginnote4/capability.py +80 -0
  12. deeptutor/capabilities/marginnote4/models.py +173 -0
  13. deeptutor/capabilities/marginnote4/prompts/en/system.md +54 -0
  14. deeptutor/capabilities/marginnote4/prompts/zh/system.md +44 -0
  15. deeptutor/capabilities/marginnote4/store.py +562 -0
  16. deeptutor/capabilities/marginnote4/tools.py +329 -0
  17. deeptutor/capabilities/obsidian/__init__.py +6 -0
  18. deeptutor/capabilities/obsidian/binding.py +75 -0
  19. deeptutor/capabilities/obsidian/capability.py +82 -0
  20. deeptutor/capabilities/obsidian/prompts/en/system.md +32 -0
  21. deeptutor/capabilities/obsidian/prompts/zh/system.md +28 -0
  22. deeptutor/capabilities/obsidian/tools.py +399 -0
  23. deeptutor/capabilities/obsidian/vault.py +341 -0
  24. deeptutor/knowledge/__init__.py +28 -0
  25. deeptutor/knowledge/add_documents.py +734 -0
  26. deeptutor/knowledge/indexing_run.py +432 -0
  27. deeptutor/knowledge/initializer.py +448 -0
  28. deeptutor/knowledge/manager.py +2570 -0
  29. deeptutor/knowledge/module_service.py +29 -0
  30. deeptutor/knowledge/naming.py +40 -0
  31. deeptutor/knowledge/progress_events.py +45 -0
  32. deeptutor/knowledge/progress_tracker.py +355 -0
  33. deeptutor/knowledge/staging.py +69 -0
  34. deeptutor/services/config/knowledge_base_config.py +239 -0
  35. deeptutor/services/embedding/__init__.py +46 -0
  36. deeptutor/services/embedding/adapters/__init__.py +40 -0
  37. deeptutor/services/embedding/adapters/base.py +196 -0
  38. deeptutor/services/embedding/adapters/cohere.py +176 -0
  39. deeptutor/services/embedding/adapters/dashscope_native.py +242 -0
  40. deeptutor/services/embedding/adapters/gemini.py +462 -0
  41. deeptutor/services/embedding/adapters/jina.py +161 -0
  42. deeptutor/services/embedding/adapters/ollama.py +145 -0
  43. deeptutor/services/embedding/adapters/openai_compatible.py +406 -0
  44. deeptutor/services/embedding/adapters/openai_sdk.py +150 -0
  45. deeptutor/services/embedding/client.py +323 -0
  46. deeptutor/services/embedding/config.py +114 -0
  47. deeptutor/services/embedding/request_options.py +46 -0
  48. deeptutor/services/embedding/validation.py +128 -0
  49. deeptutor/services/github_source/__init__.py +5 -0
  50. deeptutor/services/github_source/client.py +164 -0
  51. deeptutor/services/github_source/sync.py +384 -0
  52. deeptutor/services/github_source/sync_service.py +72 -0
  53. deeptutor/services/rag/__init__.py +21 -0
  54. deeptutor/services/rag/embedding_binding.py +331 -0
  55. deeptutor/services/rag/embedding_signature.py +63 -0
  56. deeptutor/services/rag/eval/__init__.py +92 -0
  57. deeptutor/services/rag/eval/dataset.py +244 -0
  58. deeptutor/services/rag/eval/diff.py +312 -0
  59. deeptutor/services/rag/eval/matching.py +218 -0
  60. deeptutor/services/rag/eval/metrics.py +213 -0
  61. deeptutor/services/rag/eval/report.py +111 -0
  62. deeptutor/services/rag/eval/runner.py +309 -0
  63. deeptutor/services/rag/factory.py +369 -0
  64. deeptutor/services/rag/file_routing.py +5 -0
  65. deeptutor/services/rag/index_probe.py +327 -0
  66. deeptutor/services/rag/index_versioning.py +387 -0
  67. deeptutor/services/rag/kb_paths.py +50 -0
  68. deeptutor/services/rag/linked_kb.py +237 -0
  69. deeptutor/services/rag/pipelines/__init__.py +7 -0
  70. deeptutor/services/rag/pipelines/base.py +41 -0
  71. deeptutor/services/rag/pipelines/graphrag/__init__.py +18 -0
  72. deeptutor/services/rag/pipelines/graphrag/compatibility.py +42 -0
  73. deeptutor/services/rag/pipelines/graphrag/completion_adapter.py +251 -0
  74. deeptutor/services/rag/pipelines/graphrag/config.py +357 -0
  75. deeptutor/services/rag/pipelines/graphrag/engine.py +479 -0
  76. deeptutor/services/rag/pipelines/graphrag/errors.py +328 -0
  77. deeptutor/services/rag/pipelines/graphrag/ingestion.py +135 -0
  78. deeptutor/services/rag/pipelines/graphrag/pandas_compat.py +68 -0
  79. deeptutor/services/rag/pipelines/graphrag/pipeline.py +284 -0
  80. deeptutor/services/rag/pipelines/graphrag/provider.py +134 -0
  81. deeptutor/services/rag/pipelines/graphrag/storage.py +127 -0
  82. deeptutor/services/rag/pipelines/ima/__init__.py +28 -0
  83. deeptutor/services/rag/pipelines/ima/client.py +364 -0
  84. deeptutor/services/rag/pipelines/ima/config.py +140 -0
  85. deeptutor/services/rag/pipelines/ima/envelope.py +109 -0
  86. deeptutor/services/rag/pipelines/ima/inventory.py +209 -0
  87. deeptutor/services/rag/pipelines/ima/media.py +190 -0
  88. deeptutor/services/rag/pipelines/ima/models.py +348 -0
  89. deeptutor/services/rag/pipelines/ima/notes.py +240 -0
  90. deeptutor/services/rag/pipelines/ima/pipeline.py +238 -0
  91. deeptutor/services/rag/pipelines/ima/probe.py +105 -0
  92. deeptutor/services/rag/pipelines/ima/sources.py +105 -0
  93. deeptutor/services/rag/pipelines/ima/transport.py +130 -0
  94. deeptutor/services/rag/pipelines/kiwix/__init__.py +1 -0
  95. deeptutor/services/rag/pipelines/kiwix/client.py +369 -0
  96. deeptutor/services/rag/pipelines/kiwix/pipeline.py +75 -0
  97. deeptutor/services/rag/pipelines/lightrag/README.md +78 -0
  98. deeptutor/services/rag/pipelines/lightrag/__init__.py +15 -0
  99. deeptutor/services/rag/pipelines/lightrag/block_policy.py +266 -0
  100. deeptutor/services/rag/pipelines/lightrag/cache_reuse.py +137 -0
  101. deeptutor/services/rag/pipelines/lightrag/config.py +453 -0
  102. deeptutor/services/rag/pipelines/lightrag/engine.py +536 -0
  103. deeptutor/services/rag/pipelines/lightrag/indexing_policy.py +696 -0
  104. deeptutor/services/rag/pipelines/lightrag/ingress.py +364 -0
  105. deeptutor/services/rag/pipelines/lightrag/parser.py +99 -0
  106. deeptutor/services/rag/pipelines/lightrag/pipeline.py +692 -0
  107. deeptutor/services/rag/pipelines/lightrag/roles.py +132 -0
  108. deeptutor/services/rag/pipelines/lightrag/sidecar.py +320 -0
  109. deeptutor/services/rag/pipelines/lightrag/storage.py +338 -0
  110. deeptutor/services/rag/pipelines/lightrag/worker.py +259 -0
  111. deeptutor/services/rag/pipelines/lightrag/write_lock.py +55 -0
  112. deeptutor/services/rag/pipelines/lightrag_server/__init__.py +15 -0
  113. deeptutor/services/rag/pipelines/lightrag_server/client.py +149 -0
  114. deeptutor/services/rag/pipelines/lightrag_server/config.py +68 -0
  115. deeptutor/services/rag/pipelines/lightrag_server/pipeline.py +120 -0
  116. deeptutor/services/rag/pipelines/lightrag_server/probe.py +110 -0
  117. deeptutor/services/rag/pipelines/llamaindex/__init__.py +3 -0
  118. deeptutor/services/rag/pipelines/llamaindex/config.py +199 -0
  119. deeptutor/services/rag/pipelines/llamaindex/document_loader.py +648 -0
  120. deeptutor/services/rag/pipelines/llamaindex/embedding_adapter.py +264 -0
  121. deeptutor/services/rag/pipelines/llamaindex/errors.py +76 -0
  122. deeptutor/services/rag/pipelines/llamaindex/exercise_lookup.py +373 -0
  123. deeptutor/services/rag/pipelines/llamaindex/ingestion.py +129 -0
  124. deeptutor/services/rag/pipelines/llamaindex/pipeline.py +556 -0
  125. deeptutor/services/rag/pipelines/llamaindex/rerank.py +130 -0
  126. deeptutor/services/rag/pipelines/llamaindex/retrievers.py +360 -0
  127. deeptutor/services/rag/pipelines/llamaindex/storage.py +655 -0
  128. deeptutor/services/rag/pipelines/llamaindex/vector_store.py +288 -0
  129. deeptutor/services/rag/pipelines/modes.py +48 -0
  130. deeptutor/services/rag/pipelines/pageindex/__init__.py +47 -0
  131. deeptutor/services/rag/pipelines/pageindex/client.py +146 -0
  132. deeptutor/services/rag/pipelines/pageindex/config.py +49 -0
  133. deeptutor/services/rag/pipelines/pageindex/pipeline.py +313 -0
  134. deeptutor/services/rag/pipelines/pageindex/reasoning.py +262 -0
  135. deeptutor/services/rag/pipelines/pageindex/storage.py +133 -0
  136. deeptutor/services/rag/pipelines/pageindex/tools.py +221 -0
  137. deeptutor/services/rag/pipelines/weknora/__init__.py +13 -0
  138. deeptutor/services/rag/pipelines/weknora/client.py +106 -0
  139. deeptutor/services/rag/pipelines/weknora/config.py +56 -0
  140. deeptutor/services/rag/pipelines/weknora/pipeline.py +74 -0
  141. deeptutor/services/rag/pipelines/weknora/probe.py +80 -0
  142. deeptutor/services/rag/preflight.py +274 -0
  143. deeptutor/services/rag/provider_binding.py +70 -0
  144. deeptutor/services/rag/query_rewrite.py +186 -0
  145. deeptutor/services/rag/service.py +379 -0
  146. deeptutor/services/rag/smart_retriever.py +81 -0
  147. deeptutor/services/rag/source_pages.py +292 -0
  148. deeptutor/services/rag/source_visuals.py +434 -0
  149. deeptutor/services/rag/visual_assets.py +430 -0
  150. deeptutor/services/rag/visual_coverage.py +165 -0
  151. deeptutor/services/web_source/__init__.py +1 -0
  152. deeptutor/services/web_source/bilingual.py +255 -0
  153. deeptutor/services/web_source/crawler.py +681 -0
  154. deeptutor/services/web_source/html_extractor.py +643 -0
  155. deeptutor/services/web_source/markdown.py +28 -0
  156. deeptutor/services/web_source/navigation.py +72 -0
  157. deeptutor/services/web_source/repository.py +640 -0
  158. deeptutor/services/web_source/robots.py +182 -0
  159. deeptutor/services/web_source/scheduler.py +336 -0
  160. deeptutor/services/web_source/snapshot_assets.py +171 -0
  161. deeptutor/services/web_source/sync.py +253 -0
  162. deeptutor/services/workspace/kb_move.py +446 -0
  163. deeptutor/tools/rag_tool.py +110 -0
  164. deeptutor_cli/kb.py +756 -0
  165. deeptutor_knowledge-1.7.0.dist-info/METADATA +15 -0
  166. deeptutor_knowledge-1.7.0.dist-info/RECORD +168 -0
  167. deeptutor_knowledge-1.7.0.dist-info/WHEEL +5 -0
  168. deeptutor_knowledge-1.7.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,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"]