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,327 @@
1
+ """Provider-owned index readiness probes.
2
+
3
+ DeepTutor owns KB lifecycle/status, but each RAG provider owns the shape of its
4
+ persisted index. This module is the narrow read-only seam between those worlds:
5
+ callers ask "is this provider index really queryable?" and get a structured
6
+ answer without knowing provider-specific filenames.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from functools import lru_cache
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from deeptutor.services.rag.factory import (
18
+ DEFAULT_PROVIDER,
19
+ GRAPHRAG_PROVIDER,
20
+ LIGHTRAG_PROVIDER,
21
+ PAGEINDEX_OSS_PROVIDER,
22
+ PAGEINDEX_PROVIDER,
23
+ normalize_provider_name,
24
+ version_matches_provider,
25
+ )
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ProviderIndexProbe:
30
+ """Read-only verdict for one provider storage directory."""
31
+
32
+ provider: str
33
+ storage_dir: str | None
34
+ ready: bool
35
+ failure_summary: str = ""
36
+ doc_count: int | None = None
37
+ diagnostics: dict[str, Any] = field(default_factory=dict)
38
+
39
+
40
+ def inspect_provider_index(
41
+ provider: str | None, storage_dir: str | Path | None
42
+ ) -> ProviderIndexProbe:
43
+ """Inspect one provider storage directory using real provider artifacts."""
44
+ resolved = normalize_provider_name(provider)
45
+ path = Path(storage_dir) if storage_dir is not None else None
46
+ if path is None:
47
+ return ProviderIndexProbe(resolved, None, False, "No storage path recorded.")
48
+ if resolved in {PAGEINDEX_PROVIDER, PAGEINDEX_OSS_PROVIDER}:
49
+ return _inspect_pageindex(path, resolved)
50
+ if resolved == GRAPHRAG_PROVIDER:
51
+ return _inspect_graphrag(path)
52
+ if resolved == LIGHTRAG_PROVIDER:
53
+ return _inspect_lightrag(path)
54
+ return _inspect_llamaindex(path)
55
+
56
+
57
+ def inspect_provider_version(entry: dict[str, Any], provider: str | None) -> ProviderIndexProbe:
58
+ """Inspect a version-list entry for ``provider``."""
59
+ resolved = normalize_provider_name(provider)
60
+ storage_path = entry.get("storage_path")
61
+ if not storage_path:
62
+ return ProviderIndexProbe(resolved, None, False, "No storage path recorded.")
63
+ if not version_matches_provider(entry, resolved):
64
+ return ProviderIndexProbe(
65
+ resolved,
66
+ str(storage_path),
67
+ False,
68
+ diagnostics={
69
+ "version_provider": entry.get("provider"),
70
+ "version_signature": entry.get("signature"),
71
+ "provider_mismatch": True,
72
+ },
73
+ )
74
+ return inspect_provider_index(resolved, Path(str(storage_path)))
75
+
76
+
77
+ def inspect_kb_versions(kb_dir: str | Path, provider: str | None) -> list[dict[str, Any]]:
78
+ """Return version entries annotated with provider-probe readiness."""
79
+ from deeptutor.services.rag.index_versioning import list_kb_versions
80
+
81
+ versions: list[dict[str, Any]] = []
82
+ for entry in list_kb_versions(Path(kb_dir)):
83
+ adjusted = dict(entry)
84
+ probe = inspect_provider_version(adjusted, provider)
85
+ adjusted["ready"] = probe.ready
86
+ if probe.failure_summary:
87
+ adjusted["failure_summary"] = probe.failure_summary
88
+ if probe.doc_count is not None:
89
+ adjusted["doc_count"] = probe.doc_count
90
+ if probe.diagnostics:
91
+ adjusted["probe_diagnostics"] = probe.diagnostics
92
+ versions.append(adjusted)
93
+ return versions
94
+
95
+
96
+ def latest_ready_provider_version(
97
+ kb_dir: str | Path, provider: str | None
98
+ ) -> dict[str, Any] | None:
99
+ """Return the newest provider-ready version, if any."""
100
+ for entry in inspect_kb_versions(kb_dir, provider):
101
+ if entry.get("ready"):
102
+ return entry
103
+ return None
104
+
105
+
106
+ def has_ready_provider_index(kb_dir: str | Path, provider: str | None) -> bool:
107
+ """Return whether ``kb_dir`` has a genuinely ready index for ``provider``."""
108
+ return latest_ready_provider_version(kb_dir, provider) is not None
109
+
110
+
111
+ def provider_failure_summary(
112
+ kb_dir: str | Path,
113
+ provider: str | None,
114
+ *,
115
+ limit: int = 3,
116
+ versions: list[dict[str, Any]] | None = None,
117
+ ) -> str:
118
+ """Return the first provider-specific failure summary under ``kb_dir``.
119
+
120
+ ``versions`` may carry a pre-computed :func:`inspect_kb_versions` result so
121
+ bulk callers (e.g. ``KnowledgeBaseManager.get_info``) do not rescan and
122
+ re-parse every index version just to collect failure text.
123
+
124
+ """
125
+ entries = versions if versions is not None else inspect_kb_versions(kb_dir, provider)
126
+ failures: list[str] = []
127
+ for entry in entries:
128
+ summary = str(entry.get("failure_summary") or "").strip()
129
+ if summary:
130
+ failures.append(summary)
131
+ if len(failures) >= limit:
132
+ break
133
+ return "; ".join(failures[:limit])
134
+
135
+
136
+ def _inspect_llamaindex(storage_dir: Path) -> ProviderIndexProbe:
137
+ diagnostics: dict[str, Any] = {}
138
+ if not storage_dir.is_dir():
139
+ return ProviderIndexProbe(
140
+ DEFAULT_PROVIDER,
141
+ str(storage_dir),
142
+ False,
143
+ "LlamaIndex storage directory does not exist.",
144
+ )
145
+
146
+ if (storage_dir / ".building.json").exists():
147
+ meta = _read_json(storage_dir / "meta.json")
148
+ if not meta or meta.get("state") != "published" or not meta.get("readiness_verified"):
149
+ return ProviderIndexProbe(
150
+ DEFAULT_PROVIDER,
151
+ str(storage_dir),
152
+ False,
153
+ "Candidate index has not passed persistence verification.",
154
+ )
155
+ docstore = storage_dir / "docstore.json"
156
+ index_store = storage_dir / "index_store.json"
157
+ vector_stores = sorted(path.name for path in storage_dir.glob("*vector_store.json"))
158
+ diagnostics["vector_stores"] = vector_stores
159
+
160
+ if not docstore.exists():
161
+ return ProviderIndexProbe(
162
+ DEFAULT_PROVIDER,
163
+ str(storage_dir),
164
+ False,
165
+ "Missing LlamaIndex docstore.json.",
166
+ diagnostics=diagnostics,
167
+ )
168
+ if not index_store.exists():
169
+ return ProviderIndexProbe(
170
+ DEFAULT_PROVIDER,
171
+ str(storage_dir),
172
+ False,
173
+ "Missing LlamaIndex index_store.json.",
174
+ doc_count=_llamaindex_doc_count(docstore),
175
+ diagnostics=diagnostics,
176
+ )
177
+
178
+ return ProviderIndexProbe(
179
+ DEFAULT_PROVIDER,
180
+ str(storage_dir),
181
+ True,
182
+ doc_count=_llamaindex_doc_count(docstore),
183
+ diagnostics=diagnostics,
184
+ )
185
+
186
+
187
+ def _inspect_pageindex(storage_dir: Path, provider: str) -> ProviderIndexProbe:
188
+ from deeptutor.services.rag.pipelines.pageindex import storage
189
+
190
+ manifest = storage.read_manifest(storage_dir, provider=provider)
191
+ ids = storage.doc_ids(manifest)
192
+ if not ids:
193
+ return ProviderIndexProbe(
194
+ provider,
195
+ str(storage_dir),
196
+ False,
197
+ "PageIndex manifest has no document ids.",
198
+ doc_count=0,
199
+ )
200
+ if provider == PAGEINDEX_OSS_PROVIDER:
201
+ docs_dir = storage.sdk_storage_path(storage_dir) / "docs"
202
+ missing = [
203
+ doc_id
204
+ for doc_id in ids
205
+ if not all(
206
+ (docs_dir / doc_id / name).is_file()
207
+ for name in ("doc.json", "tree.json", "pages.json")
208
+ )
209
+ ]
210
+ if missing:
211
+ return ProviderIndexProbe(
212
+ provider,
213
+ str(storage_dir),
214
+ False,
215
+ "PageIndex OSS Local Library is missing document artifacts.",
216
+ doc_count=len(ids) - len(missing),
217
+ diagnostics={"missing_doc_ids": missing[:10]},
218
+ )
219
+ return ProviderIndexProbe(
220
+ provider,
221
+ str(storage_dir),
222
+ True,
223
+ doc_count=len(ids),
224
+ diagnostics={"manifest": str(storage.manifest_path(storage_dir))},
225
+ )
226
+
227
+
228
+ def _inspect_graphrag(storage_dir: Path) -> ProviderIndexProbe:
229
+ from deeptutor.services.rag.pipelines.graphrag import storage
230
+
231
+ out = storage.output_dir(storage_dir)
232
+ tables = [name for name in storage.OUTPUT_TABLES if (out / f"{name}.parquet").exists()]
233
+ if not storage.has_output(storage_dir):
234
+ return ProviderIndexProbe(
235
+ GRAPHRAG_PROVIDER,
236
+ str(storage_dir),
237
+ False,
238
+ "GraphRAG output has no core parquet tables.",
239
+ diagnostics={"output_tables": tables},
240
+ )
241
+ return ProviderIndexProbe(
242
+ GRAPHRAG_PROVIDER,
243
+ str(storage_dir),
244
+ True,
245
+ diagnostics={"output_tables": tables},
246
+ )
247
+
248
+
249
+ def _inspect_lightrag(storage_dir: Path) -> ProviderIndexProbe:
250
+ from deeptutor.services.rag.pipelines.lightrag import storage
251
+
252
+ failure = storage.failure_summary(storage_dir)
253
+ ready = storage.has_output(storage_dir)
254
+ return ProviderIndexProbe(
255
+ LIGHTRAG_PROVIDER,
256
+ str(storage_dir),
257
+ ready,
258
+ failure_summary="" if ready else failure,
259
+ doc_count=_lightrag_doc_count(storage_dir),
260
+ )
261
+
262
+
263
+ # docstore.json holds one entry per chunk/node and can be multi-MB, so parsing
264
+ # it is the dominant cost of a LlamaIndex readiness probe. The count only
265
+ # changes when the file itself changes, so it is cached keyed on size +
266
+ # mtime_ns (same freshness convention as storage._freshness_token).
267
+ _DOCSTORE_COUNT_CACHE_SIZE = 128
268
+
269
+
270
+ @lru_cache(maxsize=_DOCSTORE_COUNT_CACHE_SIZE)
271
+ def _llamaindex_doc_count_cached(path: str, size: int, mtime_ns: int) -> int | None:
272
+ payload = _read_json(Path(path))
273
+ if not isinstance(payload, dict):
274
+ return None
275
+ data = payload.get("docstore/data")
276
+ return len(data) if isinstance(data, dict) else None
277
+
278
+
279
+ def _llamaindex_doc_count(docstore_path: Path) -> int | None:
280
+ try:
281
+ stat = docstore_path.stat()
282
+ except OSError:
283
+ return None
284
+ return _llamaindex_doc_count_cached(str(docstore_path), stat.st_size, stat.st_mtime_ns)
285
+
286
+
287
+ def _lightrag_doc_count(storage_dir: Path) -> int | None:
288
+ payload = _read_json(storage_dir / "kv_store_doc_status.json")
289
+ if not isinstance(payload, dict):
290
+ return None
291
+ count = 0
292
+ for item in payload.values():
293
+ if not isinstance(item, dict):
294
+ continue
295
+ chunks = item.get("chunks_list")
296
+ status = str(item.get("status") or "").lower()
297
+ if (isinstance(chunks, list) and chunks) or status in {
298
+ "processed",
299
+ "completed",
300
+ "done",
301
+ "success",
302
+ "indexed",
303
+ }:
304
+ count += 1
305
+ return count
306
+
307
+
308
+ def _read_json(path: Path) -> dict[str, Any] | None:
309
+ if not path.exists():
310
+ return None
311
+ try:
312
+ with open(path, encoding="utf-8") as handle:
313
+ payload = json.load(handle)
314
+ return payload if isinstance(payload, dict) else None
315
+ except Exception:
316
+ return None
317
+
318
+
319
+ __all__ = [
320
+ "ProviderIndexProbe",
321
+ "inspect_provider_index",
322
+ "inspect_provider_version",
323
+ "inspect_kb_versions",
324
+ "latest_ready_provider_version",
325
+ "has_ready_provider_index",
326
+ "provider_failure_summary",
327
+ ]