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,292 @@
|
|
|
1
|
+
"""Page-bound PDF evidence shared by indexing, tutoring and authorized previews."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import tempfile
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import quote, urlencode
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
15
|
+
|
|
16
|
+
SOURCE_SCHEMA_VERSION = 1
|
|
17
|
+
CATALOG_FILENAME = "source_pages.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SourcePageError(ValueError):
|
|
21
|
+
"""A safe, user-facing source resolution failure."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SourceRef(BaseModel):
|
|
25
|
+
model_config = ConfigDict(extra="forbid")
|
|
26
|
+
kb_ref: str
|
|
27
|
+
document_key: str
|
|
28
|
+
source_digest: str = Field(pattern=r"^[0-9a-f]{64}$")
|
|
29
|
+
page_index: int = Field(ge=0)
|
|
30
|
+
page_label: str = ""
|
|
31
|
+
parser_signature: str = ""
|
|
32
|
+
node_id: str = ""
|
|
33
|
+
crop: tuple[float, float, float, float] | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def source_digest(path: Path) -> str:
|
|
37
|
+
digest = hashlib.sha256()
|
|
38
|
+
with path.open("rb") as handle:
|
|
39
|
+
for block in iter(lambda: handle.read(1 << 20), b""):
|
|
40
|
+
digest.update(block)
|
|
41
|
+
return digest.hexdigest()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_source(ref: SourceRef) -> Path:
|
|
45
|
+
from deeptutor.multi_user.knowledge_access import resolve_kb_document_path
|
|
46
|
+
|
|
47
|
+
path = resolve_kb_document_path(ref.kb_ref, ref.document_key)
|
|
48
|
+
if path is None or path.suffix.lower() != ".pdf":
|
|
49
|
+
raise SourcePageError("The selected PDF is unavailable or access was revoked.")
|
|
50
|
+
if source_digest(path) != ref.source_digest:
|
|
51
|
+
raise SourcePageError(
|
|
52
|
+
"The source document has changed. Refresh its index before continuing."
|
|
53
|
+
)
|
|
54
|
+
return path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def render_source_page(ref: SourceRef) -> bytes:
|
|
58
|
+
from deeptutor.multi_user.knowledge_access import resolve_for_rag
|
|
59
|
+
from deeptutor.services.rag.source_visuals import page_image
|
|
60
|
+
|
|
61
|
+
# Authorize through the same source resolver as existing lesson snapshots.
|
|
62
|
+
resolve_source(ref)
|
|
63
|
+
resource = resolve_for_rag(ref.kb_ref)
|
|
64
|
+
if resource is None:
|
|
65
|
+
raise SourcePageError("The selected PDF is unavailable or access was revoked.")
|
|
66
|
+
try:
|
|
67
|
+
_, pixels = page_image(
|
|
68
|
+
Path(resource.base_dir) / resource.name,
|
|
69
|
+
ref.document_key,
|
|
70
|
+
ref.page_index + 1,
|
|
71
|
+
expected_hash=ref.source_digest,
|
|
72
|
+
region=list(ref.crop) if ref.crop else None,
|
|
73
|
+
)
|
|
74
|
+
return pixels
|
|
75
|
+
except (OSError, ValueError) as exc:
|
|
76
|
+
raise SourcePageError(str(exc)) from exc
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def preview_url(ref: SourceRef) -> str:
|
|
80
|
+
query = {
|
|
81
|
+
"source_path": ref.document_key,
|
|
82
|
+
"source_hash": ref.source_digest,
|
|
83
|
+
"page": str(ref.page_index + 1),
|
|
84
|
+
}
|
|
85
|
+
if ref.crop:
|
|
86
|
+
query["region"] = ",".join(str(v) for v in ref.crop)
|
|
87
|
+
return f"/api/knowledge-bases/{quote(ref.kb_ref, safe='')}/source-page?{urlencode(query)}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def public_source(ref: SourceRef) -> dict[str, Any]:
|
|
91
|
+
return {
|
|
92
|
+
"type": "rag",
|
|
93
|
+
"kb_name": ref.kb_ref,
|
|
94
|
+
"title": f"{ref.document_key} · PDF page {ref.page_index + 1}",
|
|
95
|
+
"source": ref.document_key,
|
|
96
|
+
"page": ref.page_index + 1,
|
|
97
|
+
"url": (
|
|
98
|
+
f"/api/knowledge-bases/{quote(ref.kb_ref, safe='')}/files/"
|
|
99
|
+
f"{quote(ref.document_key, safe='/')}#page={ref.page_index + 1}"
|
|
100
|
+
),
|
|
101
|
+
"preview_url": preview_url(ref),
|
|
102
|
+
"source_ref": ref.model_dump(),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_pdf_documents(path: Path, parsed: Any, kb_dir: Path) -> list[Any]:
|
|
107
|
+
"""Prefer located IR; native PDF pages provide physical locators otherwise."""
|
|
108
|
+
from llama_index.core import Document
|
|
109
|
+
import pymupdf
|
|
110
|
+
|
|
111
|
+
resolved = path.resolve()
|
|
112
|
+
raw = (kb_dir / "raw").resolve()
|
|
113
|
+
if not resolved.is_relative_to(raw):
|
|
114
|
+
return []
|
|
115
|
+
key = resolved.relative_to(raw).as_posix()
|
|
116
|
+
digest = source_digest(path)
|
|
117
|
+
grouped: dict[int, list[str]] = {}
|
|
118
|
+
unlocated: list[str] = []
|
|
119
|
+
for block in parsed.blocks or []:
|
|
120
|
+
if not isinstance(block, dict):
|
|
121
|
+
continue
|
|
122
|
+
pieces = [str(block.get(k) or "") for k in ("text", "content", "table_body")]
|
|
123
|
+
for name in ("image_caption", "table_caption", "table_footnote"):
|
|
124
|
+
value = block.get(name) or []
|
|
125
|
+
if isinstance(value, list):
|
|
126
|
+
pieces.extend(str(v) for v in value)
|
|
127
|
+
else:
|
|
128
|
+
pieces.append(str(value))
|
|
129
|
+
text = "\n".join(p for p in pieces if p.strip())
|
|
130
|
+
index = block.get("page_idx", block.get("page_index"))
|
|
131
|
+
if type(index) is int and index >= 0:
|
|
132
|
+
grouped.setdefault(index, []).append(text)
|
|
133
|
+
elif text:
|
|
134
|
+
unlocated.append(text)
|
|
135
|
+
documents = []
|
|
136
|
+
with pymupdf.open(path) as pdf:
|
|
137
|
+
if pdf.needs_pass:
|
|
138
|
+
raise SourcePageError("Password protected PDFs cannot supply teaching evidence.")
|
|
139
|
+
for index, page in enumerate(pdf):
|
|
140
|
+
located = "\n\n".join(grouped.get(index, []))
|
|
141
|
+
native = page.get_text(sort=True).strip()
|
|
142
|
+
text = located.strip() or native
|
|
143
|
+
metadata = {
|
|
144
|
+
"file_name": path.name,
|
|
145
|
+
"file_path": str(path),
|
|
146
|
+
"document_key": key,
|
|
147
|
+
"source_digest": digest,
|
|
148
|
+
"source_schema_version": SOURCE_SCHEMA_VERSION,
|
|
149
|
+
"page_index": index,
|
|
150
|
+
"page": index + 1,
|
|
151
|
+
"page_label": page.get_label() or "",
|
|
152
|
+
"page_count": pdf.page_count,
|
|
153
|
+
"parser_signature": parsed.parser_signature,
|
|
154
|
+
"parse_engine": parsed.engine if located.strip() else "native_pdf",
|
|
155
|
+
"page_status": "searchable" if text else "needs_ocr",
|
|
156
|
+
"visual_status": "present"
|
|
157
|
+
if page.get_images() or page.get_drawings()
|
|
158
|
+
else "unknown",
|
|
159
|
+
}
|
|
160
|
+
documents.append(
|
|
161
|
+
Document(
|
|
162
|
+
id_=f"pdf-{digest}-{index}",
|
|
163
|
+
text=text or f"[Unsearchable PDF page {index + 1}]",
|
|
164
|
+
metadata=metadata,
|
|
165
|
+
excluded_embed_metadata_keys=list(metadata),
|
|
166
|
+
excluded_llm_metadata_keys=["file_path"],
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
for index in grouped:
|
|
170
|
+
if index >= len(documents):
|
|
171
|
+
unlocated.extend(grouped[index])
|
|
172
|
+
if unlocated or (not grouped and parsed.engine not in {"text_only", "native_pdf"}):
|
|
173
|
+
extra = "\n\n".join(unlocated) or parsed.markdown.strip()
|
|
174
|
+
if extra:
|
|
175
|
+
documents.append(
|
|
176
|
+
Document(
|
|
177
|
+
text=extra,
|
|
178
|
+
metadata={
|
|
179
|
+
"file_name": path.name,
|
|
180
|
+
"file_path": str(path),
|
|
181
|
+
"document_key": key,
|
|
182
|
+
"source_digest": digest,
|
|
183
|
+
"source_schema_version": SOURCE_SCHEMA_VERSION,
|
|
184
|
+
"page_status": "unlocated",
|
|
185
|
+
"parse_engine": parsed.engine,
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
return documents
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def persist_page_catalog(documents: list[Any], storage_dir: Path) -> None:
|
|
193
|
+
pages = []
|
|
194
|
+
for document in documents:
|
|
195
|
+
meta = getattr(document, "metadata", {})
|
|
196
|
+
if meta.get("source_schema_version") == SOURCE_SCHEMA_VERSION and "page_index" in meta:
|
|
197
|
+
pages.append({k: v for k, v in meta.items() if k != "file_path"})
|
|
198
|
+
if not pages:
|
|
199
|
+
return
|
|
200
|
+
target = storage_dir / CATALOG_FILENAME
|
|
201
|
+
if target.exists():
|
|
202
|
+
old = json.loads(target.read_text(encoding="utf-8")).get("pages", [])
|
|
203
|
+
replaced = {p["document_key"] for p in pages}
|
|
204
|
+
pages = [p for p in old if p.get("document_key") not in replaced] + pages
|
|
205
|
+
temp = target.with_suffix(".tmp")
|
|
206
|
+
temp.write_text(
|
|
207
|
+
json.dumps({"schema_version": SOURCE_SCHEMA_VERSION, "pages": pages}, ensure_ascii=False),
|
|
208
|
+
encoding="utf-8",
|
|
209
|
+
)
|
|
210
|
+
temp.replace(target)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def legacy_page_index(
|
|
214
|
+
kb_dir: Path, storage_dir: Path, document_keys: list[str] | None, *, build: bool = False
|
|
215
|
+
) -> Path:
|
|
216
|
+
"""Compatibility sidecar: native page text + cached IR, no OCR or embedding.
|
|
217
|
+
|
|
218
|
+
Old vectors cannot acquire trustworthy page locators by guessing. Use the
|
|
219
|
+
BM25 engine over physical pages instead, without requiring optional stemming. Keep this
|
|
220
|
+
explicitly distinct from a freshly rebuilt, hybrid page-aware vector index.
|
|
221
|
+
"""
|
|
222
|
+
from deeptutor.services.parsing import cache, get_parse_service
|
|
223
|
+
from deeptutor.services.parsing.engines.factory import get_parser
|
|
224
|
+
from deeptutor.services.path_service import get_path_service
|
|
225
|
+
|
|
226
|
+
raw = (kb_dir / "raw").resolve()
|
|
227
|
+
candidates = (
|
|
228
|
+
sorted(raw.rglob("*.pdf")) if document_keys is None else [raw / k for k in document_keys]
|
|
229
|
+
)
|
|
230
|
+
paths = [p for p in candidates if p.suffix.lower() == ".pdf" and p.is_file()]
|
|
231
|
+
if not paths:
|
|
232
|
+
raise SourcePageError("No selected PDF is available for page lookup.")
|
|
233
|
+
try:
|
|
234
|
+
engine = get_parse_service().active_engine()
|
|
235
|
+
parser = get_parser(engine)
|
|
236
|
+
signature = parser.signature(parser.resolve_config()).hash()
|
|
237
|
+
except Exception:
|
|
238
|
+
engine, signature = "native_pdf", "native-pdf-page-v1"
|
|
239
|
+
inventory = []
|
|
240
|
+
for path in paths:
|
|
241
|
+
if not path.resolve().is_relative_to(raw):
|
|
242
|
+
raise SourcePageError("A selected document is outside the knowledge-base directory.")
|
|
243
|
+
digest = source_digest(path)
|
|
244
|
+
hit = cache.lookup(get_path_service().get_parse_cache_root(), digest[:16], signature)
|
|
245
|
+
inventory.append((path, digest, hit))
|
|
246
|
+
identity = [
|
|
247
|
+
(p.relative_to(raw).as_posix(), digest, signature if hit else "native-pdf-page-v1")
|
|
248
|
+
for p, digest, hit in inventory
|
|
249
|
+
]
|
|
250
|
+
key = hashlib.sha256(json.dumps(identity).encode()).hexdigest()[:24]
|
|
251
|
+
target = storage_dir / ".mastery-pages" / key
|
|
252
|
+
if (target / CATALOG_FILENAME).is_file() and (target / "docstore.json").is_file():
|
|
253
|
+
return target
|
|
254
|
+
if not build:
|
|
255
|
+
raise SourcePageError("The original-page lookup needs preparation. Retry the lesson.")
|
|
256
|
+
from llama_index.core.node_parser import SentenceSplitter
|
|
257
|
+
from llama_index.core.storage.docstore import SimpleDocumentStore
|
|
258
|
+
|
|
259
|
+
documents = []
|
|
260
|
+
for path, _, hit in inventory:
|
|
261
|
+
markdown, blocks, assets = cache.load_ir(hit) if hit else ("", None, None)
|
|
262
|
+
parsed = SimpleNamespace(
|
|
263
|
+
markdown=markdown,
|
|
264
|
+
blocks=blocks,
|
|
265
|
+
asset_dir=assets,
|
|
266
|
+
engine=engine if hit else "native_pdf",
|
|
267
|
+
parser_signature=signature if hit else "native-pdf-page-v1",
|
|
268
|
+
)
|
|
269
|
+
documents.extend(build_pdf_documents(path, parsed, kb_dir))
|
|
270
|
+
nodes = SentenceSplitter(chunk_size=512, chunk_overlap=50).get_nodes_from_documents(
|
|
271
|
+
[d for d in documents if d.metadata.get("page_status") == "searchable"]
|
|
272
|
+
)
|
|
273
|
+
if not nodes:
|
|
274
|
+
raise SourcePageError("These PDFs have no searchable page text. Finish their OCR first.")
|
|
275
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
with tempfile.TemporaryDirectory(prefix=".preparing-", dir=target.parent) as temporary:
|
|
277
|
+
stage = Path(temporary)
|
|
278
|
+
docstore = SimpleDocumentStore()
|
|
279
|
+
docstore.add_documents(nodes)
|
|
280
|
+
docstore.persist(str(stage / "docstore.json"))
|
|
281
|
+
persist_page_catalog(documents, stage)
|
|
282
|
+
# The publication is atomic. Concurrent identical preparations may
|
|
283
|
+
# reuse the first complete directory; neither changes the vector index.
|
|
284
|
+
try:
|
|
285
|
+
os.replace(stage, target)
|
|
286
|
+
except OSError:
|
|
287
|
+
if (
|
|
288
|
+
not (target / CATALOG_FILENAME).is_file()
|
|
289
|
+
or not (target / "docstore.json").is_file()
|
|
290
|
+
):
|
|
291
|
+
raise
|
|
292
|
+
return target
|