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,448 @@
1
+ #!/usr/bin/env python
2
+ """Knowledge base initialization through the selected RAG provider."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import asyncio
8
+ from datetime import datetime
9
+ import json
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Optional
13
+
14
+ if TYPE_CHECKING:
15
+ from deeptutor.multi_user.models import CurrentUser
16
+ from deeptutor.services.rag.pipelines.lightrag.indexing_policy import IndexingPolicySnapshot
17
+
18
+ from deeptutor.knowledge.naming import validate_knowledge_base_name
19
+ from deeptutor.knowledge.progress_tracker import ProgressStage, ProgressTracker
20
+ from deeptutor.knowledge.staging import stage_source
21
+ from deeptutor.services.config import resolve_llm_runtime_config
22
+ from deeptutor.services.file_io import atomic_write_json
23
+ from deeptutor.services.rag.factory import normalize_provider_name
24
+ from deeptutor.services.rag.service import RAGService
25
+ from deeptutor.services.setup.data_volume import (
26
+ DataVolumePermissionError,
27
+ ensure_data_volume_writable,
28
+ format_data_volume_permission_error,
29
+ )
30
+ from deeptutor.utils.file_types import FileTypeRouter
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class KnowledgeBaseInitializer:
36
+ """Knowledge base initializer."""
37
+
38
+ def __init__(
39
+ self,
40
+ kb_name: str,
41
+ base_dir: str = "./data/knowledge_bases",
42
+ api_key: str | None = None,
43
+ base_url: str | None = None,
44
+ progress_tracker: ProgressTracker | None = None,
45
+ rag_provider: str | None = None,
46
+ indexing_snapshot: IndexingPolicySnapshot | None = None,
47
+ owner: CurrentUser | None = None,
48
+ ):
49
+ self.kb_name = validate_knowledge_base_name(kb_name)
50
+ self.base_dir = Path(base_dir)
51
+ self.kb_dir = self.base_dir / self.kb_name
52
+
53
+ self.raw_dir = self.kb_dir / "raw"
54
+ self.llamaindex_storage_dir = self.kb_dir / "llamaindex_storage"
55
+
56
+ self.api_key = api_key
57
+ self.base_url = base_url
58
+ self.progress_tracker = progress_tracker or ProgressTracker(self.kb_name, self.base_dir)
59
+ self.rag_provider = normalize_provider_name(rag_provider)
60
+ self.indexing_snapshot = indexing_snapshot
61
+ self.owner = owner
62
+ self.index_published = False
63
+
64
+ def _register_to_config(self) -> None:
65
+ """Register KB in kb_config.json with initializing state."""
66
+ try:
67
+ from deeptutor.knowledge.manager import KnowledgeBaseManager
68
+
69
+ manager = KnowledgeBaseManager(base_dir=str(self.base_dir))
70
+ manager.config = manager._load_config()
71
+ if self.kb_name in manager.config.get("knowledge_bases", {}):
72
+ return
73
+
74
+ manager.update_kb_status(
75
+ name=self.kb_name,
76
+ status="initializing",
77
+ progress={
78
+ "stage": "initializing",
79
+ "message": "Creating directory structure...",
80
+ "percent": 0,
81
+ "current": 0,
82
+ "total": 0,
83
+ },
84
+ )
85
+ manager.config = manager._load_config()
86
+ manager.config.setdefault("knowledge_bases", {}).setdefault(self.kb_name, {})[
87
+ "rag_provider"
88
+ ] = self.rag_provider
89
+ manager._save_config()
90
+ except Exception as e:
91
+ logger.warning(f"Failed to register KB to config: {e}")
92
+
93
+ def _update_metadata_with_provider(self, provider: str) -> None:
94
+ metadata_file = self.kb_dir / "metadata.json"
95
+ metadata: dict = {}
96
+ if metadata_file.exists():
97
+ try:
98
+ with open(metadata_file, "r", encoding="utf-8") as f:
99
+ metadata = json.load(f)
100
+ except Exception:
101
+ metadata = {}
102
+
103
+ metadata["rag_provider"] = provider
104
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
105
+ metadata["last_updated"] = timestamp
106
+ metadata["last_indexed_at"] = timestamp
107
+ metadata["last_indexed_count"] = len(
108
+ FileTypeRouter.collect_supported_files(self.raw_dir, recursive=True)
109
+ )
110
+ if provider == "llamaindex":
111
+ from deeptutor.knowledge.indexing_run import load_run
112
+
113
+ receipt = load_run(self.kb_dir)
114
+ if receipt:
115
+ metadata["last_indexed_count"] = sum(
116
+ doc.get("status") == "completed" for doc in receipt["documents"].values()
117
+ )
118
+ metadata["indexing_state"] = receipt["state"]
119
+ metadata["last_indexed_action"] = "create"
120
+
121
+ atomic_write_json(metadata_file, metadata)
122
+
123
+ try:
124
+ from deeptutor.services.config import get_kb_config_service
125
+
126
+ service = get_kb_config_service()
127
+ service.set_rag_provider(self.kb_name, provider)
128
+ service.set_kb_config(self.kb_name, {"needs_reindex": False})
129
+ except Exception as config_err:
130
+ logger.warning(f"Failed to persist provider in centralized config: {config_err}")
131
+
132
+ def create_directory_structure(self) -> None:
133
+ """Create KB directory structure."""
134
+ logger.info(f"Creating directory structure for knowledge base: {self.kb_name}")
135
+ ensure_data_volume_writable(self.base_dir)
136
+
137
+ for dir_path in [
138
+ self.raw_dir,
139
+ ]:
140
+ dir_path.mkdir(parents=True, exist_ok=True)
141
+
142
+ metadata = {
143
+ "name": self.kb_name,
144
+ "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
145
+ "description": f"Knowledge base: {self.kb_name}",
146
+ "version": "1.0",
147
+ "rag_provider": self.rag_provider,
148
+ "needs_reindex": False,
149
+ }
150
+
151
+ atomic_write_json(self.kb_dir / "metadata.json", metadata)
152
+
153
+ self._register_to_config()
154
+
155
+ def copy_documents(self, source_files: list[str]) -> list[str]:
156
+ """Stage source documents into the raw directory.
157
+
158
+ Sources arrive flattened to their basename, so a docs tree holding one
159
+ ``full.md`` per volume wants the same path over and over. Each document
160
+ is kept under a distinct name instead of overwriting the one before it
161
+ (#2041) — the same rule the incremental add path already followed.
162
+ """
163
+ copied_files: list[str] = []
164
+ staged: set[Path] = set()
165
+ for source in source_files:
166
+ source_path = Path(source)
167
+ if not source_path.exists() or not source_path.is_file():
168
+ logger.warning(f"Source file not found: {source}")
169
+ continue
170
+ staged_path, copied = stage_source(source_path, self.raw_dir / source_path.name)
171
+ if staged_path in staged:
172
+ continue
173
+ staged.add(staged_path)
174
+ copied_files.append(str(staged_path))
175
+ if copied and staged_path.name != source_path.name:
176
+ logger.warning(f"Name taken by another document, staged as {staged_path}")
177
+ return copied_files
178
+
179
+ async def process_documents(
180
+ self,
181
+ ) -> bool:
182
+ """Process documents with the KB's bound provider."""
183
+ provider = self.rag_provider
184
+
185
+ self.progress_tracker.update(
186
+ ProgressStage.PROCESSING_DOCUMENTS,
187
+ message_key="Starting to process documents with {{provider}} provider...",
188
+ message_params={"provider": provider},
189
+ current=0,
190
+ total=0,
191
+ )
192
+
193
+ # recursive=True so documents organized into folders are indexed too
194
+ # (folders are display-only and don't otherwise affect retrieval).
195
+ doc_files = FileTypeRouter.collect_supported_files(self.raw_dir, recursive=True)
196
+
197
+ if not doc_files:
198
+ self.progress_tracker.update(
199
+ ProgressStage.ERROR,
200
+ message_key="No documents found to process",
201
+ error="No documents found",
202
+ )
203
+ raise ValueError("No documents found to process")
204
+
205
+ self.progress_tracker.update(
206
+ ProgressStage.PROCESSING_DOCUMENTS,
207
+ message_key="Found {{count}} documents, starting to process...",
208
+ message_params={"count": len(doc_files)},
209
+ current=0,
210
+ total=len(doc_files),
211
+ )
212
+
213
+ rag_service = RAGService(
214
+ kb_base_dir=str(self.base_dir),
215
+ provider=provider,
216
+ )
217
+ file_paths = [str(doc_file) for doc_file in doc_files]
218
+
219
+ def _on_progress(batch_num, total_batches):
220
+ self.progress_tracker.update(
221
+ ProgressStage.PROCESSING_DOCUMENTS,
222
+ message_key="Embedding batches: {{current}}/{{total}} complete",
223
+ message_params={"current": batch_num, "total": total_batches},
224
+ current=batch_num,
225
+ total=total_batches,
226
+ )
227
+
228
+ def _on_image_progress(current: int, total: int):
229
+ self.progress_tracker.update(
230
+ ProgressStage.PROCESSING_DOCUMENTS,
231
+ message_key="Describing images: {{current}}/{{total}}",
232
+ message_params={"current": current, "total": total},
233
+ current=current,
234
+ total=total,
235
+ )
236
+
237
+ def _prepare_publication(candidate_root: Path) -> None:
238
+ self._update_metadata_with_provider(provider)
239
+ self.progress_tracker.update(
240
+ ProgressStage.COMPLETED,
241
+ message_key="Knowledge base initialization complete!",
242
+ current=1,
243
+ total=1,
244
+ indexed_count=len(doc_files),
245
+ index_changed=True,
246
+ index_action="create",
247
+ publication_version=candidate_root.name,
248
+ )
249
+ self.progress_tracker.verify_terminal(
250
+ current=1,
251
+ total=1,
252
+ indexed_count=len(doc_files),
253
+ index_action="create",
254
+ publication_version=candidate_root.name,
255
+ )
256
+
257
+ try:
258
+ success = await rag_service.initialize(
259
+ kb_name=self.kb_name,
260
+ file_paths=file_paths,
261
+ task_id=self.progress_tracker.task_id,
262
+ progress_callback=_on_progress,
263
+ image_progress_callback=_on_image_progress,
264
+ indexing_snapshot=self.indexing_snapshot,
265
+ before_publish=_prepare_publication if provider == "lightrag" else None,
266
+ )
267
+ if not success:
268
+ self.progress_tracker.update(
269
+ ProgressStage.ERROR,
270
+ message_key="Document processing failed",
271
+ error="RAG pipeline returned failure",
272
+ )
273
+ raise RuntimeError("RAG pipeline returned failure")
274
+
275
+ self.index_published = provider == "lightrag"
276
+ try:
277
+ if provider != "lightrag":
278
+ self._update_metadata_with_provider(provider)
279
+ self.progress_tracker.update(
280
+ ProgressStage.PROCESSING_DOCUMENTS,
281
+ message_key="Documents processed successfully",
282
+ current=len(doc_files),
283
+ total=len(doc_files),
284
+ )
285
+ except Exception:
286
+ if not self.index_published:
287
+ raise
288
+ logger.warning(
289
+ "LightRAG index for %s was published before metadata bookkeeping failed",
290
+ self.kb_name,
291
+ exc_info=True,
292
+ )
293
+ except PermissionError as e:
294
+ error_msg = format_data_volume_permission_error(self.kb_dir, cause=e)
295
+ logger.error("Error processing documents: %s", error_msg)
296
+ self.progress_tracker.update(
297
+ ProgressStage.ERROR,
298
+ message_key="Failed to process documents",
299
+ error=error_msg,
300
+ )
301
+ raise DataVolumePermissionError(error_msg) from e
302
+ except Exception as e:
303
+ error_msg = str(e)
304
+ logger.error(f"Error processing documents: {error_msg}")
305
+ self.progress_tracker.update(
306
+ ProgressStage.ERROR,
307
+ message_key="Failed to process documents",
308
+ error=error_msg,
309
+ )
310
+ raise
311
+
312
+ try:
313
+ await self.fix_structure()
314
+ await self.display_statistics_generic()
315
+ except Exception:
316
+ if not self.index_published:
317
+ raise
318
+ logger.warning(
319
+ "LightRAG index for %s was published before statistics bookkeeping failed",
320
+ self.kb_name,
321
+ exc_info=True,
322
+ )
323
+ return True
324
+
325
+ async def fix_structure(self) -> None:
326
+ """No-op retained for compatibility with previous pipelines."""
327
+ logger.info("Skipping legacy structure cleanup")
328
+
329
+ async def display_statistics_generic(self) -> None:
330
+ """Display basic statistics."""
331
+ raw_files = list(self.raw_dir.glob("*")) if self.raw_dir.exists() else []
332
+ from deeptutor.services.rag.index_probe import inspect_kb_versions
333
+
334
+ index_versions = inspect_kb_versions(self.kb_dir, self.rag_provider)
335
+
336
+ logger.info("=" * 50)
337
+ logger.info("Knowledge Base Statistics")
338
+ logger.info("=" * 50)
339
+ logger.info(f"Raw documents: {len(raw_files)}")
340
+ logger.info(f"Index versions: {len(index_versions)}")
341
+ logger.info(f"Provider used: {self.rag_provider}")
342
+ logger.info("=" * 50)
343
+
344
+
345
+ async def initialize_knowledge_base(
346
+ kb_name: str,
347
+ source_files: list[str],
348
+ base_dir: str = "./data/knowledge_bases",
349
+ api_key: Optional[str] = None,
350
+ base_url: Optional[str] = None,
351
+ rag_provider: Optional[str] = None,
352
+ ) -> bool:
353
+ """Convenience initializer used by CLI wrappers."""
354
+ from deeptutor.knowledge.manager import KnowledgeBaseManager
355
+
356
+ manager = KnowledgeBaseManager(base_dir=base_dir)
357
+ initializer = KnowledgeBaseInitializer(
358
+ kb_name=kb_name,
359
+ base_dir=base_dir,
360
+ api_key=api_key,
361
+ base_url=base_url,
362
+ rag_provider=rag_provider,
363
+ )
364
+ try:
365
+ initializer.create_directory_structure()
366
+ copied_files = initializer.copy_documents(source_files)
367
+ await initializer.process_documents()
368
+ manager.update_kb_status(
369
+ name=kb_name,
370
+ status="ready",
371
+ progress={
372
+ "stage": "completed",
373
+ "message": "Knowledge base initialization complete!",
374
+ "percent": 100,
375
+ "current": 1,
376
+ "total": 1,
377
+ "file_name": "",
378
+ "error": None,
379
+ "timestamp": datetime.now().isoformat(),
380
+ "indexed_count": len(copied_files),
381
+ "index_changed": True,
382
+ "index_action": "create",
383
+ },
384
+ )
385
+ return True
386
+ except Exception as exc:
387
+ manager.update_kb_status(
388
+ name=kb_name,
389
+ status="error",
390
+ progress={
391
+ "stage": "error",
392
+ "message": "Knowledge base initialization failed",
393
+ "percent": 0,
394
+ "current": 0,
395
+ "total": 1,
396
+ "file_name": "",
397
+ "error": str(exc),
398
+ "timestamp": datetime.now().isoformat(),
399
+ },
400
+ )
401
+ raise
402
+
403
+
404
+ async def main() -> None:
405
+ try:
406
+ llm_config = resolve_llm_runtime_config()
407
+ default_api_key = llm_config.api_key
408
+ default_base_url = llm_config.effective_url
409
+ except Exception:
410
+ default_api_key = ""
411
+ default_base_url = ""
412
+
413
+ parser = argparse.ArgumentParser(description="Initialize a new knowledge base from documents")
414
+ parser.add_argument("name", help="Knowledge base name")
415
+ parser.add_argument("--docs", nargs="+", help="Document files to process")
416
+ parser.add_argument("--docs-dir", help="Directory containing documents to process")
417
+ parser.add_argument("--base-dir", default="./knowledge_bases")
418
+ parser.add_argument("--api-key", default=default_api_key)
419
+ parser.add_argument("--base-url", default=default_base_url)
420
+ parser.add_argument("--skip-processing", action="store_true")
421
+
422
+ args = parser.parse_args()
423
+
424
+ doc_files: list[str] = []
425
+ if args.docs:
426
+ doc_files.extend(args.docs)
427
+ if args.docs_dir:
428
+ docs_dir = Path(args.docs_dir)
429
+ if docs_dir.exists() and docs_dir.is_dir():
430
+ doc_files.extend(str(f) for f in FileTypeRouter.collect_supported_files(docs_dir))
431
+
432
+ initializer = KnowledgeBaseInitializer(
433
+ kb_name=args.name,
434
+ base_dir=args.base_dir,
435
+ api_key=args.api_key,
436
+ base_url=args.base_url,
437
+ )
438
+ initializer.create_directory_structure()
439
+
440
+ if doc_files:
441
+ initializer.copy_documents(doc_files)
442
+
443
+ if not args.skip_processing:
444
+ await initializer.process_documents()
445
+
446
+
447
+ if __name__ == "__main__":
448
+ asyncio.run(main())