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,696 @@
|
|
|
1
|
+
"""Immutable model identity for native LightRAG indexing writes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from dataclasses import dataclass, field, replace
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
12
|
+
|
|
13
|
+
from deeptutor.multi_user.context import get_current_user, get_current_user_or_none
|
|
14
|
+
from deeptutor.multi_user.model_access import (
|
|
15
|
+
OWNER_BOUND_BINDINGS,
|
|
16
|
+
apply_allowed_llm_selection,
|
|
17
|
+
)
|
|
18
|
+
from deeptutor.multi_user.models import CurrentUser
|
|
19
|
+
from deeptutor.services.llm.capabilities import supports_vision
|
|
20
|
+
from deeptutor.services.llm.config import LLMConfig
|
|
21
|
+
from deeptutor.services.llm.exceptions import LLMConfigError
|
|
22
|
+
from deeptutor.services.model_selection.llm import LLMSelection
|
|
23
|
+
from deeptutor.services.model_selection.runtime import resolve_llm_config_for_selection
|
|
24
|
+
|
|
25
|
+
POLICY_PINNED = "pinned"
|
|
26
|
+
POLICY_PENDING = "pending_pinned"
|
|
27
|
+
POLICY_LEGACY = "legacy_unpinned"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class IndexingPolicyError(RuntimeError):
|
|
31
|
+
"""A pinned model can no longer be used without changing index identity."""
|
|
32
|
+
|
|
33
|
+
code = "indexing_model_unavailable"
|
|
34
|
+
retryable = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class IndexingModelChangedError(IndexingPolicyError):
|
|
38
|
+
"""The selected catalog entry no longer matches the published fingerprint."""
|
|
39
|
+
|
|
40
|
+
code = "reindex_required"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EmbeddingMismatchError(IndexingPolicyError):
|
|
44
|
+
"""The selected embedding does not match the published vector space."""
|
|
45
|
+
|
|
46
|
+
code = "lightrag_embedding_incompatible"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _endpoint_identity(value: str | None) -> str:
|
|
50
|
+
if not value:
|
|
51
|
+
return ""
|
|
52
|
+
parsed = urlsplit(value)
|
|
53
|
+
host = parsed.hostname or ""
|
|
54
|
+
if parsed.port is not None:
|
|
55
|
+
host = f"{host}:{parsed.port}"
|
|
56
|
+
return urlunsplit((parsed.scheme.lower(), host.lower(), parsed.path.rstrip("/"), "", ""))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _canonical_identity(
|
|
60
|
+
selection: LLMSelection | None,
|
|
61
|
+
config: LLMConfig,
|
|
62
|
+
*,
|
|
63
|
+
vision_available: bool | None = None,
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
identity = {
|
|
66
|
+
"binding": config.binding,
|
|
67
|
+
"provider_mode": config.provider_mode,
|
|
68
|
+
"endpoint": _endpoint_identity(config.effective_url or config.base_url),
|
|
69
|
+
"api_version": config.api_version or None,
|
|
70
|
+
"model": config.model,
|
|
71
|
+
"reasoning_effort": config.reasoning_effort,
|
|
72
|
+
}
|
|
73
|
+
if selection is not None:
|
|
74
|
+
identity["profile_id"] = selection.profile_id
|
|
75
|
+
identity["model_id"] = selection.model_id
|
|
76
|
+
if vision_available is not None:
|
|
77
|
+
identity["vision_available"] = vision_available
|
|
78
|
+
return identity
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _fingerprint(identity: dict[str, Any]) -> str:
|
|
82
|
+
encoded = json.dumps(identity, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
83
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True)
|
|
87
|
+
class IndexingLLMSnapshot:
|
|
88
|
+
config: LLMConfig
|
|
89
|
+
owner: CurrentUser
|
|
90
|
+
policy: str
|
|
91
|
+
selection: LLMSelection | None
|
|
92
|
+
descriptor: dict[str, Any]
|
|
93
|
+
fingerprint: str | None
|
|
94
|
+
vision_available: bool
|
|
95
|
+
|
|
96
|
+
def persisted_policy(self) -> dict[str, Any]:
|
|
97
|
+
payload: dict[str, Any] = {
|
|
98
|
+
"policy": self.policy,
|
|
99
|
+
"descriptor": self.descriptor,
|
|
100
|
+
"fingerprint": self.fingerprint,
|
|
101
|
+
"vision_available": self.vision_available,
|
|
102
|
+
}
|
|
103
|
+
if self.selection is not None:
|
|
104
|
+
payload["selection"] = self.selection.to_dict()
|
|
105
|
+
return payload
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def freeze_snapshot(
|
|
109
|
+
selection_value: Any = None,
|
|
110
|
+
*,
|
|
111
|
+
policy: str | None = None,
|
|
112
|
+
require_explicit_user: bool = True,
|
|
113
|
+
) -> IndexingLLMSnapshot:
|
|
114
|
+
"""Resolve and freeze one access-checked indexing model in the caller scope."""
|
|
115
|
+
selection = LLMSelection.from_payload(selection_value)
|
|
116
|
+
explicit_user = get_current_user_or_none()
|
|
117
|
+
if selection is not None and explicit_user is None and require_explicit_user:
|
|
118
|
+
raise IndexingPolicyError(
|
|
119
|
+
"Pinned indexing models require an explicit initiating user scope."
|
|
120
|
+
)
|
|
121
|
+
try:
|
|
122
|
+
if selection is not None:
|
|
123
|
+
apply_allowed_llm_selection(selection.to_dict())
|
|
124
|
+
config = resolve_llm_config_for_selection(selection)
|
|
125
|
+
except (LLMConfigError, PermissionError, ValueError) as exc:
|
|
126
|
+
raise IndexingPolicyError(str(exc)) from exc
|
|
127
|
+
|
|
128
|
+
if config.binding in OWNER_BOUND_BINDINGS and explicit_user is None:
|
|
129
|
+
raise IndexingPolicyError(
|
|
130
|
+
"Owner-bound indexing models require an explicit initiating user scope."
|
|
131
|
+
)
|
|
132
|
+
owner = explicit_user or get_current_user()
|
|
133
|
+
resolved_policy = policy or (POLICY_PINNED if selection is not None else POLICY_LEGACY)
|
|
134
|
+
vision_available = supports_vision(config.binding, config.model)
|
|
135
|
+
if resolved_policy == POLICY_LEGACY:
|
|
136
|
+
descriptor = {
|
|
137
|
+
"model": config.model,
|
|
138
|
+
"binding": config.binding,
|
|
139
|
+
"reasoning_effort": config.reasoning_effort,
|
|
140
|
+
}
|
|
141
|
+
fingerprint = None
|
|
142
|
+
else:
|
|
143
|
+
identity = _canonical_identity(
|
|
144
|
+
selection,
|
|
145
|
+
config,
|
|
146
|
+
vision_available=vision_available,
|
|
147
|
+
)
|
|
148
|
+
descriptor = identity
|
|
149
|
+
fingerprint = _fingerprint(identity)
|
|
150
|
+
return IndexingLLMSnapshot(
|
|
151
|
+
config=config,
|
|
152
|
+
owner=owner,
|
|
153
|
+
policy=resolved_policy,
|
|
154
|
+
selection=None if resolved_policy == POLICY_LEGACY else selection,
|
|
155
|
+
descriptor=descriptor,
|
|
156
|
+
fingerprint=fingerprint,
|
|
157
|
+
vision_available=vision_available,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def pending_policy_for_selection(selection_value: Any) -> dict[str, Any]:
|
|
162
|
+
return freeze_roles(selection_value, policy=POLICY_PENDING).persisted_policy()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _active_catalog_selection() -> dict[str, str] | None:
|
|
166
|
+
from deeptutor.services.config import get_model_catalog_service
|
|
167
|
+
|
|
168
|
+
catalog = get_model_catalog_service().load()
|
|
169
|
+
service = catalog.get("services", {}).get("llm", {})
|
|
170
|
+
profile_id = str(service.get("active_profile_id") or "").strip()
|
|
171
|
+
model_id = str(service.get("active_model_id") or "").strip()
|
|
172
|
+
if not profile_id or not model_id:
|
|
173
|
+
return None
|
|
174
|
+
return {"profile_id": profile_id, "model_id": model_id}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _freeze_legacy_default_snapshot() -> IndexingLLMSnapshot:
|
|
178
|
+
"""Freeze the released LightRAG model, or the active model when it is unset."""
|
|
179
|
+
from .config import lightrag_indexing_selection_from_settings
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
selection = lightrag_indexing_selection_from_settings()
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
raise IndexingPolicyError(
|
|
185
|
+
"The LightRAG indexing-model setting could not be resolved."
|
|
186
|
+
) from exc
|
|
187
|
+
if selection is None:
|
|
188
|
+
selection = _active_catalog_selection()
|
|
189
|
+
return freeze_snapshot(
|
|
190
|
+
selection,
|
|
191
|
+
policy=POLICY_PINNED,
|
|
192
|
+
require_explicit_user=False,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _single_snapshot_from_persisted(policy: dict[str, Any]) -> IndexingLLMSnapshot:
|
|
197
|
+
if type(policy.get("vision_available")) is not bool:
|
|
198
|
+
raise IndexingModelChangedError(
|
|
199
|
+
"The historical vision capability is unknown; run a full re-index."
|
|
200
|
+
)
|
|
201
|
+
selection = policy.get("selection")
|
|
202
|
+
if not isinstance(selection, dict):
|
|
203
|
+
raise IndexingPolicyError("Pinned LightRAG metadata has an invalid model selection.")
|
|
204
|
+
snapshot = freeze_snapshot(selection, policy=POLICY_PINNED)
|
|
205
|
+
expected = str(policy.get("fingerprint") or "")
|
|
206
|
+
if not expected or snapshot.fingerprint != expected:
|
|
207
|
+
raise IndexingModelChangedError(
|
|
208
|
+
"The pinned LightRAG indexing model changed; run a full re-index to publish "
|
|
209
|
+
"the new model identity."
|
|
210
|
+
)
|
|
211
|
+
return snapshot
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass(frozen=True, slots=True)
|
|
215
|
+
class IndexingPolicySnapshot:
|
|
216
|
+
"""Freeze role identities, runtime limits, and accepted indexing target state."""
|
|
217
|
+
|
|
218
|
+
extract: IndexingLLMSnapshot
|
|
219
|
+
vlm: IndexingLLMSnapshot | None
|
|
220
|
+
policy: str = POLICY_PINNED
|
|
221
|
+
limits: dict[str, dict[str, int]] = field(default_factory=dict)
|
|
222
|
+
legacy_policy: dict[str, Any] | None = None
|
|
223
|
+
target_version: str | None = None
|
|
224
|
+
target_bound: bool = False
|
|
225
|
+
target_policy: str | None = None
|
|
226
|
+
image_analysis: bool | None = None
|
|
227
|
+
target_revision: str | None = None
|
|
228
|
+
embedding_config: Any | None = field(default=None, repr=False, compare=False)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def vision_available(self) -> bool:
|
|
232
|
+
"""Return whether the pinned vision model supports image inputs."""
|
|
233
|
+
return self.vlm is not None and self.vlm.vision_available
|
|
234
|
+
|
|
235
|
+
def persisted_policy(self) -> dict[str, Any]:
|
|
236
|
+
"""Serialize role identities while preserving an existing legacy policy."""
|
|
237
|
+
if self.legacy_policy is not None:
|
|
238
|
+
# Appending must not rewrite the old fingerprint as a v2 identity.
|
|
239
|
+
return dict(self.legacy_policy)
|
|
240
|
+
return {
|
|
241
|
+
"schema_version": 2,
|
|
242
|
+
"policy": self.policy,
|
|
243
|
+
"extract": self.extract.persisted_policy(),
|
|
244
|
+
"vlm": {
|
|
245
|
+
"mode": "enabled" if self.vlm else "disabled",
|
|
246
|
+
**({"snapshot": self.vlm.persisted_policy()} if self.vlm else {}),
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _role_identity(snapshot: IndexingLLMSnapshot, *, vision: bool) -> dict[str, Any]:
|
|
252
|
+
config = snapshot.config
|
|
253
|
+
return {
|
|
254
|
+
**_canonical_identity(
|
|
255
|
+
snapshot.selection,
|
|
256
|
+
config,
|
|
257
|
+
vision_available=snapshot.vision_available if vision else None,
|
|
258
|
+
),
|
|
259
|
+
"wire_api": config.wire_api,
|
|
260
|
+
"api_format": config.api_format,
|
|
261
|
+
"context_window": config.context_window,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _with_role_identity(snapshot: IndexingLLMSnapshot, *, vision: bool) -> IndexingLLMSnapshot:
|
|
266
|
+
snapshot = replace(
|
|
267
|
+
snapshot,
|
|
268
|
+
selection=replace(snapshot.selection, reasoning_effort=snapshot.config.reasoning_effort),
|
|
269
|
+
)
|
|
270
|
+
identity = _role_identity(snapshot, vision=vision)
|
|
271
|
+
return replace(snapshot, descriptor=identity, fingerprint=_fingerprint(identity))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _freeze_role(
|
|
275
|
+
selection_value: Any, *, vision: bool = False, provider_default: bool = True
|
|
276
|
+
) -> IndexingLLMSnapshot:
|
|
277
|
+
from deeptutor.services.config.lightrag_roles import LightRagModelSelection
|
|
278
|
+
|
|
279
|
+
from .roles import resolve_selection
|
|
280
|
+
|
|
281
|
+
if isinstance(selection_value, LLMSelection):
|
|
282
|
+
selection_value = selection_value.to_dict()
|
|
283
|
+
selected = LightRagModelSelection.model_validate(selection_value)
|
|
284
|
+
selection = LLMSelection(**selected.model_dump())
|
|
285
|
+
try:
|
|
286
|
+
config = resolve_selection(selection, vision=vision, provider_default=provider_default)
|
|
287
|
+
except (ValueError, PermissionError, LLMConfigError) as exc:
|
|
288
|
+
raise IndexingPolicyError(str(exc)) from exc
|
|
289
|
+
explicit_user = get_current_user_or_none()
|
|
290
|
+
if config.binding in OWNER_BOUND_BINDINGS and explicit_user is None:
|
|
291
|
+
raise IndexingPolicyError(
|
|
292
|
+
"Owner-bound indexing models require an explicit initiating user scope."
|
|
293
|
+
)
|
|
294
|
+
snapshot = IndexingLLMSnapshot(
|
|
295
|
+
config=config,
|
|
296
|
+
owner=explicit_user or get_current_user(),
|
|
297
|
+
policy=POLICY_PINNED,
|
|
298
|
+
selection=replace(selection, reasoning_effort=config.reasoning_effort),
|
|
299
|
+
descriptor={},
|
|
300
|
+
fingerprint=None,
|
|
301
|
+
vision_available=supports_vision(config.binding, config.model),
|
|
302
|
+
)
|
|
303
|
+
return _with_role_identity(snapshot, vision=vision)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def freeze_roles(
|
|
307
|
+
selection_value: Any = None, *, policy: str = POLICY_PINNED
|
|
308
|
+
) -> IndexingPolicySnapshot:
|
|
309
|
+
"""Freeze create/rebuild choices; old request bodies still mean one model."""
|
|
310
|
+
from deeptutor.services.config import load_lightrag_settings
|
|
311
|
+
from deeptutor.services.config.lightrag_roles import LightRagIndexingSelection
|
|
312
|
+
|
|
313
|
+
from .roles import runtime_limits, settings_models
|
|
314
|
+
|
|
315
|
+
settings = load_lightrag_settings()
|
|
316
|
+
models = settings_models(settings)
|
|
317
|
+
limits = runtime_limits(settings)
|
|
318
|
+
if selection_value is None and models is None and settings.get("version") == 2:
|
|
319
|
+
raise IndexingPolicyError("Choose a LightRAG base model or explicit indexing models first.")
|
|
320
|
+
if selection_value is None and models is not None:
|
|
321
|
+
extract_selection = models.selection_for("extract").model_dump()
|
|
322
|
+
vlm_selection = models.selection_for("vlm")
|
|
323
|
+
value = {
|
|
324
|
+
"extract": extract_selection,
|
|
325
|
+
"vlm": {"mode": "enabled", "selection": vlm_selection.model_dump()}
|
|
326
|
+
if vlm_selection
|
|
327
|
+
else {"mode": "disabled"},
|
|
328
|
+
}
|
|
329
|
+
elif (
|
|
330
|
+
selection_value is not None
|
|
331
|
+
and isinstance(selection_value, dict)
|
|
332
|
+
and "extract" in selection_value
|
|
333
|
+
):
|
|
334
|
+
value = selection_value
|
|
335
|
+
else:
|
|
336
|
+
# Supported v1 settings and form producer: freeze the one known model,
|
|
337
|
+
# including its known vision capability, before constructing v2 roles.
|
|
338
|
+
single = (
|
|
339
|
+
freeze_snapshot(selection_value, policy=POLICY_PINNED)
|
|
340
|
+
if selection_value is not None
|
|
341
|
+
else _freeze_legacy_default_snapshot()
|
|
342
|
+
)
|
|
343
|
+
if single.selection is None:
|
|
344
|
+
raise IndexingPolicyError("Choose a catalog model before creating a LightRAG index.")
|
|
345
|
+
selected = single.selection.to_dict()
|
|
346
|
+
value = {
|
|
347
|
+
"extract": selected,
|
|
348
|
+
"vlm": {"mode": "enabled", "selection": selected}
|
|
349
|
+
if single.vision_available and settings.get("version") != 2
|
|
350
|
+
else {"mode": "disabled"},
|
|
351
|
+
}
|
|
352
|
+
parsed = LightRagIndexingSelection.model_validate(value)
|
|
353
|
+
extract = _freeze_role(parsed.extract.model_dump())
|
|
354
|
+
vlm = (
|
|
355
|
+
_freeze_role(parsed.vlm.selection.model_dump(), vision=True)
|
|
356
|
+
if parsed.vlm.selection
|
|
357
|
+
else None
|
|
358
|
+
)
|
|
359
|
+
return IndexingPolicySnapshot(extract, vlm, policy=policy, limits=limits)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def freeze_default_snapshot() -> IndexingPolicySnapshot:
|
|
363
|
+
return freeze_roles()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def snapshot_from_persisted(policy: dict[str, Any]) -> IndexingPolicySnapshot:
|
|
367
|
+
from deeptutor.services.config import load_lightrag_settings
|
|
368
|
+
|
|
369
|
+
from .roles import runtime_limits
|
|
370
|
+
|
|
371
|
+
if not isinstance(policy, dict) or policy.get("policy") not in {POLICY_PINNED, POLICY_PENDING}:
|
|
372
|
+
raise IndexingModelChangedError(
|
|
373
|
+
"The index has no verified model policy; run a full re-index."
|
|
374
|
+
)
|
|
375
|
+
limits = runtime_limits(load_lightrag_settings())
|
|
376
|
+
if "schema_version" not in policy:
|
|
377
|
+
single = _single_snapshot_from_persisted(policy)
|
|
378
|
+
if policy["policy"] == POLICY_PENDING:
|
|
379
|
+
return IndexingPolicySnapshot(
|
|
380
|
+
_with_role_identity(single, vision=False),
|
|
381
|
+
_with_role_identity(single, vision=True) if single.vision_available else None,
|
|
382
|
+
policy=POLICY_PENDING,
|
|
383
|
+
limits=limits,
|
|
384
|
+
)
|
|
385
|
+
return IndexingPolicySnapshot(
|
|
386
|
+
single,
|
|
387
|
+
single if single.vision_available else None,
|
|
388
|
+
policy=policy["policy"],
|
|
389
|
+
limits=limits,
|
|
390
|
+
legacy_policy=dict(policy),
|
|
391
|
+
)
|
|
392
|
+
if type(policy["schema_version"]) is not int or policy["schema_version"] != 2:
|
|
393
|
+
raise IndexingPolicyError("Unsupported LightRAG indexing-policy version.")
|
|
394
|
+
vision = policy.get("vlm")
|
|
395
|
+
if not isinstance(vision, dict) or vision.get("mode") not in {"disabled", "enabled"}:
|
|
396
|
+
raise IndexingPolicyError("Invalid LightRAG vision policy.")
|
|
397
|
+
if vision["mode"] == "disabled" and "snapshot" in vision:
|
|
398
|
+
raise IndexingPolicyError("A disabled VLM cannot have an indexing snapshot.")
|
|
399
|
+
|
|
400
|
+
def restore(value: Any, *, vision: bool = False) -> IndexingLLMSnapshot:
|
|
401
|
+
if not isinstance(value, dict) or not isinstance(value.get("selection"), dict):
|
|
402
|
+
raise IndexingPolicyError("Invalid LightRAG role snapshot.")
|
|
403
|
+
descriptor = value.get("descriptor")
|
|
404
|
+
result = _freeze_role(
|
|
405
|
+
value["selection"],
|
|
406
|
+
vision=vision,
|
|
407
|
+
provider_default=isinstance(descriptor, dict)
|
|
408
|
+
and descriptor.get("reasoning_effort") == "",
|
|
409
|
+
)
|
|
410
|
+
if not value.get("fingerprint") or result.fingerprint != value["fingerprint"]:
|
|
411
|
+
raise IndexingModelChangedError("The pinned role model changed; run a full re-index.")
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
return IndexingPolicySnapshot(
|
|
415
|
+
restore(policy.get("extract")),
|
|
416
|
+
restore(vision.get("snapshot"), vision=True) if vision["mode"] == "enabled" else None,
|
|
417
|
+
policy=policy["policy"],
|
|
418
|
+
limits=limits,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def revalidate_snapshot(snapshot: IndexingPolicySnapshot) -> IndexingPolicySnapshot:
|
|
423
|
+
"""Refresh credentials and permissions without changing frozen task settings."""
|
|
424
|
+
from deeptutor.multi_user.paths import user_context
|
|
425
|
+
|
|
426
|
+
with user_context(snapshot.extract.owner):
|
|
427
|
+
fresh = snapshot_from_persisted(snapshot.persisted_policy())
|
|
428
|
+
return replace(
|
|
429
|
+
fresh,
|
|
430
|
+
limits=snapshot.limits,
|
|
431
|
+
target_version=snapshot.target_version,
|
|
432
|
+
target_bound=snapshot.target_bound,
|
|
433
|
+
target_policy=snapshot.target_policy,
|
|
434
|
+
image_analysis=snapshot.image_analysis,
|
|
435
|
+
target_revision=snapshot.target_revision,
|
|
436
|
+
embedding_config=snapshot.embedding_config,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _bound_published_root(kb_dir: Path) -> Path | None:
|
|
441
|
+
"""Use the published version selected by the KB's existing embedding binding."""
|
|
442
|
+
from deeptutor.services.config.knowledge_base_config import KnowledgeBaseConfigService
|
|
443
|
+
|
|
444
|
+
from .storage import published_root_for_embedding
|
|
445
|
+
|
|
446
|
+
entry = KnowledgeBaseConfigService(kb_dir.parent / "kb_config.json").get_kb_config(kb_dir.name)
|
|
447
|
+
signature = entry.get("embedding_signature") if entry.get("embedding_selection") else None
|
|
448
|
+
return published_root_for_embedding(kb_dir, signature)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _target_policy_key(kb_dir: Path) -> str | None:
|
|
452
|
+
policy = effective_policy(kb_dir, base_dir=str(kb_dir.parent), kb_name=kb_dir.name)
|
|
453
|
+
return (
|
|
454
|
+
_fingerprint({key: value for key, value in policy.items() if key != "vlm_used"})
|
|
455
|
+
if policy is not None
|
|
456
|
+
else None
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _content_revision(root: Path | None) -> str:
|
|
461
|
+
# Native document-status flushes are atomic. Track inode and nanosecond
|
|
462
|
+
# timestamps as well as size so a same-version append invalidates a queued rebuild.
|
|
463
|
+
from .storage import _store_root
|
|
464
|
+
|
|
465
|
+
revisions = {}
|
|
466
|
+
if root is not None:
|
|
467
|
+
paths = {
|
|
468
|
+
"meta.json": root / "meta.json",
|
|
469
|
+
"kv_store_doc_status.json": _store_root(root) / "kv_store_doc_status.json",
|
|
470
|
+
}
|
|
471
|
+
for name, path in paths.items():
|
|
472
|
+
try:
|
|
473
|
+
stat = path.stat()
|
|
474
|
+
revisions[name] = [stat.st_ino, stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns]
|
|
475
|
+
except FileNotFoundError:
|
|
476
|
+
revisions[name] = None
|
|
477
|
+
return _fingerprint(revisions)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def bind_target(
|
|
481
|
+
snapshot: IndexingPolicySnapshot, kb_dir: Path, *, protect_contents: bool = False
|
|
482
|
+
) -> IndexingPolicySnapshot:
|
|
483
|
+
"""Bind a snapshot to the current index and optionally its content revision."""
|
|
484
|
+
target = _bound_published_root(kb_dir)
|
|
485
|
+
snapshot = with_embedding(snapshot, kb_dir=kb_dir)
|
|
486
|
+
return replace(
|
|
487
|
+
snapshot,
|
|
488
|
+
target_version=str(target) if target else None,
|
|
489
|
+
target_bound=True,
|
|
490
|
+
target_policy=_target_policy_key(kb_dir),
|
|
491
|
+
target_revision=_content_revision(target) if protect_contents else None,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def validate_target(snapshot: IndexingPolicySnapshot, kb_dir: Path) -> None:
|
|
496
|
+
"""Reject a queued write if its bound index, policy, or revision changed."""
|
|
497
|
+
if not snapshot.target_bound:
|
|
498
|
+
return
|
|
499
|
+
target = _bound_published_root(kb_dir)
|
|
500
|
+
if (
|
|
501
|
+
snapshot.target_version != (str(target) if target else None)
|
|
502
|
+
or snapshot.target_policy != _target_policy_key(kb_dir)
|
|
503
|
+
or (
|
|
504
|
+
snapshot.target_revision is not None
|
|
505
|
+
and snapshot.target_revision != _content_revision(target)
|
|
506
|
+
)
|
|
507
|
+
):
|
|
508
|
+
raise IndexingPolicyError(
|
|
509
|
+
"The target index or pending policy changed while queued; resubmit this operation."
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def with_image_analysis(
|
|
514
|
+
snapshot: IndexingPolicySnapshot, requested: bool | None
|
|
515
|
+
) -> IndexingPolicySnapshot:
|
|
516
|
+
"""Record the requested image mode, rejecting unavailable pinned vision."""
|
|
517
|
+
if requested is True and not snapshot.vision_available:
|
|
518
|
+
raise IndexingModelChangedError(
|
|
519
|
+
"Image analysis is disabled for this index; run a full re-index with a VLM."
|
|
520
|
+
)
|
|
521
|
+
return replace(snapshot, image_analysis=requested)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def effective_policy(kb_dir: Path, *, base_dir: str, kb_name: str) -> dict[str, Any] | None:
|
|
525
|
+
"""Return published policy first, then pre-publication pending policy."""
|
|
526
|
+
from .storage import read_published_policy
|
|
527
|
+
|
|
528
|
+
root = _bound_published_root(kb_dir)
|
|
529
|
+
published = read_published_policy(root)
|
|
530
|
+
if published is not None:
|
|
531
|
+
return published
|
|
532
|
+
|
|
533
|
+
from deeptutor.knowledge.manager import KnowledgeBaseManager
|
|
534
|
+
|
|
535
|
+
manager = KnowledgeBaseManager(base_dir=base_dir)
|
|
536
|
+
entry = manager.config.get("knowledge_bases", {}).get(kb_name) or {}
|
|
537
|
+
pending = entry.get("pending_indexing_policy")
|
|
538
|
+
if pending is not None and not isinstance(pending, dict):
|
|
539
|
+
raise IndexingPolicyError("Invalid pending LightRAG indexing policy.")
|
|
540
|
+
return pending
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def resolve_write_snapshot(
|
|
544
|
+
kb_dir: Path,
|
|
545
|
+
*,
|
|
546
|
+
base_dir: str,
|
|
547
|
+
kb_name: str,
|
|
548
|
+
explicit: IndexingPolicySnapshot | None = None,
|
|
549
|
+
) -> IndexingPolicySnapshot:
|
|
550
|
+
if explicit is not None:
|
|
551
|
+
return explicit
|
|
552
|
+
# Empty KBs (including old creation-time pending policies) follow defaults.
|
|
553
|
+
# Published/partial versions still require verified historical identities.
|
|
554
|
+
from deeptutor.services.rag.index_versioning import list_kb_versions
|
|
555
|
+
|
|
556
|
+
from .storage import latest_published_root
|
|
557
|
+
|
|
558
|
+
if latest_published_root(kb_dir) is None and not list_kb_versions(kb_dir):
|
|
559
|
+
return freeze_default_snapshot()
|
|
560
|
+
policy = effective_policy(kb_dir, base_dir=base_dir, kb_name=kb_name)
|
|
561
|
+
if policy is None or policy.get("policy") == POLICY_LEGACY:
|
|
562
|
+
raise IndexingModelChangedError(
|
|
563
|
+
"This LightRAG index has no verified indexing model; run a full re-index "
|
|
564
|
+
"before appending documents."
|
|
565
|
+
)
|
|
566
|
+
return snapshot_from_persisted(policy)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def with_embedding(
|
|
570
|
+
snapshot: IndexingPolicySnapshot, *, kb_dir: Path | None = None
|
|
571
|
+
) -> IndexingPolicySnapshot:
|
|
572
|
+
"""Freeze embedding configuration once, before accepting an indexing task."""
|
|
573
|
+
if snapshot.embedding_config is not None:
|
|
574
|
+
return snapshot
|
|
575
|
+
from deeptutor.services.embedding import get_embedding_config
|
|
576
|
+
|
|
577
|
+
config = None
|
|
578
|
+
if kb_dir is not None:
|
|
579
|
+
from deeptutor.services.config.knowledge_base_config import KnowledgeBaseConfigService
|
|
580
|
+
from deeptutor.services.rag.embedding_binding import binding_status
|
|
581
|
+
|
|
582
|
+
entry = KnowledgeBaseConfigService(kb_dir.parent / "kb_config.json").get_kb_config(
|
|
583
|
+
kb_dir.name
|
|
584
|
+
)
|
|
585
|
+
state, config = binding_status(entry)
|
|
586
|
+
if state in {"missing", "unconfigured", "changed"}:
|
|
587
|
+
raise IndexingPolicyError(
|
|
588
|
+
"The bound embedding model is unavailable or changed. Restore its configuration or rebuild."
|
|
589
|
+
)
|
|
590
|
+
config = deepcopy(config if config is not None else get_embedding_config())
|
|
591
|
+
if not config.dim:
|
|
592
|
+
raise IndexingPolicyError(
|
|
593
|
+
"Configure an embedding model with a known dimension in Settings."
|
|
594
|
+
)
|
|
595
|
+
return replace(snapshot, embedding_config=config)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def public_rebuild_config(
|
|
599
|
+
snapshot: IndexingPolicySnapshot, embedding_selection: dict | None = None
|
|
600
|
+
) -> dict[str, Any]:
|
|
601
|
+
"""Bind the confirmation to resolved identities without exposing credentials."""
|
|
602
|
+
from deeptutor.services.rag.embedding_signature import signature_from_config
|
|
603
|
+
|
|
604
|
+
snapshot = with_embedding(snapshot)
|
|
605
|
+
signature = signature_from_config(snapshot.embedding_config)
|
|
606
|
+
policy = snapshot.persisted_policy()
|
|
607
|
+
return {
|
|
608
|
+
"fingerprint": _fingerprint(
|
|
609
|
+
{
|
|
610
|
+
"indexing_policy": policy,
|
|
611
|
+
"embedding": signature.hash(),
|
|
612
|
+
"embedding_selection": embedding_selection,
|
|
613
|
+
}
|
|
614
|
+
),
|
|
615
|
+
"indexing_policy": public_policy(policy),
|
|
616
|
+
"embedding": {"model": signature.model, "dimension": signature.dimension},
|
|
617
|
+
"embedding_selection": embedding_selection,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def cache_identity(snapshot: IndexingLLMSnapshot) -> str:
|
|
622
|
+
if snapshot.fingerprint:
|
|
623
|
+
return snapshot.fingerprint
|
|
624
|
+
identity = {
|
|
625
|
+
"binding": snapshot.config.binding,
|
|
626
|
+
"provider_mode": snapshot.config.provider_mode,
|
|
627
|
+
"endpoint": _endpoint_identity(snapshot.config.effective_url or snapshot.config.base_url),
|
|
628
|
+
"model": snapshot.config.model,
|
|
629
|
+
"reasoning_effort": snapshot.config.reasoning_effort,
|
|
630
|
+
}
|
|
631
|
+
return _fingerprint(identity)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def cache_identity_for_config(config: LLMConfig) -> str:
|
|
635
|
+
"""Return a credential-free cache identity for a resolved query model."""
|
|
636
|
+
return _fingerprint(
|
|
637
|
+
{
|
|
638
|
+
**_canonical_identity(None, config),
|
|
639
|
+
"wire_api": config.wire_api,
|
|
640
|
+
"api_format": config.api_format,
|
|
641
|
+
"context_window": config.context_window,
|
|
642
|
+
}
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def public_policy(policy: dict[str, Any]) -> dict[str, Any]:
|
|
647
|
+
"""Expose provenance without private endpoint or owner metadata."""
|
|
648
|
+
result = {
|
|
649
|
+
key: policy[key]
|
|
650
|
+
for key in ("schema_version", "policy", "fingerprint", "vision_available", "vlm_used")
|
|
651
|
+
if key in policy
|
|
652
|
+
}
|
|
653
|
+
selection = policy.get("selection")
|
|
654
|
+
if isinstance(selection, dict):
|
|
655
|
+
result["selection"] = {
|
|
656
|
+
key: selection[key]
|
|
657
|
+
for key in ("profile_id", "model_id", "reasoning_effort")
|
|
658
|
+
if key in selection
|
|
659
|
+
}
|
|
660
|
+
descriptor = policy.get("descriptor")
|
|
661
|
+
if isinstance(descriptor, dict):
|
|
662
|
+
result["descriptor"] = {
|
|
663
|
+
key: descriptor[key]
|
|
664
|
+
for key in ("model", "binding", "provider_mode", "reasoning_effort", "vision_available")
|
|
665
|
+
if key in descriptor
|
|
666
|
+
}
|
|
667
|
+
if isinstance(policy.get("extract"), dict):
|
|
668
|
+
result["extract"] = public_policy(policy["extract"])
|
|
669
|
+
vlm = policy.get("vlm")
|
|
670
|
+
if isinstance(vlm, dict):
|
|
671
|
+
result["vlm"] = {"mode": vlm.get("mode")}
|
|
672
|
+
if isinstance(vlm.get("snapshot"), dict):
|
|
673
|
+
result["vlm"]["snapshot"] = public_policy(vlm["snapshot"])
|
|
674
|
+
return result
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
__all__ = [
|
|
678
|
+
"IndexingLLMSnapshot",
|
|
679
|
+
"IndexingPolicySnapshot",
|
|
680
|
+
"freeze_roles",
|
|
681
|
+
"bind_target",
|
|
682
|
+
"revalidate_snapshot",
|
|
683
|
+
"IndexingModelChangedError",
|
|
684
|
+
"IndexingPolicyError",
|
|
685
|
+
"POLICY_LEGACY",
|
|
686
|
+
"POLICY_PENDING",
|
|
687
|
+
"POLICY_PINNED",
|
|
688
|
+
"cache_identity",
|
|
689
|
+
"cache_identity_for_config",
|
|
690
|
+
"effective_policy",
|
|
691
|
+
"freeze_default_snapshot",
|
|
692
|
+
"freeze_snapshot",
|
|
693
|
+
"pending_policy_for_selection",
|
|
694
|
+
"resolve_write_snapshot",
|
|
695
|
+
"snapshot_from_persisted",
|
|
696
|
+
]
|