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,266 @@
|
|
|
1
|
+
"""Versioned MinerU block policy for LightRAG indexing.
|
|
2
|
+
|
|
3
|
+
MinerU's legacy ``content_list.json`` mixes semantic document content with
|
|
4
|
+
page-layout helpers. DeepTutor retains the raw parser cache for audit and
|
|
5
|
+
derives an independent, deep-copied list before the native LightRAG Sidecar
|
|
6
|
+
bridge decides which parser blocks are semantic.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from copy import deepcopy
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
import hashlib
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any
|
|
18
|
+
import uuid
|
|
19
|
+
|
|
20
|
+
from deeptutor.services.file_io import atomic_write_json
|
|
21
|
+
|
|
22
|
+
POLICY_ID = "mineru-legacy-content-list-v1"
|
|
23
|
+
LEDGER_DIRNAME = "deeptutor_ingestion_audit"
|
|
24
|
+
ATTEMPT_LEDGER_DIRNAME = "deeptutor_ingestion_attempts"
|
|
25
|
+
|
|
26
|
+
FILTERED_LAYOUT_TYPES = frozenset({"footer", "header", "page_number"})
|
|
27
|
+
PRESERVED_AUXILIARY_TYPES = frozenset({"aside_text", "page_footnote"})
|
|
28
|
+
PRESERVED_SEMANTIC_TYPES = frozenset(
|
|
29
|
+
{"chart", "code", "equation", "image", "list", "table", "text"}
|
|
30
|
+
)
|
|
31
|
+
PRESERVED_TYPES = PRESERVED_AUXILIARY_TYPES | PRESERVED_SEMANTIC_TYPES
|
|
32
|
+
MULTIMODAL_TYPES = PRESERVED_TYPES - {"text"}
|
|
33
|
+
|
|
34
|
+
# MinerU renamed its auxiliary block types between output schemas: what the
|
|
35
|
+
# legacy ``content_list.json`` calls ``header`` a newer parser emits as
|
|
36
|
+
# ``page_header``. Map the newer names back onto the vocabulary the policy is
|
|
37
|
+
# written in, so upgrading MinerU keeps classifying blocks instead of
|
|
38
|
+
# presenting every layout block as an unrecognized type.
|
|
39
|
+
V2_AUXILIARY_EQUIVALENTS = {
|
|
40
|
+
"aside_text": "page_aside_text",
|
|
41
|
+
"footer": "page_footer",
|
|
42
|
+
"header": "page_header",
|
|
43
|
+
"page_footnote": "page_footnote",
|
|
44
|
+
"page_number": "page_number",
|
|
45
|
+
}
|
|
46
|
+
_V2_TO_LEGACY_TYPE = {
|
|
47
|
+
v2_name: legacy_name
|
|
48
|
+
for legacy_name, v2_name in V2_AUXILIARY_EQUIVALENTS.items()
|
|
49
|
+
if v2_name != legacy_name
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_SAFE_TYPE = re.compile(r"[a-z][a-z0-9_]{0,63}\Z")
|
|
53
|
+
_INVALID_TYPE = "<invalid>"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class BlockPolicyDecision:
|
|
58
|
+
"""Sanitized policy outcome plus an independent list for indexing."""
|
|
59
|
+
|
|
60
|
+
content_list: list[dict[str, Any]]
|
|
61
|
+
ledger: dict[str, Any] | None
|
|
62
|
+
unknown_type_counts: tuple[tuple[str, int], ...] = ()
|
|
63
|
+
|
|
64
|
+
def unknown_summary(self) -> str:
|
|
65
|
+
"""``type=count`` rundown of block types the policy does not know."""
|
|
66
|
+
return ", ".join(
|
|
67
|
+
f"{content_type}={count}" for content_type, count in self.unknown_type_counts
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _sanitized_type(block: dict[str, Any]) -> str:
|
|
72
|
+
value = block.get("type")
|
|
73
|
+
if isinstance(value, str) and _SAFE_TYPE.fullmatch(value):
|
|
74
|
+
return value
|
|
75
|
+
return _INVALID_TYPE
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _sorted_counts(counter: Counter[str]) -> dict[str, int]:
|
|
79
|
+
return dict(sorted(counter.items()))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _page_type_key(content_type: str, block: dict[str, Any]) -> str | None:
|
|
83
|
+
page_idx = block.get("page_idx")
|
|
84
|
+
if isinstance(page_idx, int) and page_idx >= 0:
|
|
85
|
+
return f"{content_type}:{page_idx}"
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def prepare_content_list(
|
|
90
|
+
blocks: list[dict[str, Any]],
|
|
91
|
+
*,
|
|
92
|
+
engine: str,
|
|
93
|
+
source_hash: str,
|
|
94
|
+
parser_signature: str,
|
|
95
|
+
) -> BlockPolicyDecision:
|
|
96
|
+
"""Derive the indexable content list without mutating parser-owned blocks.
|
|
97
|
+
|
|
98
|
+
Only MinerU's legacy structured output is classified. Other parse engines
|
|
99
|
+
retain their existing behavior because their block vocabularies have
|
|
100
|
+
independent contracts.
|
|
101
|
+
"""
|
|
102
|
+
if engine != "mineru":
|
|
103
|
+
return BlockPolicyDecision(content_list=blocks, ledger=None)
|
|
104
|
+
|
|
105
|
+
raw_counts: Counter[str] = Counter()
|
|
106
|
+
filtered_counts: Counter[str] = Counter()
|
|
107
|
+
eligible_counts: Counter[str] = Counter()
|
|
108
|
+
eligible_multimodal_page_counts: Counter[str] = Counter()
|
|
109
|
+
unknown_counts: Counter[str] = Counter()
|
|
110
|
+
indexable: list[dict[str, Any]] = []
|
|
111
|
+
|
|
112
|
+
for raw_block in blocks:
|
|
113
|
+
if not isinstance(raw_block, dict):
|
|
114
|
+
raw_counts[_INVALID_TYPE] += 1
|
|
115
|
+
unknown_counts[_INVALID_TYPE] += 1
|
|
116
|
+
continue
|
|
117
|
+
block = raw_block
|
|
118
|
+
content_type = _sanitized_type(block)
|
|
119
|
+
content_type = _V2_TO_LEGACY_TYPE.get(content_type, content_type)
|
|
120
|
+
raw_counts[content_type] += 1
|
|
121
|
+
if content_type in FILTERED_LAYOUT_TYPES:
|
|
122
|
+
filtered_counts[content_type] += 1
|
|
123
|
+
continue
|
|
124
|
+
if content_type not in PRESERVED_TYPES:
|
|
125
|
+
# Filter only what we positively recognize as page chrome; index
|
|
126
|
+
# anything else. A MinerU release that adds a block type must not
|
|
127
|
+
# silently drop its content, nor abort the whole ingest — before
|
|
128
|
+
# this policy existed every block was indexed, and an unrecognized
|
|
129
|
+
# type is far more likely to be new content than new chrome.
|
|
130
|
+
unknown_counts[content_type] += 1
|
|
131
|
+
|
|
132
|
+
eligible_counts[content_type] += 1
|
|
133
|
+
if content_type in MULTIMODAL_TYPES:
|
|
134
|
+
page_type_key = _page_type_key(content_type, block)
|
|
135
|
+
if page_type_key is not None:
|
|
136
|
+
eligible_multimodal_page_counts[page_type_key] += 1
|
|
137
|
+
indexable.append(deepcopy(block))
|
|
138
|
+
|
|
139
|
+
ledger: dict[str, Any] = {
|
|
140
|
+
"schema_version": 1,
|
|
141
|
+
"policy": {
|
|
142
|
+
"id": POLICY_ID,
|
|
143
|
+
"input_schema": "legacy-content-list",
|
|
144
|
+
"filtered_layout_types": sorted(FILTERED_LAYOUT_TYPES),
|
|
145
|
+
"preserved_auxiliary_types": sorted(PRESERVED_AUXILIARY_TYPES),
|
|
146
|
+
"preserved_semantic_types": sorted(PRESERVED_SEMANTIC_TYPES),
|
|
147
|
+
"v2_auxiliary_equivalents": dict(sorted(V2_AUXILIARY_EQUIVALENTS.items())),
|
|
148
|
+
"v2_auxiliary_normalized": True,
|
|
149
|
+
},
|
|
150
|
+
"parser": {
|
|
151
|
+
"engine": engine,
|
|
152
|
+
"source_hash": source_hash,
|
|
153
|
+
"parser_signature": parser_signature,
|
|
154
|
+
},
|
|
155
|
+
"counts": {
|
|
156
|
+
"raw_total": len(blocks),
|
|
157
|
+
"raw_by_type": _sorted_counts(raw_counts),
|
|
158
|
+
"filtered_total": sum(filtered_counts.values()),
|
|
159
|
+
"filtered_by_type": _sorted_counts(filtered_counts),
|
|
160
|
+
"eligible_total": len(indexable),
|
|
161
|
+
"eligible_by_type": _sorted_counts(eligible_counts),
|
|
162
|
+
"eligible_multimodal_total": sum(
|
|
163
|
+
count
|
|
164
|
+
for content_type, count in eligible_counts.items()
|
|
165
|
+
if content_type in MULTIMODAL_TYPES
|
|
166
|
+
),
|
|
167
|
+
"eligible_multimodal_by_type_and_page": _sorted_counts(eligible_multimodal_page_counts),
|
|
168
|
+
"unknown_total": sum(unknown_counts.values()),
|
|
169
|
+
"unknown_by_type": _sorted_counts(unknown_counts),
|
|
170
|
+
},
|
|
171
|
+
"invariants": {
|
|
172
|
+
"input_blocks_mutated": False,
|
|
173
|
+
"eligible_order_preserved": True,
|
|
174
|
+
"unknown_types_indexed": True,
|
|
175
|
+
"raw_parser_artifacts_mutated": False,
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
return BlockPolicyDecision(
|
|
179
|
+
content_list=indexable,
|
|
180
|
+
ledger=ledger,
|
|
181
|
+
unknown_type_counts=tuple(sorted(unknown_counts.items())),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def write_decision_ledger(
|
|
186
|
+
working_dir: Path,
|
|
187
|
+
document_id: str,
|
|
188
|
+
ledger: dict[str, Any],
|
|
189
|
+
*,
|
|
190
|
+
attempt_id: str | None = None,
|
|
191
|
+
) -> Path:
|
|
192
|
+
"""Persist the accepted decision that corresponds to the current index."""
|
|
193
|
+
document_id_sha256 = hashlib.sha256(document_id.encode()).hexdigest()
|
|
194
|
+
path = Path(working_dir) / LEDGER_DIRNAME / f"{document_id_sha256[:16]}.json"
|
|
195
|
+
decision = {
|
|
196
|
+
"ledger_role": "current-index",
|
|
197
|
+
"policy_outcome": "accepted",
|
|
198
|
+
}
|
|
199
|
+
if attempt_id is not None:
|
|
200
|
+
decision["attempt_id"] = attempt_id
|
|
201
|
+
atomic_write_json(
|
|
202
|
+
path,
|
|
203
|
+
{
|
|
204
|
+
**ledger,
|
|
205
|
+
"document_id_sha256": document_id_sha256,
|
|
206
|
+
"decision": decision,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
return path
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def write_attempt_ledger(
|
|
213
|
+
kb_dir: Path,
|
|
214
|
+
document_id: str,
|
|
215
|
+
ledger: dict[str, Any],
|
|
216
|
+
*,
|
|
217
|
+
outcome: str,
|
|
218
|
+
) -> tuple[Path, str]:
|
|
219
|
+
"""Persist one immutable policy attempt outside a candidate version.
|
|
220
|
+
|
|
221
|
+
``unknown_types`` means the document indexed, but carried block types the
|
|
222
|
+
policy has no entry for — the signal to extend ``PRESERVED_TYPES`` or
|
|
223
|
+
``FILTERED_LAYOUT_TYPES``, not an ingest failure.
|
|
224
|
+
"""
|
|
225
|
+
if outcome not in {"accepted", "unknown_types"}:
|
|
226
|
+
raise ValueError(f"Unsupported policy outcome: {outcome}")
|
|
227
|
+
|
|
228
|
+
document_id_sha256 = hashlib.sha256(document_id.encode()).hexdigest()
|
|
229
|
+
attempt_id = uuid.uuid4().hex
|
|
230
|
+
policy = ledger.get("policy")
|
|
231
|
+
parser = ledger.get("parser")
|
|
232
|
+
policy_id = str(policy.get("id") or "") if isinstance(policy, dict) else ""
|
|
233
|
+
parser_signature = str(parser.get("parser_signature") or "") if isinstance(parser, dict) else ""
|
|
234
|
+
key_material = "\0".join((document_id_sha256, policy_id, parser_signature, outcome, attempt_id))
|
|
235
|
+
attempt_key_sha256 = hashlib.sha256(key_material.encode()).hexdigest()
|
|
236
|
+
path = Path(kb_dir) / ATTEMPT_LEDGER_DIRNAME / f"{attempt_key_sha256}.json"
|
|
237
|
+
atomic_write_json(
|
|
238
|
+
path,
|
|
239
|
+
{
|
|
240
|
+
**ledger,
|
|
241
|
+
"document_id_sha256": document_id_sha256,
|
|
242
|
+
"decision": {
|
|
243
|
+
"attempt_id": attempt_id,
|
|
244
|
+
"attempt_key_sha256": attempt_key_sha256,
|
|
245
|
+
"ledger_role": "attempt",
|
|
246
|
+
"policy_outcome": outcome,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
return path, attempt_id
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
__all__ = [
|
|
254
|
+
"ATTEMPT_LEDGER_DIRNAME",
|
|
255
|
+
"BlockPolicyDecision",
|
|
256
|
+
"FILTERED_LAYOUT_TYPES",
|
|
257
|
+
"LEDGER_DIRNAME",
|
|
258
|
+
"MULTIMODAL_TYPES",
|
|
259
|
+
"POLICY_ID",
|
|
260
|
+
"PRESERVED_AUXILIARY_TYPES",
|
|
261
|
+
"PRESERVED_SEMANTIC_TYPES",
|
|
262
|
+
"PRESERVED_TYPES",
|
|
263
|
+
"prepare_content_list",
|
|
264
|
+
"write_attempt_ledger",
|
|
265
|
+
"write_decision_ledger",
|
|
266
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Seed a rebuild with content-bound LightRAG indexing responses (#1577)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from deeptutor.services.file_io import atomic_write_json
|
|
12
|
+
|
|
13
|
+
from .engine import workspace_for
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_CACHE_FILE = "kv_store_llm_response_cache.json"
|
|
18
|
+
# Query answers and keyword extraction depend on the current graph and must
|
|
19
|
+
# never cross a full rebuild. These types are produced from source content by
|
|
20
|
+
# the pinned LightRAG SDK and include the model identity in their hash.
|
|
21
|
+
_INDEX_TYPES = frozenset({"extract", "summary", "analysis", "smartheading"})
|
|
22
|
+
_CACHE_KEY = re.compile(r"^[^:]+:([a-z]+):[0-9a-f]{32,64}$")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _policy_identity(policy: Any) -> tuple[str, str, str] | None:
|
|
26
|
+
"""Read both current nested role fingerprints and older flat policies."""
|
|
27
|
+
if not isinstance(policy, dict):
|
|
28
|
+
return None
|
|
29
|
+
extract = policy.get("extract")
|
|
30
|
+
if isinstance(extract, dict):
|
|
31
|
+
vlm = policy.get("vlm") if isinstance(policy.get("vlm"), dict) else {}
|
|
32
|
+
vlm_snapshot = vlm.get("snapshot") if isinstance(vlm.get("snapshot"), dict) else {}
|
|
33
|
+
fingerprint = extract.get("fingerprint")
|
|
34
|
+
if isinstance(fingerprint, str) and fingerprint:
|
|
35
|
+
return (
|
|
36
|
+
fingerprint,
|
|
37
|
+
str(vlm.get("mode") or "disabled"),
|
|
38
|
+
str(vlm_snapshot.get("fingerprint") or ""),
|
|
39
|
+
)
|
|
40
|
+
fingerprint = policy.get("fingerprint")
|
|
41
|
+
if isinstance(fingerprint, str) and fingerprint:
|
|
42
|
+
return (fingerprint, "", "")
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _read_policy_identity(root: Path) -> tuple[str, str, str] | None:
|
|
47
|
+
try:
|
|
48
|
+
meta = json.loads((root / "meta.json").read_text(encoding="utf-8"))
|
|
49
|
+
except (OSError, ValueError):
|
|
50
|
+
# Interrupted versions have no publication marker but may already
|
|
51
|
+
# have a durable cache worth preserving.
|
|
52
|
+
return None
|
|
53
|
+
return _policy_identity(meta.get("indexing_policy")) if isinstance(meta, dict) else None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _index_entries(path: Path) -> dict[str, dict[str, Any]]:
|
|
57
|
+
if path.is_symlink() or not path.is_file():
|
|
58
|
+
return {}
|
|
59
|
+
try:
|
|
60
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
61
|
+
except (OSError, ValueError) as exc:
|
|
62
|
+
logger.warning("Ignoring unreadable LightRAG cache %s: %s", path, type(exc).__name__)
|
|
63
|
+
return {}
|
|
64
|
+
if not isinstance(payload, dict):
|
|
65
|
+
return {}
|
|
66
|
+
entries: dict[str, dict[str, Any]] = {}
|
|
67
|
+
for key, value in payload.items():
|
|
68
|
+
match = _CACHE_KEY.fullmatch(key) if isinstance(key, str) else None
|
|
69
|
+
if (
|
|
70
|
+
match
|
|
71
|
+
and match.group(1) in _INDEX_TYPES
|
|
72
|
+
and isinstance(value, dict)
|
|
73
|
+
and value.get("cache_type") == match.group(1)
|
|
74
|
+
and isinstance(value.get("return"), str)
|
|
75
|
+
and value["return"]
|
|
76
|
+
):
|
|
77
|
+
entries[key] = value
|
|
78
|
+
return entries
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def inherit_index_cache(kb_dir: Path, target_root: Path, policy: dict[str, Any]) -> bool:
|
|
82
|
+
"""Carry only verified indexing responses into a fresh version workspace.
|
|
83
|
+
|
|
84
|
+
Merge matching published versions with the immediately preceding
|
|
85
|
+
interrupted version, which may have no ``meta.json``. A partial interrupted
|
|
86
|
+
cache must not hide the rest of an older published cache. Newer entries win
|
|
87
|
+
on duplicate content keys; query answers never enter the merge.
|
|
88
|
+
"""
|
|
89
|
+
target = target_root / workspace_for(target_root) / _CACHE_FILE
|
|
90
|
+
if target.exists():
|
|
91
|
+
return False
|
|
92
|
+
target_identity = _policy_identity(policy)
|
|
93
|
+
target_version = int(target_root.name.removeprefix("version-"))
|
|
94
|
+
candidates: list[tuple[int, int, int, Path]] = []
|
|
95
|
+
for root in kb_dir.glob("version-*"):
|
|
96
|
+
if not root.is_dir() or root == target_root:
|
|
97
|
+
continue
|
|
98
|
+
try:
|
|
99
|
+
version = int(root.name.removeprefix("version-"))
|
|
100
|
+
except ValueError:
|
|
101
|
+
continue
|
|
102
|
+
if version >= target_version:
|
|
103
|
+
continue
|
|
104
|
+
donor_identity = _read_policy_identity(root)
|
|
105
|
+
# An unpublished predecessor may already have useful content-addressed
|
|
106
|
+
# extraction calls. Published versions must match the current roles.
|
|
107
|
+
if donor_identity is None and version == target_version - 1:
|
|
108
|
+
identity_rank = 3
|
|
109
|
+
elif donor_identity is not None and (
|
|
110
|
+
target_identity is None or donor_identity == target_identity
|
|
111
|
+
):
|
|
112
|
+
identity_rank = 2
|
|
113
|
+
else:
|
|
114
|
+
continue
|
|
115
|
+
paths = [root / _CACHE_FILE, *root.glob(f"deeptutor_*/{_CACHE_FILE}")]
|
|
116
|
+
for path in paths:
|
|
117
|
+
if path.is_file() and not path.is_symlink():
|
|
118
|
+
candidates.append((identity_rank, version, int(path.parent != root), path))
|
|
119
|
+
if not candidates:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
merged: dict[str, dict[str, Any]] = {}
|
|
123
|
+
used = 0
|
|
124
|
+
for _, _, _, donor in sorted(candidates):
|
|
125
|
+
entries = _index_entries(donor)
|
|
126
|
+
if not entries:
|
|
127
|
+
continue
|
|
128
|
+
merged.update(entries)
|
|
129
|
+
used += 1
|
|
130
|
+
if not merged:
|
|
131
|
+
return False
|
|
132
|
+
atomic_write_json(target, merged)
|
|
133
|
+
logger.info("Inherited %d LightRAG indexing cache entries from %d donors", len(merged), used)
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
__all__ = ["inherit_index_cache"]
|