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,72 @@
|
|
|
1
|
+
"""Background service that periodically syncs GitHub sources into KBs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from deeptutor.multi_user.context import reset_current_user, set_current_user
|
|
9
|
+
from deeptutor.multi_user.paths import local_admin_user
|
|
10
|
+
from deeptutor.services.base_sync import BaseSourceSyncService, default_base_dir, is_stale
|
|
11
|
+
from deeptutor.services.github_source.sync import SYNC_INTERVAL_HOURS, redact_sync_error
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GitHubSourceSyncService(BaseSourceSyncService):
|
|
17
|
+
"""Periodically syncs all GitHub sources."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, *, base_dir=None, client=None, check_interval_s=3600, owner=None):
|
|
20
|
+
if owner is None and base_dir is not None:
|
|
21
|
+
if Path(base_dir).resolve() != Path(default_base_dir()).resolve():
|
|
22
|
+
raise ValueError("A custom GitHub sync base_dir requires an explicit owner.")
|
|
23
|
+
super().__init__(base_dir=base_dir, check_interval_s=check_interval_s)
|
|
24
|
+
self._client = client
|
|
25
|
+
self._owner = owner or local_admin_user()
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def task_name(self) -> str:
|
|
29
|
+
return "github-source-sync"
|
|
30
|
+
|
|
31
|
+
async def _sync_one_cycle(self) -> None:
|
|
32
|
+
from deeptutor.knowledge.manager import KnowledgeBaseManager
|
|
33
|
+
from deeptutor.services.github_source.sync import sync_source
|
|
34
|
+
|
|
35
|
+
token = set_current_user(self._owner)
|
|
36
|
+
try:
|
|
37
|
+
base_dir = self.effective_base_dir
|
|
38
|
+
manager = KnowledgeBaseManager(base_dir=base_dir)
|
|
39
|
+
|
|
40
|
+
all_sources = manager.get_all_github_sources()
|
|
41
|
+
for kb_name, source in all_sources:
|
|
42
|
+
if not source.get("enabled", True):
|
|
43
|
+
continue
|
|
44
|
+
if not is_stale(source, stale_hours=SYNC_INTERVAL_HOURS):
|
|
45
|
+
continue
|
|
46
|
+
sid = source.get("id", "?")
|
|
47
|
+
try:
|
|
48
|
+
await sync_source(
|
|
49
|
+
kb_name=kb_name,
|
|
50
|
+
source=source,
|
|
51
|
+
base_dir=base_dir,
|
|
52
|
+
client=self._client,
|
|
53
|
+
)
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
manager.update_github_source_state(
|
|
56
|
+
kb_name=kb_name,
|
|
57
|
+
source_id=sid,
|
|
58
|
+
last_sync_status="error",
|
|
59
|
+
last_sync_error=redact_sync_error(exc),
|
|
60
|
+
)
|
|
61
|
+
finally:
|
|
62
|
+
reset_current_user(token)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_sync_service = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_sync_service():
|
|
69
|
+
global _sync_service
|
|
70
|
+
if _sync_service is None:
|
|
71
|
+
_sync_service = GitHubSourceSyncService()
|
|
72
|
+
return _sync_service
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""RAG service exports."""
|
|
2
|
+
|
|
3
|
+
from .factory import (
|
|
4
|
+
DEFAULT_PROVIDER,
|
|
5
|
+
get_pipeline,
|
|
6
|
+
list_pipelines,
|
|
7
|
+
normalize_provider_name,
|
|
8
|
+
)
|
|
9
|
+
from .file_routing import DocumentType, FileClassification, FileTypeRouter
|
|
10
|
+
from .service import RAGService
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"RAGService",
|
|
14
|
+
"FileTypeRouter",
|
|
15
|
+
"FileClassification",
|
|
16
|
+
"DocumentType",
|
|
17
|
+
"get_pipeline",
|
|
18
|
+
"list_pipelines",
|
|
19
|
+
"normalize_provider_name",
|
|
20
|
+
"DEFAULT_PROVIDER",
|
|
21
|
+
]
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""Per-knowledge-base embedding references and compatible legacy migration.
|
|
2
|
+
|
|
3
|
+
Persist catalog IDs and index identity only. Credentials are always resolved
|
|
4
|
+
from the current catalog; changing the global default never changes a binding.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from deeptutor.services.embedding.config import EmbeddingConfig
|
|
15
|
+
|
|
16
|
+
from .embedding_signature import signature_from_config
|
|
17
|
+
|
|
18
|
+
EMBEDDING_PROVIDERS = {"llamaindex", "graphrag", "lightrag"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_embedding_config(selection=None, *, catalog=None):
|
|
22
|
+
# Config imports the RAG factory during package initialization.
|
|
23
|
+
from deeptutor.services.embedding.config import get_embedding_config as resolve
|
|
24
|
+
|
|
25
|
+
return resolve(selection, catalog=catalog)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def uses_bound_embedding(entry: dict) -> bool:
|
|
29
|
+
from deeptutor.knowledge.kb_types import is_connected_kb
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
not is_connected_kb(entry)
|
|
33
|
+
and entry.get("rag_provider", "llamaindex") in EMBEDDING_PROVIDERS
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_catalog() -> dict:
|
|
38
|
+
from deeptutor.services.config import get_model_catalog_service
|
|
39
|
+
|
|
40
|
+
return get_model_catalog_service().load()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def default_selection(catalog: dict | None = None) -> dict | None:
|
|
44
|
+
service = (
|
|
45
|
+
(catalog if catalog is not None else load_catalog())
|
|
46
|
+
.get("services", {})
|
|
47
|
+
.get("embedding", {})
|
|
48
|
+
)
|
|
49
|
+
if service.get("active_profile_id") and service.get("active_model_id"):
|
|
50
|
+
return {"profile_id": service["active_profile_id"], "model_id": service["active_model_id"]}
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def binding_fields(selection: dict, config) -> dict:
|
|
55
|
+
return {
|
|
56
|
+
"embedding_selection": {
|
|
57
|
+
"profile_id": selection["profile_id"],
|
|
58
|
+
"model_id": selection["model_id"],
|
|
59
|
+
},
|
|
60
|
+
"embedding_model": config.model,
|
|
61
|
+
"embedding_dim": config.dim,
|
|
62
|
+
"embedding_signature": signature_from_config(config).hash(),
|
|
63
|
+
"embedding_status": "ready",
|
|
64
|
+
"embedding_mismatch": False,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def migrate_binding(entry: dict, kb_dir: Path, catalog: dict | None = None) -> bool:
|
|
69
|
+
"""Pin only when existing evidence identifies a compatible catalog model.
|
|
70
|
+
|
|
71
|
+
Older indexes without enough evidence retain their legacy read path. A
|
|
72
|
+
successful full rebuild will pin the model that produced the new vectors.
|
|
73
|
+
"""
|
|
74
|
+
if entry.get("embedding_selection") or not uses_bound_embedding(entry):
|
|
75
|
+
return False
|
|
76
|
+
from .index_versioning import list_kb_versions
|
|
77
|
+
|
|
78
|
+
catalog = catalog if catalog is not None else load_catalog()
|
|
79
|
+
versions = list_kb_versions(kb_dir)
|
|
80
|
+
signatures = {v.get("embedding_signature") or v.get("signature") for v in versions}
|
|
81
|
+
stored_signature = entry.get("embedding_signature")
|
|
82
|
+
if stored_signature in EMBEDDING_PROVIDERS | {"legacy"}:
|
|
83
|
+
stored_signature = None
|
|
84
|
+
matches = []
|
|
85
|
+
for profile in catalog.get("services", {}).get("embedding", {}).get("profiles", []):
|
|
86
|
+
for model in profile.get("models", []):
|
|
87
|
+
selection = {"profile_id": profile.get("id"), "model_id": model.get("id")}
|
|
88
|
+
try:
|
|
89
|
+
config = get_embedding_config(selection, catalog=catalog)
|
|
90
|
+
except ValueError:
|
|
91
|
+
continue
|
|
92
|
+
signature = signature_from_config(config).hash()
|
|
93
|
+
# Prefer the recorded index identity, not whichever version happens
|
|
94
|
+
# to match today's global default.
|
|
95
|
+
if stored_signature:
|
|
96
|
+
matches_index = signature == stored_signature
|
|
97
|
+
elif entry.get("embedding_model"):
|
|
98
|
+
matches_index = (
|
|
99
|
+
config.model == entry["embedding_model"]
|
|
100
|
+
and (not entry.get("embedding_dim") or config.dim == entry["embedding_dim"])
|
|
101
|
+
and (
|
|
102
|
+
not signatures
|
|
103
|
+
or signature in signatures
|
|
104
|
+
or all(s in EMBEDDING_PROVIDERS | {"legacy"} or not s for s in signatures)
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
matches_index = signature in signatures
|
|
109
|
+
if matches_index:
|
|
110
|
+
matches.append((selection, config))
|
|
111
|
+
if not matches:
|
|
112
|
+
return False
|
|
113
|
+
# Duplicate catalog entries for the same vector space are equivalent. If
|
|
114
|
+
# there are different identities, leave the legacy binding unresolved.
|
|
115
|
+
if len({signature_from_config(config).hash() for _, config in matches}) != 1:
|
|
116
|
+
return False
|
|
117
|
+
active = default_selection(catalog)
|
|
118
|
+
selection, config = next((item for item in matches if item[0] == active), matches[0])
|
|
119
|
+
entry.update(binding_fields(selection, config))
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def binding_status(
|
|
124
|
+
entry: dict, *, catalog: dict | None = None
|
|
125
|
+
) -> tuple[str, EmbeddingConfig | None]:
|
|
126
|
+
selection = entry.get("embedding_selection")
|
|
127
|
+
if not selection:
|
|
128
|
+
return "legacy", None
|
|
129
|
+
if not isinstance(selection, dict):
|
|
130
|
+
return "missing", None
|
|
131
|
+
catalog = catalog if catalog is not None else load_catalog()
|
|
132
|
+
service = catalog.get("services", {}).get("embedding", {})
|
|
133
|
+
profile = next(
|
|
134
|
+
(p for p in service.get("profiles", []) if p.get("id") == selection.get("profile_id")), None
|
|
135
|
+
)
|
|
136
|
+
if not any(m.get("id") == selection.get("model_id") for m in (profile or {}).get("models", [])):
|
|
137
|
+
return "missing", None
|
|
138
|
+
try:
|
|
139
|
+
config = get_embedding_config(selection, catalog=catalog)
|
|
140
|
+
except ValueError:
|
|
141
|
+
return "unconfigured", None
|
|
142
|
+
if (
|
|
143
|
+
entry.get("embedding_signature")
|
|
144
|
+
and entry["embedding_signature"] != signature_from_config(config).hash()
|
|
145
|
+
):
|
|
146
|
+
return "changed", config
|
|
147
|
+
return "ready", config
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def entry_signature(entry: dict):
|
|
151
|
+
"""Resolve a bound signature, falling back only for unbound legacy KBs."""
|
|
152
|
+
from .embedding_signature import signature_from_embedding_config
|
|
153
|
+
|
|
154
|
+
if not entry.get("embedding_selection"):
|
|
155
|
+
return signature_from_embedding_config()
|
|
156
|
+
status, config = binding_status(entry)
|
|
157
|
+
return signature_from_config(config) if config is not None else None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def bound_graph_storage_root(kb_dir: Path, provider: str, fallback: Path | None) -> Path | None:
|
|
161
|
+
"""Keep graph reads on a compatible version if publishing a new binding failed."""
|
|
162
|
+
from deeptutor.services.embedding.config import scoped_embedding_config
|
|
163
|
+
|
|
164
|
+
from .index_probe import inspect_provider_version
|
|
165
|
+
from .index_versioning import list_kb_versions
|
|
166
|
+
|
|
167
|
+
config = scoped_embedding_config()
|
|
168
|
+
if config is None or fallback is None:
|
|
169
|
+
return fallback
|
|
170
|
+
expected = signature_from_config(config).hash()
|
|
171
|
+
versions = list_kb_versions(kb_dir)
|
|
172
|
+
for version in versions:
|
|
173
|
+
if version.get("embedding_signature") != expected or not version.get("ready"):
|
|
174
|
+
continue
|
|
175
|
+
root = Path(version["storage_path"])
|
|
176
|
+
if provider == "lightrag":
|
|
177
|
+
from .pipelines.lightrag.storage import meta_is_native_published
|
|
178
|
+
|
|
179
|
+
if not meta_is_native_published(root):
|
|
180
|
+
continue
|
|
181
|
+
if inspect_provider_version(version, provider).ready:
|
|
182
|
+
return root
|
|
183
|
+
fallback_entry = next((v for v in versions if Path(v["storage_path"]) == fallback), {})
|
|
184
|
+
if not fallback_entry.get("embedding_signature"):
|
|
185
|
+
return fallback # Older graph indexes did not record embedding identity.
|
|
186
|
+
raise ValueError(
|
|
187
|
+
"No index version matches the bound embedding model. Re-index this knowledge base."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def reconcile_bindings(entries: dict, base_dir: Path) -> bool:
|
|
192
|
+
"""Refresh binding health without touching files or changing index identity."""
|
|
193
|
+
changed = False
|
|
194
|
+
try:
|
|
195
|
+
catalog = load_catalog()
|
|
196
|
+
except (ValueError, OSError):
|
|
197
|
+
return False
|
|
198
|
+
for name, entry in entries.items():
|
|
199
|
+
if not isinstance(entry, dict) or not uses_bound_embedding(entry):
|
|
200
|
+
continue
|
|
201
|
+
before = dict(entry)
|
|
202
|
+
migrate_binding(entry, base_dir / name, catalog)
|
|
203
|
+
if entry.get("embedding_selection"):
|
|
204
|
+
state, _ = binding_status(entry, catalog=catalog)
|
|
205
|
+
entry["embedding_status"] = state
|
|
206
|
+
had_mismatch = bool(before.get("embedding_mismatch"))
|
|
207
|
+
if state == "changed":
|
|
208
|
+
entry["embedding_mismatch"] = True
|
|
209
|
+
entry["needs_reindex"] = True
|
|
210
|
+
elif state == "ready":
|
|
211
|
+
entry.pop("embedding_mismatch", None)
|
|
212
|
+
if had_mismatch:
|
|
213
|
+
entry["needs_reindex"] = False
|
|
214
|
+
# Keep the legacy index picker supplied with the same version list.
|
|
215
|
+
from .index_probe import inspect_kb_versions
|
|
216
|
+
|
|
217
|
+
entry["index_versions"] = inspect_kb_versions(
|
|
218
|
+
base_dir / name, entry.get("rag_provider")
|
|
219
|
+
)
|
|
220
|
+
changed = changed or before != entry
|
|
221
|
+
return changed
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _service(base_dir):
|
|
225
|
+
from deeptutor.services.config.knowledge_base_config import KnowledgeBaseConfigService
|
|
226
|
+
|
|
227
|
+
return KnowledgeBaseConfigService(Path(base_dir) / "kb_config.json")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def persist_binding(base_dir, kb_name: str, selection: dict, config) -> None:
|
|
231
|
+
_service(base_dir).set_kb_config(
|
|
232
|
+
kb_name, {**binding_fields(selection, config), "needs_reindex": False}
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def with_kb_embedding(method):
|
|
237
|
+
"""Resolve and isolate the model for the entire async RAG operation."""
|
|
238
|
+
|
|
239
|
+
@wraps(method)
|
|
240
|
+
async def wrapped(self, *args, **kwargs):
|
|
241
|
+
# search(query, kb_name); initialize/add_documents(kb_name, files).
|
|
242
|
+
kb_name = kwargs.get("kb_name") or args[1 if method.__name__ == "search" else 0]
|
|
243
|
+
service = _service(self.kb_base_dir)
|
|
244
|
+
entry = service.get_kb_config(kb_name)
|
|
245
|
+
entry["rag_provider"] = self._resolve_provider(kb_name)
|
|
246
|
+
if not uses_bound_embedding(entry):
|
|
247
|
+
return await method(self, *args, **kwargs)
|
|
248
|
+
selection = kwargs.pop("embedding_selection", None)
|
|
249
|
+
frozen_config = kwargs.pop("embedding_config", None)
|
|
250
|
+
if frozen_config is None and entry["rag_provider"] == "lightrag":
|
|
251
|
+
snapshot = kwargs.get("indexing_snapshot") or kwargs.get("accepted_indexing_snapshot")
|
|
252
|
+
if snapshot is not None:
|
|
253
|
+
frozen_config = snapshot.embedding_config
|
|
254
|
+
explicit = selection is not None
|
|
255
|
+
if not explicit:
|
|
256
|
+
if migrate_binding(entry, Path(self.kb_base_dir) / kb_name):
|
|
257
|
+
service.set_kb_config(
|
|
258
|
+
kb_name, {k: v for k, v in entry.items() if k.startswith("embedding_")}
|
|
259
|
+
)
|
|
260
|
+
selection = entry.get("embedding_selection")
|
|
261
|
+
if not selection and method.__name__ != "initialize":
|
|
262
|
+
from .index_versioning import list_kb_versions
|
|
263
|
+
|
|
264
|
+
if list_kb_versions(Path(self.kb_base_dir) / kb_name):
|
|
265
|
+
# In particular, old GraphRAG indexes use the model in
|
|
266
|
+
# their saved settings.yaml. Do not overwrite that runtime
|
|
267
|
+
# with today's default when migration could not identify it.
|
|
268
|
+
return await method(self, *args, **kwargs)
|
|
269
|
+
try:
|
|
270
|
+
if selection:
|
|
271
|
+
status, config = binding_status({**entry, "embedding_selection": selection})
|
|
272
|
+
if status == "missing":
|
|
273
|
+
raise ValueError(
|
|
274
|
+
"The embedding model bound to this knowledge base was deleted. Select another embedding model to re-index it."
|
|
275
|
+
)
|
|
276
|
+
if status == "unconfigured":
|
|
277
|
+
raise ValueError(
|
|
278
|
+
"The bound embedding model is not configured. Check its provider settings."
|
|
279
|
+
)
|
|
280
|
+
if status == "changed" and not explicit:
|
|
281
|
+
raise ValueError(
|
|
282
|
+
"The bound embedding model configuration changed. Re-index this knowledge base before using it."
|
|
283
|
+
)
|
|
284
|
+
if frozen_config is not None:
|
|
285
|
+
config = frozen_config
|
|
286
|
+
else:
|
|
287
|
+
selection = default_selection()
|
|
288
|
+
if selection is None:
|
|
289
|
+
# Preserve legacy callers and installations without a
|
|
290
|
+
# configured catalog. The pipeline retains its own checks.
|
|
291
|
+
return await method(self, *args, **kwargs)
|
|
292
|
+
config = get_embedding_config(selection)
|
|
293
|
+
except ValueError as exc:
|
|
294
|
+
if method.__name__ != "search":
|
|
295
|
+
raise
|
|
296
|
+
return {
|
|
297
|
+
"query": kwargs.get("query", args[0] if args else ""),
|
|
298
|
+
"answer": str(exc),
|
|
299
|
+
"content": "",
|
|
300
|
+
"error_type": "embedding_binding_unavailable",
|
|
301
|
+
}
|
|
302
|
+
from deeptutor.services.embedding.config import embedding_config_scope
|
|
303
|
+
|
|
304
|
+
locked_publication = entry["rag_provider"] == "lightrag" and method.__name__ != "search"
|
|
305
|
+
if locked_publication:
|
|
306
|
+
expected_binding = (entry.get("embedding_selection"), entry.get("embedding_signature"))
|
|
307
|
+
|
|
308
|
+
def validate_binding():
|
|
309
|
+
current = service.get_kb_config(kb_name)
|
|
310
|
+
if (
|
|
311
|
+
current.get("embedding_selection"),
|
|
312
|
+
current.get("embedding_signature"),
|
|
313
|
+
) != expected_binding:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
"The knowledge-base embedding binding changed before indexing started; resubmit the operation."
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def publish_binding():
|
|
319
|
+
persist_binding(self.kb_base_dir, kb_name, selection, config)
|
|
320
|
+
|
|
321
|
+
# LightRAG invokes both callbacks under its existing write ownership.
|
|
322
|
+
kwargs["validate_embedding_binding"] = validate_binding
|
|
323
|
+
kwargs["publish_embedding_binding"] = publish_binding
|
|
324
|
+
|
|
325
|
+
with embedding_config_scope(config):
|
|
326
|
+
result = await method(self, *args, **kwargs)
|
|
327
|
+
if result and method.__name__ != "search" and selection and not locked_publication:
|
|
328
|
+
persist_binding(self.kb_base_dir, kb_name, selection, config)
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
return wrapped
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Embedding-signature helpers for RAG index version selection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from deeptutor.services.rag.index_versioning import EmbeddingSignature
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def signature_from_config(config: Any) -> EmbeddingSignature:
|
|
14
|
+
"""Build a stable RAG index signature from an embedding config object."""
|
|
15
|
+
binding = (getattr(config, "binding", "") or "").strip().lower()
|
|
16
|
+
# Role support is a vector-space change. Jina previously sent no task, so
|
|
17
|
+
# its role-aware indexes need a different signature. Providers that were
|
|
18
|
+
# already role-aware before signatures gained this field keep the blank
|
|
19
|
+
# value to avoid invalidating compatible indexes.
|
|
20
|
+
role_semantics = "jina-task" if binding == "jina" else ""
|
|
21
|
+
return EmbeddingSignature(
|
|
22
|
+
binding=binding,
|
|
23
|
+
model=(getattr(config, "model", "") or "").strip(),
|
|
24
|
+
dimension=int(getattr(config, "dim", 0) or 0),
|
|
25
|
+
base_url=(
|
|
26
|
+
getattr(config, "effective_url", None) or getattr(config, "base_url", None) or ""
|
|
27
|
+
).strip(),
|
|
28
|
+
api_version=(getattr(config, "api_version", "") or "").strip(),
|
|
29
|
+
role_semantics=role_semantics,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def signature_from_embedding_config() -> EmbeddingSignature | None:
|
|
34
|
+
"""Compute the signature for the currently-active embedding config."""
|
|
35
|
+
try:
|
|
36
|
+
from deeptutor.services.embedding import get_embedding_config
|
|
37
|
+
except Exception: # pragma: no cover - import error
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
return signature_from_config(get_embedding_config())
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
logger.debug(f"Cannot resolve embedding signature: {exc}")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def embedding_meta_fields() -> dict[str, Any]:
|
|
48
|
+
"""Embedding identity fields to stamp into a version's ``meta.json``.
|
|
49
|
+
|
|
50
|
+
LlamaIndex versions already record the full signature; the graph engines
|
|
51
|
+
(GraphRAG/LightRAG) use a synthetic provider signature, so they stamp these
|
|
52
|
+
extra fields at build time. The probe used when *linking* an external index
|
|
53
|
+
reads them to verify the index was built with a compatible embedding model
|
|
54
|
+
— without which graph engines fail retrieval silently on a mismatch.
|
|
55
|
+
"""
|
|
56
|
+
signature = signature_from_embedding_config()
|
|
57
|
+
if signature is None:
|
|
58
|
+
return {}
|
|
59
|
+
return {
|
|
60
|
+
"embedding_signature": signature.hash(),
|
|
61
|
+
"embedding_model": signature.model,
|
|
62
|
+
"embedding_dim": signature.dimension,
|
|
63
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Retrieval-quality evaluation for knowledge bases.
|
|
2
|
+
|
|
3
|
+
``deeptutor kb eval`` scores a knowledge base against a QA set: each case pairs a
|
|
4
|
+
query with the gold passages an ideal retrieval should return, and the run reports
|
|
5
|
+
Recall@k, Precision@k, nDCG@k, MRR, MAP and Hit@k. The metrics are pure functions
|
|
6
|
+
over the ranking — no model judges the output — so a score is reproducible, cheap
|
|
7
|
+
to recompute, and comparable across engines, embedding models and retrieval
|
|
8
|
+
settings. That is what makes the set usable as a regression gate: record a
|
|
9
|
+
baseline with ``--save``, change a setting, and diff the metrics.
|
|
10
|
+
|
|
11
|
+
Nothing here writes to a knowledge base or mutates its index; evaluation only
|
|
12
|
+
reads through the same search path a chat turn uses.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from deeptutor.services.rag.eval import EvalDataset, evaluate_dataset
|
|
16
|
+
>>> dataset = EvalDataset.from_cases(
|
|
17
|
+
... [{"query": "What is attention?", "gold": ["Attention weighs each token."]}]
|
|
18
|
+
... )
|
|
19
|
+
>>> report = await evaluate_dataset(dataset, kb_name="ml-notes", k=5)
|
|
20
|
+
>>> report.aggregate()["recall@5"]
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .dataset import EvalCase, EvalDataset, EvalDatasetError, load_dataset
|
|
24
|
+
from .diff import CaseDelta, EvalDiff, MetricDelta, diff_reports
|
|
25
|
+
from .matching import (
|
|
26
|
+
DEFAULT_MIN_RATIO,
|
|
27
|
+
DEFAULT_MIN_TOKENS,
|
|
28
|
+
MatchPolicy,
|
|
29
|
+
MatchResult,
|
|
30
|
+
RelevanceMatcher,
|
|
31
|
+
normalize_text,
|
|
32
|
+
source_text,
|
|
33
|
+
)
|
|
34
|
+
from .metrics import (
|
|
35
|
+
QueryMetrics,
|
|
36
|
+
aggregate_query_metrics,
|
|
37
|
+
average_precision,
|
|
38
|
+
evaluate_hits,
|
|
39
|
+
hit_at_k,
|
|
40
|
+
hit_flags,
|
|
41
|
+
metric_keys,
|
|
42
|
+
ndcg_at_k,
|
|
43
|
+
precision_at_k,
|
|
44
|
+
recall_at_k,
|
|
45
|
+
reciprocal_rank,
|
|
46
|
+
)
|
|
47
|
+
from .report import EvalReport, format_metric
|
|
48
|
+
from .runner import (
|
|
49
|
+
SOURCE_MODE_CITATIONS,
|
|
50
|
+
SOURCE_MODE_CONTEXT,
|
|
51
|
+
ProgressFn,
|
|
52
|
+
QueryEvaluation,
|
|
53
|
+
RetrievalEvaluator,
|
|
54
|
+
evaluate_dataset,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"CaseDelta",
|
|
59
|
+
"DEFAULT_MIN_RATIO",
|
|
60
|
+
"DEFAULT_MIN_TOKENS",
|
|
61
|
+
"EvalCase",
|
|
62
|
+
"EvalDataset",
|
|
63
|
+
"EvalDatasetError",
|
|
64
|
+
"EvalDiff",
|
|
65
|
+
"EvalReport",
|
|
66
|
+
"MatchPolicy",
|
|
67
|
+
"MatchResult",
|
|
68
|
+
"MetricDelta",
|
|
69
|
+
"ProgressFn",
|
|
70
|
+
"QueryEvaluation",
|
|
71
|
+
"QueryMetrics",
|
|
72
|
+
"RelevanceMatcher",
|
|
73
|
+
"RetrievalEvaluator",
|
|
74
|
+
"SOURCE_MODE_CITATIONS",
|
|
75
|
+
"SOURCE_MODE_CONTEXT",
|
|
76
|
+
"aggregate_query_metrics",
|
|
77
|
+
"average_precision",
|
|
78
|
+
"diff_reports",
|
|
79
|
+
"evaluate_dataset",
|
|
80
|
+
"evaluate_hits",
|
|
81
|
+
"format_metric",
|
|
82
|
+
"hit_at_k",
|
|
83
|
+
"hit_flags",
|
|
84
|
+
"load_dataset",
|
|
85
|
+
"metric_keys",
|
|
86
|
+
"ndcg_at_k",
|
|
87
|
+
"normalize_text",
|
|
88
|
+
"precision_at_k",
|
|
89
|
+
"recall_at_k",
|
|
90
|
+
"reciprocal_rank",
|
|
91
|
+
"source_text",
|
|
92
|
+
]
|