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,132 @@
|
|
|
1
|
+
"""Resolve native LightRAG role choices inside the initiating user's scope."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from deeptutor.multi_user.context import get_current_user
|
|
9
|
+
from deeptutor.multi_user.model_access import allowed_llm_options, apply_allowed_llm_selection
|
|
10
|
+
from deeptutor.multi_user.models import CurrentUser
|
|
11
|
+
from deeptutor.services.config.lightrag_roles import LightRagRoleModels
|
|
12
|
+
from deeptutor.services.llm.capabilities import supports_vision
|
|
13
|
+
from deeptutor.services.llm.config import LLMConfig
|
|
14
|
+
from deeptutor.services.model_selection.llm import LLMSelection
|
|
15
|
+
from deeptutor.services.model_selection.reasoning import supported_reasoning_efforts
|
|
16
|
+
from deeptutor.services.model_selection.runtime import resolve_llm_config_for_selection
|
|
17
|
+
|
|
18
|
+
ROLES = ("extract", "keyword", "query", "vlm")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def model_options() -> dict[str, Any]:
|
|
22
|
+
"""Access-filtered choices shared by the editor and runtime validator."""
|
|
23
|
+
result = allowed_llm_options()
|
|
24
|
+
options = []
|
|
25
|
+
for row in result["options"]:
|
|
26
|
+
levels = row.get("supported_reasoning_efforts")
|
|
27
|
+
metadata = {"codex_supported_reasoning_levels": levels} if levels is not None else {}
|
|
28
|
+
metadata["capabilities"] = {"reasoning": row.get("declared_reasoning")}
|
|
29
|
+
options.append(
|
|
30
|
+
{
|
|
31
|
+
**row,
|
|
32
|
+
"supported_reasoning_efforts": supported_reasoning_efforts(
|
|
33
|
+
row["provider"], row["model"], metadata=metadata
|
|
34
|
+
),
|
|
35
|
+
"supports_vision": row.get(
|
|
36
|
+
"declared_vision", supports_vision(row["provider"], row["model"])
|
|
37
|
+
),
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
return {**result, "options": options}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve_selection(
|
|
44
|
+
selection: LLMSelection, *, vision: bool = False, provider_default: bool = True
|
|
45
|
+
) -> LLMConfig:
|
|
46
|
+
"""Resolve an accessible model and validate its reasoning and vision support."""
|
|
47
|
+
apply_allowed_llm_selection(selection.to_dict())
|
|
48
|
+
config = resolve_llm_config_for_selection(selection)
|
|
49
|
+
if provider_default and selection.reasoning_effort is None:
|
|
50
|
+
# Empty explicitly omits provider reasoning controls; None retains the
|
|
51
|
+
# legacy resolver defaults when restoring an older pinned index.
|
|
52
|
+
config = config.model_copy(update={"reasoning_effort": ""})
|
|
53
|
+
option = next(
|
|
54
|
+
(
|
|
55
|
+
item
|
|
56
|
+
for item in model_options()["options"]
|
|
57
|
+
if item["profile_id"] == selection.profile_id and item["model_id"] == selection.model_id
|
|
58
|
+
),
|
|
59
|
+
None,
|
|
60
|
+
)
|
|
61
|
+
if option is None:
|
|
62
|
+
raise ValueError("The selected model is unavailable.")
|
|
63
|
+
if config.reasoning_effort:
|
|
64
|
+
if config.reasoning_effort not in option["supported_reasoning_efforts"]:
|
|
65
|
+
raise ValueError("The selected model does not support this reasoning effort.")
|
|
66
|
+
if vision and not option["supports_vision"]:
|
|
67
|
+
raise ValueError("The selected VLM model does not support image inputs.")
|
|
68
|
+
return config
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def settings_models(settings: dict[str, Any]) -> LightRagRoleModels | None:
|
|
72
|
+
"""Parse configured role models, returning None for legacy settings."""
|
|
73
|
+
value = settings.get("role_models")
|
|
74
|
+
return LightRagRoleModels.model_validate(value) if "role_models" in settings else None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def validate_models(models: LightRagRoleModels) -> None:
|
|
78
|
+
"""Validate access and capabilities for the base and all enabled roles."""
|
|
79
|
+
resolve_selection(LLMSelection(**models.base.model_dump()))
|
|
80
|
+
for role in ROLES:
|
|
81
|
+
selection = models.selection_for(role)
|
|
82
|
+
if selection is not None:
|
|
83
|
+
resolve_selection(LLMSelection(**selection.model_dump()), vision=role == "vlm")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class RoleCall:
|
|
88
|
+
"""Carry a resolved role model, initiating user, and execution limits."""
|
|
89
|
+
|
|
90
|
+
config: LLMConfig
|
|
91
|
+
owner: CurrentUser
|
|
92
|
+
max_async: int
|
|
93
|
+
timeout: int
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def runtime_limits(settings: dict[str, Any]) -> dict[str, dict[str, int]]:
|
|
97
|
+
"""Resolve per-role concurrency and timeouts, including legacy defaults."""
|
|
98
|
+
models = settings_models(settings)
|
|
99
|
+
return {
|
|
100
|
+
role: {
|
|
101
|
+
"max_async": getattr(models, role).max_async
|
|
102
|
+
if models
|
|
103
|
+
else int(settings.get("llm_model_max_async", 4)),
|
|
104
|
+
"timeout": getattr(models, role).timeout if models else 240,
|
|
105
|
+
}
|
|
106
|
+
for role in ROLES
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def resolve_query_roles() -> dict[str, RoleCall]:
|
|
111
|
+
"""Resolve keyword and query models within the initiating user scope."""
|
|
112
|
+
from deeptutor.services.config import load_lightrag_settings
|
|
113
|
+
|
|
114
|
+
from .config import resolve_lightrag_query_llm_config
|
|
115
|
+
|
|
116
|
+
settings = load_lightrag_settings()
|
|
117
|
+
models = settings_models(settings)
|
|
118
|
+
if models is None and settings.get("version") == 2:
|
|
119
|
+
raise ValueError("Choose and save a LightRAG base model before querying.")
|
|
120
|
+
limits = runtime_limits(settings)
|
|
121
|
+
owner = get_current_user()
|
|
122
|
+
result = {}
|
|
123
|
+
# The legacy resolver is intentionally used only for old-format settings.
|
|
124
|
+
legacy = resolve_lightrag_query_llm_config() if models is None else None
|
|
125
|
+
for role in ("keyword", "query"):
|
|
126
|
+
config = (
|
|
127
|
+
legacy
|
|
128
|
+
if models is None
|
|
129
|
+
else resolve_selection(LLMSelection(**models.selection_for(role).model_dump()))
|
|
130
|
+
)
|
|
131
|
+
result[role] = RoleCall(config=config, owner=owner, **limits[role])
|
|
132
|
+
return result
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Map frozen DeepTutor MinerU blocks to LightRAG's public Sidecar IR."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SidecarMappingError(ValueError):
|
|
12
|
+
"""Raised when a structured block cannot be represented without data loss."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _text(item: dict[str, Any]) -> str:
|
|
16
|
+
for key in ("text", "content", "body", "code_body"):
|
|
17
|
+
value = item.get(key)
|
|
18
|
+
if isinstance(value, str) and value.strip():
|
|
19
|
+
return value.strip()
|
|
20
|
+
return ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _strings(value: Any) -> list[str]:
|
|
24
|
+
if isinstance(value, list):
|
|
25
|
+
return [str(item).strip() for item in value if str(item).strip()]
|
|
26
|
+
value = str(value or "").strip()
|
|
27
|
+
return [value] if value else []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _position(item: dict[str, Any]):
|
|
31
|
+
from lightrag.sidecar.ir import IRPosition
|
|
32
|
+
|
|
33
|
+
page = item.get("page_idx", item.get("page"))
|
|
34
|
+
if isinstance(page, bool):
|
|
35
|
+
page = None
|
|
36
|
+
elif isinstance(page, int):
|
|
37
|
+
page = str(page + 1) if page >= 0 else str(page)
|
|
38
|
+
elif page is not None:
|
|
39
|
+
page = str(page).strip() or None
|
|
40
|
+
bbox = item.get("bbox")
|
|
41
|
+
if isinstance(bbox, (list, tuple)) and len(bbox) >= 4:
|
|
42
|
+
try:
|
|
43
|
+
return IRPosition(type="bbox", anchor=page, range=[float(value) for value in bbox[:4]])
|
|
44
|
+
except (TypeError, ValueError):
|
|
45
|
+
pass
|
|
46
|
+
return IRPosition(type="bbox", anchor=page) if page is not None else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _heading(item: dict[str, Any], kind: str) -> tuple[str, int]:
|
|
50
|
+
level = item.get("text_level", item.get("level", 0))
|
|
51
|
+
try:
|
|
52
|
+
level = int(level or 0)
|
|
53
|
+
except (TypeError, ValueError):
|
|
54
|
+
level = 0
|
|
55
|
+
if kind in {"title", "section_header"}:
|
|
56
|
+
return _text(item), max(level, 1)
|
|
57
|
+
if kind == "text" and level > 0:
|
|
58
|
+
return _text(item), level
|
|
59
|
+
return "", 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _rows(value: Any) -> list[list[str]] | None:
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
stripped = value.strip()
|
|
65
|
+
if stripped.startswith("["):
|
|
66
|
+
try:
|
|
67
|
+
value = json.loads(stripped)
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
return None
|
|
70
|
+
else:
|
|
71
|
+
return None
|
|
72
|
+
if not isinstance(value, list):
|
|
73
|
+
return None
|
|
74
|
+
rows: list[list[str]] = []
|
|
75
|
+
for raw_row in value:
|
|
76
|
+
if not isinstance(raw_row, list):
|
|
77
|
+
continue
|
|
78
|
+
rows.append(
|
|
79
|
+
[
|
|
80
|
+
str(cell.get("text", "")).strip() if isinstance(cell, dict) else str(cell).strip()
|
|
81
|
+
for cell in raw_row
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
return rows or None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _caption(item: dict[str, Any], *keys: str) -> str:
|
|
88
|
+
direct = str(item.get("caption") or "").strip()
|
|
89
|
+
if direct:
|
|
90
|
+
return direct
|
|
91
|
+
for key in keys:
|
|
92
|
+
values = _strings(item.get(key))
|
|
93
|
+
if values:
|
|
94
|
+
return values[0]
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _resolve_asset(bundle: Path, manifest: dict[str, Any], raw: str) -> Path | None:
|
|
99
|
+
if not raw:
|
|
100
|
+
return None
|
|
101
|
+
relative = Path(raw)
|
|
102
|
+
if relative.is_absolute() or ".." in relative.parts:
|
|
103
|
+
raise SidecarMappingError(f"Asset path escapes the frozen bundle: {raw}")
|
|
104
|
+
candidates: list[Path] = []
|
|
105
|
+
direct = bundle / "assets" / relative
|
|
106
|
+
if direct.is_file():
|
|
107
|
+
candidates.append(direct)
|
|
108
|
+
basename = relative.name
|
|
109
|
+
for record in manifest.get("assets", []):
|
|
110
|
+
path = bundle / str(record.get("path") or "")
|
|
111
|
+
if path.name == basename and path.is_file() and path not in candidates:
|
|
112
|
+
candidates.append(path)
|
|
113
|
+
if len(candidates) > 1:
|
|
114
|
+
raise SidecarMappingError(f"Asset basename is ambiguous in the frozen bundle: {raw}")
|
|
115
|
+
return candidates[0] if candidates else None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def build_ir(manifest: dict[str, Any], bundle: Path):
|
|
119
|
+
"""Return an ``IRDoc`` using only LightRAG's public Sidecar types."""
|
|
120
|
+
from lightrag.sidecar.ir import (
|
|
121
|
+
AssetSpec,
|
|
122
|
+
IRBlock,
|
|
123
|
+
IRDoc,
|
|
124
|
+
IRDrawing,
|
|
125
|
+
IREquation,
|
|
126
|
+
IRTable,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
blocks_record = manifest.get("blocks")
|
|
130
|
+
if not isinstance(blocks_record, dict):
|
|
131
|
+
raise SidecarMappingError("Sidecar mapping requires a blocks payload")
|
|
132
|
+
raw_blocks = json.loads((bundle / blocks_record["path"]).read_text(encoding="utf-8"))
|
|
133
|
+
if not isinstance(raw_blocks, list):
|
|
134
|
+
raise SidecarMappingError("Frozen blocks payload is not an array")
|
|
135
|
+
|
|
136
|
+
output: list[Any] = []
|
|
137
|
+
assets: list[Any] = []
|
|
138
|
+
asset_refs: dict[Path, str] = {}
|
|
139
|
+
heading_stack: list[str] = []
|
|
140
|
+
current_lines: list[str] = []
|
|
141
|
+
current_tables: list[Any] = []
|
|
142
|
+
current_drawings: list[Any] = []
|
|
143
|
+
current_equations: list[Any] = []
|
|
144
|
+
current_positions: list[Any] = []
|
|
145
|
+
current_heading = "Preface/Uncategorized"
|
|
146
|
+
current_level = 0
|
|
147
|
+
current_parents: list[str] = []
|
|
148
|
+
sequence = 0
|
|
149
|
+
document_title = ""
|
|
150
|
+
|
|
151
|
+
def next_key(prefix: str) -> str:
|
|
152
|
+
nonlocal sequence
|
|
153
|
+
sequence += 1
|
|
154
|
+
return f"{prefix}{sequence}"
|
|
155
|
+
|
|
156
|
+
def flush() -> None:
|
|
157
|
+
nonlocal \
|
|
158
|
+
current_lines, \
|
|
159
|
+
current_tables, \
|
|
160
|
+
current_drawings, \
|
|
161
|
+
current_equations, \
|
|
162
|
+
current_positions
|
|
163
|
+
content = "\n".join(line for line in current_lines if line).strip()
|
|
164
|
+
if not content and not (current_tables or current_drawings or current_equations):
|
|
165
|
+
current_lines = []
|
|
166
|
+
current_positions = []
|
|
167
|
+
return
|
|
168
|
+
output.append(
|
|
169
|
+
IRBlock(
|
|
170
|
+
content_template=content,
|
|
171
|
+
heading=current_heading,
|
|
172
|
+
level=current_level,
|
|
173
|
+
parent_headings=list(current_parents),
|
|
174
|
+
positions=list(current_positions),
|
|
175
|
+
tables=list(current_tables),
|
|
176
|
+
drawings=list(current_drawings),
|
|
177
|
+
equations=list(current_equations),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
current_lines = []
|
|
181
|
+
current_tables = []
|
|
182
|
+
current_drawings = []
|
|
183
|
+
current_equations = []
|
|
184
|
+
current_positions = []
|
|
185
|
+
|
|
186
|
+
for index, item in enumerate(raw_blocks):
|
|
187
|
+
if not isinstance(item, dict):
|
|
188
|
+
raise SidecarMappingError(f"Structured block {index} is not an object")
|
|
189
|
+
kind = str(item.get("type") or item.get("label") or "").strip().lower()
|
|
190
|
+
heading, level = _heading(item, kind)
|
|
191
|
+
position = _position(item)
|
|
192
|
+
if heading:
|
|
193
|
+
flush()
|
|
194
|
+
heading = re.sub(r"^#{1,6}\s+", "", heading).strip()
|
|
195
|
+
heading_stack = heading_stack[: max(level - 1, 0)]
|
|
196
|
+
current_parents = [value for value in heading_stack if value]
|
|
197
|
+
heading_stack.append(heading)
|
|
198
|
+
current_heading = heading
|
|
199
|
+
current_level = level
|
|
200
|
+
current_lines.append(f"{'#' * min(level, 6)} {heading}")
|
|
201
|
+
if position is not None:
|
|
202
|
+
current_positions.append(position)
|
|
203
|
+
if not document_title and level == 1:
|
|
204
|
+
document_title = heading
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if kind in {"text", "aside_text", "page_footnote"}:
|
|
208
|
+
value = _text(item)
|
|
209
|
+
if value:
|
|
210
|
+
current_lines.append(value)
|
|
211
|
+
elif kind == "list":
|
|
212
|
+
values = item.get("list_items")
|
|
213
|
+
value = "\n".join(_strings(values)) if isinstance(values, list) else _text(item)
|
|
214
|
+
if value:
|
|
215
|
+
current_lines.append(value)
|
|
216
|
+
elif kind == "code":
|
|
217
|
+
value = str(item.get("code_body") or _text(item)).strip()
|
|
218
|
+
if value:
|
|
219
|
+
current_lines.extend(_strings(item.get("code_caption")))
|
|
220
|
+
current_lines.append(value)
|
|
221
|
+
current_lines.extend(_strings(item.get("code_footnote")))
|
|
222
|
+
elif kind == "table":
|
|
223
|
+
body = item.get("rows", item.get("table_body"))
|
|
224
|
+
table_rows = _rows(body)
|
|
225
|
+
table_html = (
|
|
226
|
+
body.strip() if isinstance(body, str) and "<table" in body.lower() else None
|
|
227
|
+
)
|
|
228
|
+
if not table_rows and not table_html:
|
|
229
|
+
raise SidecarMappingError(f"Structured table {index} has no usable body")
|
|
230
|
+
key = next_key("tb")
|
|
231
|
+
current_tables.append(
|
|
232
|
+
IRTable(
|
|
233
|
+
placeholder_key=key,
|
|
234
|
+
rows=table_rows,
|
|
235
|
+
html=table_html,
|
|
236
|
+
num_rows=int(item.get("num_rows") or (len(table_rows) if table_rows else 0)),
|
|
237
|
+
num_cols=int(
|
|
238
|
+
item.get("num_cols") or (max(map(len, table_rows)) if table_rows else 0)
|
|
239
|
+
),
|
|
240
|
+
caption=_caption(item, "table_caption"),
|
|
241
|
+
footnotes=_strings(item.get("table_footnote") or item.get("footnotes")),
|
|
242
|
+
table_header=_rows(item.get("header")),
|
|
243
|
+
self_ref=f"blocks.json#/{index}",
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
current_lines.append(f"{{{{TBL:{key}}}}}")
|
|
247
|
+
elif kind in {"image", "picture", "drawing", "chart"}:
|
|
248
|
+
raw_path = str(item.get("img_path") or item.get("path") or "")
|
|
249
|
+
asset_path = _resolve_asset(bundle, manifest, raw_path)
|
|
250
|
+
if asset_path is None:
|
|
251
|
+
raise SidecarMappingError(f"Structured {kind} {index} has no verified bundle asset")
|
|
252
|
+
ref = ""
|
|
253
|
+
ref = asset_refs.get(asset_path, "")
|
|
254
|
+
if not ref:
|
|
255
|
+
ref = f"asset-{len(asset_refs) + 1}"
|
|
256
|
+
asset_refs[asset_path] = ref
|
|
257
|
+
assets.append(AssetSpec(ref=ref, suggested_name=asset_path.name, source=asset_path))
|
|
258
|
+
key = next_key("im")
|
|
259
|
+
current_drawings.append(
|
|
260
|
+
IRDrawing(
|
|
261
|
+
placeholder_key=key,
|
|
262
|
+
asset_ref=ref,
|
|
263
|
+
fmt=Path(raw_path).suffix.lower().lstrip(".") or str(item.get("format") or ""),
|
|
264
|
+
caption=_caption(item, "image_caption", "chart_caption", "captions"),
|
|
265
|
+
footnotes=_strings(
|
|
266
|
+
item.get("image_footnote")
|
|
267
|
+
or item.get("chart_footnote")
|
|
268
|
+
or item.get("footnotes")
|
|
269
|
+
),
|
|
270
|
+
src=str(item.get("src") or ""),
|
|
271
|
+
self_ref=f"blocks.json#/{index}",
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
current_lines.append(f"{{{{IMG:{key}}}}}")
|
|
275
|
+
elif kind in {"equation", "formula"}:
|
|
276
|
+
latex = _text(item)
|
|
277
|
+
if not latex:
|
|
278
|
+
raise SidecarMappingError(f"Structured equation {index} has no LaTeX")
|
|
279
|
+
key = next_key("eq")
|
|
280
|
+
inline = str(item.get("text_format") or "").lower() in {"inline", "inline_equation"}
|
|
281
|
+
current_equations.append(
|
|
282
|
+
IREquation(
|
|
283
|
+
placeholder_key=key,
|
|
284
|
+
latex=latex,
|
|
285
|
+
is_block=not inline,
|
|
286
|
+
caption=_caption(item, "equation_caption"),
|
|
287
|
+
footnotes=_strings(item.get("equation_footnote") or item.get("footnotes")),
|
|
288
|
+
self_ref=f"blocks.json#/{index}" if not inline else "",
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
current_lines.append(f"{{{{{'EQI' if inline else 'EQ'}:{key}}}}}")
|
|
292
|
+
else:
|
|
293
|
+
value = _text(item)
|
|
294
|
+
if not value:
|
|
295
|
+
raise SidecarMappingError(
|
|
296
|
+
f"Unknown structured block type {kind or '<missing>'!r} at {index} has no preservable text"
|
|
297
|
+
)
|
|
298
|
+
current_lines.append(value)
|
|
299
|
+
if position is not None:
|
|
300
|
+
current_positions.append(position)
|
|
301
|
+
|
|
302
|
+
flush()
|
|
303
|
+
if not output:
|
|
304
|
+
raise SidecarMappingError("Structured document produced no Sidecar blocks")
|
|
305
|
+
name = str(manifest.get("canonical_filename") or "document")
|
|
306
|
+
return IRDoc(
|
|
307
|
+
document_name=name,
|
|
308
|
+
document_format=Path(name).suffix.lower().lstrip("."),
|
|
309
|
+
doc_title=document_title or Path(name).stem,
|
|
310
|
+
split_option={
|
|
311
|
+
"parser": str((manifest.get("parser") or {}).get("engine") or ""),
|
|
312
|
+
"parser_signature": str((manifest.get("parser") or {}).get("parser_signature") or ""),
|
|
313
|
+
},
|
|
314
|
+
blocks=output,
|
|
315
|
+
assets=assets,
|
|
316
|
+
bbox_attributes={"origin": "LEFTTOP", "max": 1000},
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
__all__ = ["SidecarMappingError", "build_ir"]
|