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
deeptutor_cli/kb.py
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI Knowledge Base Command
|
|
3
|
+
===========================
|
|
4
|
+
|
|
5
|
+
Manage knowledge bases from the command line.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Optional
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from deeptutor.knowledge.manager import KnowledgeBaseManager
|
|
20
|
+
from deeptutor.knowledge.naming import validate_knowledge_base_name
|
|
21
|
+
from deeptutor.services.path_service import get_path_service
|
|
22
|
+
from deeptutor.services.rag.factory import DEFAULT_PROVIDER
|
|
23
|
+
from deeptutor.services.rag.file_routing import FileTypeRouter
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING: # pragma: no cover - imported for type annotations only
|
|
26
|
+
from deeptutor.services.rag.eval import EvalDataset, EvalReport, ProgressFn, QueryEvaluation
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_kb_manager() -> KnowledgeBaseManager:
|
|
32
|
+
"""Return a KnowledgeBaseManager rooted at the canonical project-level KB directory."""
|
|
33
|
+
base_dir = get_path_service().project_root / "data" / "knowledge_bases"
|
|
34
|
+
return KnowledgeBaseManager(base_dir=str(base_dir))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _collect_documents(docs: list[str], docs_dir: Optional[str]) -> list[str]:
|
|
38
|
+
"""Collect and de-duplicate document files from explicit paths and a directory."""
|
|
39
|
+
candidates: list[Path] = []
|
|
40
|
+
|
|
41
|
+
for doc in docs:
|
|
42
|
+
path = Path(doc).expanduser().resolve()
|
|
43
|
+
if path.exists() and path.is_file():
|
|
44
|
+
candidates.append(path)
|
|
45
|
+
|
|
46
|
+
if docs_dir:
|
|
47
|
+
base = Path(docs_dir).expanduser().resolve()
|
|
48
|
+
if not base.exists() or not base.is_dir():
|
|
49
|
+
raise typer.BadParameter(f"docs directory does not exist: {base}")
|
|
50
|
+
candidates.extend(FileTypeRouter.collect_supported_files(base, recursive=True))
|
|
51
|
+
|
|
52
|
+
unique: list[str] = []
|
|
53
|
+
seen: set[str] = set()
|
|
54
|
+
for path in candidates:
|
|
55
|
+
key = str(path)
|
|
56
|
+
if key in seen:
|
|
57
|
+
continue
|
|
58
|
+
seen.add(key)
|
|
59
|
+
unique.append(key)
|
|
60
|
+
|
|
61
|
+
return unique
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _load_eval_dataset(dataset: str) -> EvalDataset:
|
|
65
|
+
"""Load the QA set for ``kb eval``, exiting with a readable message when it is invalid."""
|
|
66
|
+
from deeptutor.services.rag.eval import EvalDatasetError, load_dataset
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
return load_dataset(dataset)
|
|
70
|
+
except EvalDatasetError as exc:
|
|
71
|
+
console.print(f"[red]{exc}[/]")
|
|
72
|
+
raise typer.Exit(code=1) from exc
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve_eval_provider(mgr: KnowledgeBaseManager, name: str) -> str:
|
|
76
|
+
"""Return the provider bound to ``name``, rejecting engines that return no ranked chunks."""
|
|
77
|
+
from deeptutor.services.rag.factory import PAGEINDEX_OSS_PROVIDER, PAGEINDEX_PROVIDER
|
|
78
|
+
from deeptutor.services.rag.provider_binding import resolve_bound_provider
|
|
79
|
+
|
|
80
|
+
provider = resolve_bound_provider(str(mgr.base_dir), name)
|
|
81
|
+
if provider in {PAGEINDEX_PROVIDER, PAGEINDEX_OSS_PROVIDER}:
|
|
82
|
+
console.print(
|
|
83
|
+
"[red]PageIndex answers by reasoning over pages inside an agent loop, so it "
|
|
84
|
+
"returns no ranked chunks to score. Evaluate a KB bound to llamaindex, "
|
|
85
|
+
"graphrag, lightrag, a LightRAG server, WeKnora or IMA instead.[/]"
|
|
86
|
+
)
|
|
87
|
+
raise typer.Exit(code=1)
|
|
88
|
+
return provider
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _build_eval_progress(top_k: int, fmt: str) -> ProgressFn:
|
|
92
|
+
"""Return a per-case progress printer that stays silent while emitting JSON."""
|
|
93
|
+
from deeptutor.services.rag.eval import format_metric
|
|
94
|
+
|
|
95
|
+
recall_key = f"recall@{top_k}"
|
|
96
|
+
|
|
97
|
+
def _report(index: int, total: int, item: QueryEvaluation) -> None:
|
|
98
|
+
if fmt == "json":
|
|
99
|
+
return
|
|
100
|
+
if item.failed:
|
|
101
|
+
console.print(f" {index}/{total} [red]failed[/] {item.case_id}: {item.error}")
|
|
102
|
+
return
|
|
103
|
+
value = format_metric(item.metrics.values()[recall_key])
|
|
104
|
+
console.print(f" {index}/{total} [green]ok[/] {item.case_id} {recall_key}={value}")
|
|
105
|
+
|
|
106
|
+
return _report
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _save_eval_report(save: str, payload: str) -> None:
|
|
110
|
+
"""Write a serialized evaluation report to ``save``, creating parent directories."""
|
|
111
|
+
target = Path(save).expanduser()
|
|
112
|
+
try:
|
|
113
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
target.write_text(payload, encoding="utf-8")
|
|
115
|
+
except OSError as exc:
|
|
116
|
+
typer.echo(f"Could not write report: {exc}", err=True)
|
|
117
|
+
raise typer.Exit(code=1) from exc
|
|
118
|
+
typer.echo(f"Report written to {target}", err=True)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _render_eval_report(report: EvalReport, name: str, provider: str) -> None:
|
|
122
|
+
"""Print the per-case metric table followed by the aggregate summary lines."""
|
|
123
|
+
columns = report.metric_columns()
|
|
124
|
+
table = Table(title=f"Retrieval evaluation - {name} ({provider})")
|
|
125
|
+
table.add_column("Case", style="bold")
|
|
126
|
+
table.add_column("Status")
|
|
127
|
+
table.add_column("Query", overflow="fold", max_width=48)
|
|
128
|
+
for column in columns:
|
|
129
|
+
table.add_column(column, justify="right")
|
|
130
|
+
for item in report.evaluations:
|
|
131
|
+
cells = report.format_metrics(item)
|
|
132
|
+
table.add_row(
|
|
133
|
+
item.case_id,
|
|
134
|
+
"[red]failed[/]" if item.failed else "[green]ok[/]",
|
|
135
|
+
item.query,
|
|
136
|
+
*(cells[column] for column in columns),
|
|
137
|
+
)
|
|
138
|
+
console.print(table)
|
|
139
|
+
for line in report.summary_lines():
|
|
140
|
+
console.print(line)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _parse_rewrite_flag(value: Optional[str]) -> Optional[bool]:
|
|
144
|
+
"""Map a ``--rewrite`` value to the three-state rewrite flag, or None to defer to settings.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
value: ``None`` (flag omitted, defer to settings), ``"off"`` or ``"standalone"``.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
``False`` for ``"off"``, ``True`` for ``"standalone"``, ``None`` when omitted.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
typer.BadParameter: If ``value`` is not a recognized strategy.
|
|
154
|
+
"""
|
|
155
|
+
if value is None:
|
|
156
|
+
return None
|
|
157
|
+
normalized = value.strip().lower()
|
|
158
|
+
if normalized == "off":
|
|
159
|
+
return False
|
|
160
|
+
if normalized == "standalone":
|
|
161
|
+
return True
|
|
162
|
+
raise typer.BadParameter(
|
|
163
|
+
f"unknown rewrite strategy '{value}'; expected 'off' or 'standalone'",
|
|
164
|
+
param_hint="--rewrite",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _load_eval_report(path: str) -> dict:
|
|
169
|
+
"""Load a saved ``kb eval --save`` report for ``kb eval-diff``, exiting readably on error.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
path: Path to a report JSON file written by ``kb eval --save``.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
The parsed report as a mapping, ready for
|
|
176
|
+
:func:`deeptutor.services.rag.eval.diff_reports`.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
typer.Exit: If the file is missing, is not valid JSON, or is not a JSON object.
|
|
180
|
+
"""
|
|
181
|
+
target = Path(path).expanduser()
|
|
182
|
+
if not target.exists():
|
|
183
|
+
console.print(f"[red]Report not found: {target}[/]")
|
|
184
|
+
raise typer.Exit(code=1)
|
|
185
|
+
try:
|
|
186
|
+
loaded = json.loads(target.read_text(encoding="utf-8"))
|
|
187
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
188
|
+
console.print(f"[red]Could not read report '{target}': {exc}[/]")
|
|
189
|
+
raise typer.Exit(code=1) from exc
|
|
190
|
+
if not isinstance(loaded, dict):
|
|
191
|
+
console.print(f"[red]Report '{target}' is not a JSON object.[/]")
|
|
192
|
+
raise typer.Exit(code=1)
|
|
193
|
+
return loaded
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def register(app: typer.Typer) -> None:
|
|
197
|
+
@app.command("list")
|
|
198
|
+
def kb_list(
|
|
199
|
+
fmt: str = typer.Option("rich", "--format", "-f", help="Output format: rich | json."),
|
|
200
|
+
) -> None:
|
|
201
|
+
"""List all knowledge bases."""
|
|
202
|
+
mgr = _get_kb_manager()
|
|
203
|
+
kb_names = mgr.list_knowledge_bases()
|
|
204
|
+
if not kb_names:
|
|
205
|
+
if fmt == "json":
|
|
206
|
+
console.print_json("[]")
|
|
207
|
+
else:
|
|
208
|
+
console.print("[dim]No knowledge bases found.[/]")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if fmt == "json":
|
|
212
|
+
items = []
|
|
213
|
+
for name in kb_names:
|
|
214
|
+
info = mgr.get_info(name)
|
|
215
|
+
stats = info.get("statistics", {})
|
|
216
|
+
metadata = info.get("metadata", {})
|
|
217
|
+
items.append(
|
|
218
|
+
{
|
|
219
|
+
"name": name,
|
|
220
|
+
"status": info.get("status", "unknown"),
|
|
221
|
+
"documents": stats.get("raw_documents", 0),
|
|
222
|
+
"rag_provider": metadata.get(
|
|
223
|
+
"rag_provider", stats.get("rag_provider", DEFAULT_PROVIDER)
|
|
224
|
+
),
|
|
225
|
+
"is_default": bool(info.get("is_default")),
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
console.print_json(json.dumps(items, ensure_ascii=False, default=str))
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
table = Table(title="Knowledge Bases")
|
|
232
|
+
table.add_column("Name", style="bold")
|
|
233
|
+
table.add_column("Status")
|
|
234
|
+
table.add_column("Documents", justify="right")
|
|
235
|
+
table.add_column("RAG Provider")
|
|
236
|
+
table.add_column("Default")
|
|
237
|
+
|
|
238
|
+
for name in kb_names:
|
|
239
|
+
info = mgr.get_info(name)
|
|
240
|
+
stats = info.get("statistics", {})
|
|
241
|
+
metadata = info.get("metadata", {})
|
|
242
|
+
table.add_row(
|
|
243
|
+
name,
|
|
244
|
+
str(info.get("status", "unknown")),
|
|
245
|
+
str(stats.get("raw_documents", 0)),
|
|
246
|
+
str(metadata.get("rag_provider", stats.get("rag_provider", DEFAULT_PROVIDER))),
|
|
247
|
+
"yes" if info.get("is_default") else "",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
console.print(table)
|
|
251
|
+
|
|
252
|
+
@app.command("info")
|
|
253
|
+
def kb_info(name: str = typer.Argument(..., help="Knowledge base name.")) -> None:
|
|
254
|
+
"""Show details of a knowledge base."""
|
|
255
|
+
mgr = _get_kb_manager()
|
|
256
|
+
try:
|
|
257
|
+
info = mgr.get_info(name)
|
|
258
|
+
except Exception as exc:
|
|
259
|
+
console.print(f"[red]Knowledge base '{name}' not found: {exc}[/]")
|
|
260
|
+
raise typer.Exit(code=1) from exc
|
|
261
|
+
console.print_json(json.dumps(info, indent=2, ensure_ascii=False, default=str))
|
|
262
|
+
|
|
263
|
+
@app.command("set-default")
|
|
264
|
+
def kb_set_default(name: str = typer.Argument(..., help="Knowledge base name.")) -> None:
|
|
265
|
+
"""Set the default knowledge base."""
|
|
266
|
+
mgr = _get_kb_manager()
|
|
267
|
+
try:
|
|
268
|
+
mgr.set_default(name)
|
|
269
|
+
except Exception as exc:
|
|
270
|
+
console.print(f"[red]Failed to set default KB '{name}': {exc}[/]")
|
|
271
|
+
raise typer.Exit(code=1) from exc
|
|
272
|
+
console.print(f"[green]Set '{name}' as default knowledge base.[/]")
|
|
273
|
+
|
|
274
|
+
@app.command("create")
|
|
275
|
+
def kb_create(
|
|
276
|
+
name: str = typer.Argument(..., help="New KB name."),
|
|
277
|
+
docs: list[str] = typer.Option([], "--doc", "-d", help="Document paths."),
|
|
278
|
+
docs_dir: Optional[str] = typer.Option(None, "--docs-dir", help="Directory of documents."),
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Initialize a new knowledge base from documents."""
|
|
281
|
+
mgr = _get_kb_manager()
|
|
282
|
+
try:
|
|
283
|
+
name = validate_knowledge_base_name(name)
|
|
284
|
+
except ValueError as exc:
|
|
285
|
+
console.print(f"[red]{exc}[/]")
|
|
286
|
+
raise typer.Exit(code=1) from exc
|
|
287
|
+
|
|
288
|
+
if name in mgr.list_knowledge_bases():
|
|
289
|
+
console.print(f"[red]Knowledge base '{name}' already exists.[/]")
|
|
290
|
+
raise typer.Exit(code=1)
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
doc_paths = _collect_documents(docs, docs_dir)
|
|
294
|
+
except typer.BadParameter as exc:
|
|
295
|
+
console.print(f"[red]{exc}[/]")
|
|
296
|
+
raise typer.Exit(code=1) from exc
|
|
297
|
+
|
|
298
|
+
if not doc_paths:
|
|
299
|
+
console.print("[red]Provide at least one supported document (--doc or --docs-dir).[/]")
|
|
300
|
+
raise typer.Exit(code=1)
|
|
301
|
+
|
|
302
|
+
console.print(
|
|
303
|
+
f"Creating KB [bold]{name}[/] with {len(doc_paths)} document(s) via [bold]LlamaIndex[/]..."
|
|
304
|
+
)
|
|
305
|
+
from deeptutor.knowledge.initializer import initialize_knowledge_base
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
asyncio.run(
|
|
309
|
+
initialize_knowledge_base(
|
|
310
|
+
kb_name=name,
|
|
311
|
+
source_files=doc_paths,
|
|
312
|
+
base_dir=str(mgr.base_dir),
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
console.print(f"[red]KB creation failed: {exc}[/]")
|
|
317
|
+
raise typer.Exit(code=1) from exc
|
|
318
|
+
console.print("[green]Knowledge base created successfully.[/]")
|
|
319
|
+
|
|
320
|
+
@app.command("connect-kiwix")
|
|
321
|
+
def kb_connect_kiwix(
|
|
322
|
+
name: str = typer.Argument(..., help="New KB name."),
|
|
323
|
+
server_url: str = typer.Option(..., "--server-url", help="kiwix-serve base URL."),
|
|
324
|
+
zim_name: str = typer.Option(..., "--zim-name", help="One ZIM name served by Kiwix."),
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Connect an existing ZIM archive without copying or indexing it."""
|
|
327
|
+
from deeptutor.services.rag.pipelines.kiwix.client import KiwixClient
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
client = KiwixClient(server_url, zim_name)
|
|
331
|
+
title = asyncio.run(client.probe())
|
|
332
|
+
_get_kb_manager().register_kiwix_kb(
|
|
333
|
+
name, client.base_url, client.zim_name, zim_title=title
|
|
334
|
+
)
|
|
335
|
+
except Exception as exc:
|
|
336
|
+
console.print(f"[red]Could not connect Kiwix archive: {exc}[/]")
|
|
337
|
+
raise typer.Exit(code=1) from exc
|
|
338
|
+
console.print(f"[green]Connected '{name}' to Kiwix archive '{title}'.[/]")
|
|
339
|
+
|
|
340
|
+
@app.command("add")
|
|
341
|
+
def kb_add(
|
|
342
|
+
name: str = typer.Argument(..., help="KB name."),
|
|
343
|
+
docs: list[str] = typer.Option([], "--doc", "-d", help="Document paths to add."),
|
|
344
|
+
docs_dir: Optional[str] = typer.Option(None, "--docs-dir", help="Directory of documents."),
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Add documents to an existing knowledge base."""
|
|
347
|
+
mgr = _get_kb_manager()
|
|
348
|
+
if name not in mgr.list_knowledge_bases():
|
|
349
|
+
console.print(f"[red]Knowledge base '{name}' not found.[/]")
|
|
350
|
+
raise typer.Exit(code=1)
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
doc_paths = _collect_documents(docs, docs_dir)
|
|
354
|
+
except typer.BadParameter as exc:
|
|
355
|
+
console.print(f"[red]{exc}[/]")
|
|
356
|
+
raise typer.Exit(code=1) from exc
|
|
357
|
+
|
|
358
|
+
if not doc_paths:
|
|
359
|
+
console.print("[red]Provide at least one supported document.[/]")
|
|
360
|
+
raise typer.Exit(code=1)
|
|
361
|
+
|
|
362
|
+
console.print(f"Adding {len(doc_paths)} document(s) to [bold]{name}[/]...")
|
|
363
|
+
from deeptutor.knowledge.add_documents import add_documents
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
processed_count = asyncio.run(
|
|
367
|
+
add_documents(
|
|
368
|
+
kb_name=name,
|
|
369
|
+
source_files=doc_paths,
|
|
370
|
+
base_dir=str(mgr.base_dir),
|
|
371
|
+
allow_duplicates=False,
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
except Exception as exc:
|
|
375
|
+
console.print(f"[red]Document upload failed: {exc}[/]")
|
|
376
|
+
raise typer.Exit(code=1) from exc
|
|
377
|
+
|
|
378
|
+
if processed_count:
|
|
379
|
+
console.print(f"[green]Done. Indexed {processed_count} document(s).[/]")
|
|
380
|
+
else:
|
|
381
|
+
console.print("[yellow]No new unique documents were indexed.[/]")
|
|
382
|
+
|
|
383
|
+
@app.command("delete")
|
|
384
|
+
def kb_delete(
|
|
385
|
+
name: str = typer.Argument(..., help="KB name."),
|
|
386
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation."),
|
|
387
|
+
) -> None:
|
|
388
|
+
"""Delete a knowledge base."""
|
|
389
|
+
if not force:
|
|
390
|
+
confirm = typer.confirm(f"Delete knowledge base '{name}'?")
|
|
391
|
+
if not confirm:
|
|
392
|
+
raise typer.Abort()
|
|
393
|
+
|
|
394
|
+
mgr = _get_kb_manager()
|
|
395
|
+
try:
|
|
396
|
+
deleted = mgr.delete_knowledge_base(name, confirm=True)
|
|
397
|
+
except Exception as exc:
|
|
398
|
+
console.print(f"[red]Failed to delete '{name}': {exc}[/]")
|
|
399
|
+
raise typer.Exit(code=1) from exc
|
|
400
|
+
|
|
401
|
+
if deleted:
|
|
402
|
+
console.print(f"[green]Deleted '{name}'.[/]")
|
|
403
|
+
else:
|
|
404
|
+
console.print(f"[yellow]Knowledge base '{name}' was not deleted.[/]")
|
|
405
|
+
|
|
406
|
+
@app.command("search")
|
|
407
|
+
def kb_search(
|
|
408
|
+
name: str = typer.Argument(..., help="KB name."),
|
|
409
|
+
query: str = typer.Argument(..., help="Search query."),
|
|
410
|
+
mode: str = typer.Option("hybrid", help="Search mode."),
|
|
411
|
+
fmt: str = typer.Option("rich", "--format", "-f", help="Output format: rich | json."),
|
|
412
|
+
) -> None:
|
|
413
|
+
"""Search a knowledge base."""
|
|
414
|
+
from deeptutor.tools.rag_tool import rag_search
|
|
415
|
+
|
|
416
|
+
mgr = _get_kb_manager()
|
|
417
|
+
if name not in mgr.list_knowledge_bases():
|
|
418
|
+
console.print(f"[red]Knowledge base '{name}' not found.[/]")
|
|
419
|
+
raise typer.Exit(code=1)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
from deeptutor.services.rag.factory import PAGEINDEX_OSS_PROVIDER, PAGEINDEX_PROVIDER
|
|
423
|
+
from deeptutor.services.rag.provider_binding import resolve_bound_provider
|
|
424
|
+
|
|
425
|
+
provider = resolve_bound_provider(str(mgr.base_dir), name)
|
|
426
|
+
if provider in {PAGEINDEX_PROVIDER, PAGEINDEX_OSS_PROVIDER}:
|
|
427
|
+
from deeptutor.services.rag.pipelines.pageindex.reasoning import (
|
|
428
|
+
read_pageindex_with_agent,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
reading = asyncio.run(
|
|
432
|
+
read_pageindex_with_agent(
|
|
433
|
+
kb_name=name,
|
|
434
|
+
system_prompt=(
|
|
435
|
+
"Answer the CLI user's question from the selected PageIndex "
|
|
436
|
+
"knowledge base. Include page references from tool results."
|
|
437
|
+
),
|
|
438
|
+
user_prompt=query,
|
|
439
|
+
source="cli_kb_search",
|
|
440
|
+
stage="search",
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
result = {
|
|
444
|
+
"query": query,
|
|
445
|
+
"answer": reading.text,
|
|
446
|
+
"content": reading.text,
|
|
447
|
+
"sources": reading.sources,
|
|
448
|
+
"provider": reading.tool_context.provider,
|
|
449
|
+
}
|
|
450
|
+
else:
|
|
451
|
+
result = asyncio.run(
|
|
452
|
+
rag_search(
|
|
453
|
+
query=query,
|
|
454
|
+
kb_name=name,
|
|
455
|
+
mode=mode,
|
|
456
|
+
kb_base_dir=str(mgr.base_dir),
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
except Exception as exc:
|
|
460
|
+
console.print(f"[red]Search failed: {exc}[/]")
|
|
461
|
+
raise typer.Exit(code=1) from exc
|
|
462
|
+
|
|
463
|
+
if fmt == "json":
|
|
464
|
+
console.print_json(json.dumps(result, indent=2, ensure_ascii=False, default=str))
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
answer = result.get("answer") or result.get("content", "")
|
|
468
|
+
provider = str(result.get("provider", DEFAULT_PROVIDER))
|
|
469
|
+
console.print(f"[bold]Provider:[/] {provider}")
|
|
470
|
+
console.print(f"[bold]Answer:[/]\n{answer}")
|
|
471
|
+
|
|
472
|
+
@app.command("eval")
|
|
473
|
+
def kb_eval(
|
|
474
|
+
name: str = typer.Argument(..., help="KB name."),
|
|
475
|
+
dataset: str = typer.Option(..., "--dataset", "-d", help="QA set file (.jsonl or .json)."),
|
|
476
|
+
top_k: int = typer.Option(5, "--top-k", "-k", help="Rank cutoff for the metrics."),
|
|
477
|
+
mode: Optional[str] = typer.Option(
|
|
478
|
+
None, "--mode", help="Search mode for engines that support one (e.g. hybrid)."
|
|
479
|
+
),
|
|
480
|
+
limit: Optional[int] = typer.Option(
|
|
481
|
+
None, "--limit", help="Evaluate only the first N cases of the set."
|
|
482
|
+
),
|
|
483
|
+
min_ratio: float = typer.Option(
|
|
484
|
+
0.5, "--min-ratio", help="Token coverage a retrieved chunk needs to count as a match."
|
|
485
|
+
),
|
|
486
|
+
save: Optional[str] = typer.Option(None, "--save", help="Write the JSON report to a file."),
|
|
487
|
+
fmt: str = typer.Option("rich", "--format", "-f", help="Output format: rich | json."),
|
|
488
|
+
rewrite: Optional[str] = typer.Option(
|
|
489
|
+
None,
|
|
490
|
+
"--rewrite",
|
|
491
|
+
help="Force the query-rewrite layer: off | standalone. Omit to defer to settings.",
|
|
492
|
+
),
|
|
493
|
+
) -> None:
|
|
494
|
+
"""Score a knowledge base's retrieval quality against a QA set."""
|
|
495
|
+
from deeptutor.services.rag.eval import MatchPolicy, RetrievalEvaluator
|
|
496
|
+
|
|
497
|
+
rewrite_flag = _parse_rewrite_flag(rewrite)
|
|
498
|
+
|
|
499
|
+
mgr = _get_kb_manager()
|
|
500
|
+
if name not in mgr.list_knowledge_bases():
|
|
501
|
+
console.print(f"[red]Knowledge base '{name}' not found.[/]")
|
|
502
|
+
raise typer.Exit(code=1)
|
|
503
|
+
|
|
504
|
+
eval_set = _load_eval_dataset(dataset)
|
|
505
|
+
provider = _resolve_eval_provider(mgr, name)
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
evaluator = RetrievalEvaluator(
|
|
509
|
+
kb_name=name,
|
|
510
|
+
kb_base_dir=str(mgr.base_dir),
|
|
511
|
+
k=top_k,
|
|
512
|
+
mode=mode,
|
|
513
|
+
query_limit=limit,
|
|
514
|
+
policy=MatchPolicy(min_ratio=min_ratio),
|
|
515
|
+
provider=provider,
|
|
516
|
+
rewrite=rewrite_flag,
|
|
517
|
+
)
|
|
518
|
+
except ValueError as exc:
|
|
519
|
+
console.print(f"[red]{exc}[/]")
|
|
520
|
+
raise typer.Exit(code=1) from exc
|
|
521
|
+
|
|
522
|
+
cases = eval_set.limited(limit)
|
|
523
|
+
if fmt != "json":
|
|
524
|
+
suffix = ""
|
|
525
|
+
if rewrite_flag is True:
|
|
526
|
+
suffix = ", rewrite=standalone"
|
|
527
|
+
elif rewrite_flag is False:
|
|
528
|
+
suffix = ", rewrite=off"
|
|
529
|
+
console.print(
|
|
530
|
+
f"[bold]Evaluating[/] '{name}' ({provider}) on {len(cases)} case(s), "
|
|
531
|
+
f"k={top_k}{suffix} ..."
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
progress = _build_eval_progress(top_k, fmt)
|
|
535
|
+
try:
|
|
536
|
+
report = asyncio.run(evaluator.evaluate(eval_set, progress=progress))
|
|
537
|
+
except Exception as exc:
|
|
538
|
+
console.print(f"[red]Evaluation failed: {exc}[/]")
|
|
539
|
+
raise typer.Exit(code=1) from exc
|
|
540
|
+
|
|
541
|
+
payload = report.to_json()
|
|
542
|
+
if save:
|
|
543
|
+
_save_eval_report(save, payload)
|
|
544
|
+
|
|
545
|
+
if fmt == "json":
|
|
546
|
+
console.print_json(payload)
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
_render_eval_report(report, name, provider)
|
|
550
|
+
|
|
551
|
+
@app.command("eval-diff")
|
|
552
|
+
def kb_eval_diff(
|
|
553
|
+
baseline: str = typer.Argument(..., help="Baseline report JSON (from `kb eval --save`)."),
|
|
554
|
+
candidate: str = typer.Argument(..., help="Candidate report JSON (from `kb eval --save`)."),
|
|
555
|
+
fmt: str = typer.Option("rich", "--format", "-f", help="Output format: rich | json."),
|
|
556
|
+
) -> None:
|
|
557
|
+
"""Diff two `kb eval` reports to quantify a retrieval change.
|
|
558
|
+
|
|
559
|
+
Records a baseline before a settings change (e.g. turning the query-rewrite
|
|
560
|
+
layer on), a candidate after, and shows the per-metric and per-case movement.
|
|
561
|
+
"""
|
|
562
|
+
from deeptutor.services.rag.eval.diff import diff_reports
|
|
563
|
+
|
|
564
|
+
base_report = _load_eval_report(baseline)
|
|
565
|
+
cand_report = _load_eval_report(candidate)
|
|
566
|
+
|
|
567
|
+
try:
|
|
568
|
+
diff = diff_reports(
|
|
569
|
+
base_report,
|
|
570
|
+
cand_report,
|
|
571
|
+
baseline_label=Path(baseline).stem,
|
|
572
|
+
candidate_label=Path(candidate).stem,
|
|
573
|
+
)
|
|
574
|
+
except ValueError as exc:
|
|
575
|
+
console.print(f"[red]{exc}[/]")
|
|
576
|
+
raise typer.Exit(code=1) from exc
|
|
577
|
+
|
|
578
|
+
if fmt == "json":
|
|
579
|
+
console.print_json(data=diff.to_dict())
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
for line in diff.summary_lines():
|
|
583
|
+
console.print(line)
|
|
584
|
+
|
|
585
|
+
if diff.has_regression:
|
|
586
|
+
raise typer.Exit(code=2)
|
|
587
|
+
|
|
588
|
+
# ── GitHub source commands ──────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
@app.command("add-github-source")
|
|
591
|
+
def kb_add_github_source(
|
|
592
|
+
name: str = typer.Argument(..., help="KB name."),
|
|
593
|
+
repo: str = typer.Option(..., "--repo", "-r", help="GitHub repo as owner/name or URL."),
|
|
594
|
+
branch: str = typer.Option("main", "--branch", "-b", help="Branch to track."),
|
|
595
|
+
path: str = typer.Option("", "--path", "-p", help="Subdirectory prefix (e.g. docs/)."),
|
|
596
|
+
glob: str = typer.Option("*.md", "--glob", "-g", help="File glob pattern."),
|
|
597
|
+
) -> None:
|
|
598
|
+
"""Add a GitHub repo as a Markdown document source for a KB."""
|
|
599
|
+
mgr = _get_kb_manager()
|
|
600
|
+
if name not in mgr.list_knowledge_bases():
|
|
601
|
+
console.print(f"[red]Knowledge base '{name}' not found.[/]")
|
|
602
|
+
raise typer.Exit(code=1)
|
|
603
|
+
try:
|
|
604
|
+
info = mgr.add_github_source(name, repo, branch, path, glob)
|
|
605
|
+
except Exception as exc:
|
|
606
|
+
console.print(f"[red]Failed: {exc}[/]")
|
|
607
|
+
raise typer.Exit(code=1) from exc
|
|
608
|
+
console.print("[green]GitHub source added:[/]")
|
|
609
|
+
console.print_json(json.dumps(info, ensure_ascii=False))
|
|
610
|
+
|
|
611
|
+
@app.command("remove-github-source")
|
|
612
|
+
def kb_remove_github_source(
|
|
613
|
+
name: str = typer.Argument(..., help="KB name."),
|
|
614
|
+
source_id: str = typer.Option(..., "--source-id", "-s", help="Source ID to remove."),
|
|
615
|
+
) -> None:
|
|
616
|
+
"""Remove a GitHub source from a KB."""
|
|
617
|
+
mgr = _get_kb_manager()
|
|
618
|
+
ok = mgr.remove_github_source(name, source_id)
|
|
619
|
+
if ok:
|
|
620
|
+
console.print(f"[green]Removed '{source_id}'.[/]")
|
|
621
|
+
else:
|
|
622
|
+
console.print(f"[yellow]Source '{source_id}' not found.[/]")
|
|
623
|
+
|
|
624
|
+
@app.command("add-web-source")
|
|
625
|
+
def kb_add_web_source(
|
|
626
|
+
name: str = typer.Argument(..., help="KB name."),
|
|
627
|
+
url: str = typer.Option(..., "--url", "-u", help="Documentation site base URL."),
|
|
628
|
+
max_depth: int = typer.Option(3, "--max-depth", help="Crawl depth."),
|
|
629
|
+
max_pages: int = typer.Option(200, "--max-pages", help="Max pages to crawl."),
|
|
630
|
+
) -> None:
|
|
631
|
+
"""Add a documentation site URL as a source for a KB."""
|
|
632
|
+
mgr = _get_kb_manager()
|
|
633
|
+
if name not in mgr.list_knowledge_bases():
|
|
634
|
+
console.print(f"[red]Knowledge base '{name}' not found.[/]")
|
|
635
|
+
raise typer.Exit(code=1)
|
|
636
|
+
try:
|
|
637
|
+
info = mgr.add_web_source(name, url, max_depth, max_pages)
|
|
638
|
+
except Exception as exc:
|
|
639
|
+
console.print(f"[red]Failed: {exc}[/]")
|
|
640
|
+
raise typer.Exit(code=1) from exc
|
|
641
|
+
console.print("[green]Web source added:[/]")
|
|
642
|
+
console.print_json(json.dumps(info, ensure_ascii=False))
|
|
643
|
+
|
|
644
|
+
@app.command("remove-web-source")
|
|
645
|
+
def kb_remove_web_source(
|
|
646
|
+
name: str = typer.Argument(..., help="KB name."),
|
|
647
|
+
source_id: str = typer.Option(..., "--source-id", "-s", help="Source ID to remove."),
|
|
648
|
+
) -> None:
|
|
649
|
+
"""Remove a web source from a KB."""
|
|
650
|
+
mgr = _get_kb_manager()
|
|
651
|
+
removed = mgr.remove_web_source(name, source_id)
|
|
652
|
+
if removed:
|
|
653
|
+
console.print(f"[green]Removed '{source_id}'.[/]")
|
|
654
|
+
else:
|
|
655
|
+
console.print(f"[yellow]Source '{source_id}' not found.[/]")
|
|
656
|
+
|
|
657
|
+
@app.command("list-sources")
|
|
658
|
+
def kb_list_sources(
|
|
659
|
+
name: str = typer.Argument(..., help="KB name."),
|
|
660
|
+
) -> None:
|
|
661
|
+
"""List GitHub and web sources for a KB."""
|
|
662
|
+
mgr = _get_kb_manager()
|
|
663
|
+
if name not in mgr.list_knowledge_bases():
|
|
664
|
+
console.print(f"[red]Knowledge base '{name}' not found.[/]")
|
|
665
|
+
raise typer.Exit(code=1)
|
|
666
|
+
|
|
667
|
+
gh_sources = mgr.get_github_sources(name)
|
|
668
|
+
web_sources = mgr.get_web_sources(name)
|
|
669
|
+
|
|
670
|
+
if not gh_sources and not web_sources:
|
|
671
|
+
console.print("[dim]No sources for this KB.[/]")
|
|
672
|
+
return
|
|
673
|
+
|
|
674
|
+
if gh_sources:
|
|
675
|
+
table = Table(title=f"GitHub Sources - {name}")
|
|
676
|
+
table.add_column("ID", style="dim")
|
|
677
|
+
table.add_column("Repo")
|
|
678
|
+
table.add_column("Branch")
|
|
679
|
+
table.add_column("Status")
|
|
680
|
+
table.add_column("Files", justify="right")
|
|
681
|
+
for source in gh_sources:
|
|
682
|
+
table.add_row(
|
|
683
|
+
source.get("id", ""),
|
|
684
|
+
source.get("repo", ""),
|
|
685
|
+
source.get("branch", ""),
|
|
686
|
+
source.get("last_sync_status", "pending"),
|
|
687
|
+
str(source.get("files_synced", 0)),
|
|
688
|
+
)
|
|
689
|
+
console.print(table)
|
|
690
|
+
|
|
691
|
+
if web_sources:
|
|
692
|
+
table = Table(title=f"Web Sources - {name}")
|
|
693
|
+
table.add_column("ID", style="dim")
|
|
694
|
+
table.add_column("URL")
|
|
695
|
+
table.add_column("Status")
|
|
696
|
+
table.add_column("Pages", justify="right")
|
|
697
|
+
for source in web_sources:
|
|
698
|
+
table.add_row(
|
|
699
|
+
source.get("id", ""),
|
|
700
|
+
source.get("url", ""),
|
|
701
|
+
source.get("last_sync_status", "pending"),
|
|
702
|
+
str(source.get("page_count", 0)),
|
|
703
|
+
)
|
|
704
|
+
console.print(table)
|
|
705
|
+
|
|
706
|
+
@app.command("sync")
|
|
707
|
+
def kb_sync(name: str = typer.Argument(..., help="KB name.")) -> None:
|
|
708
|
+
"""Immediately sync all GitHub and web sources for a KB."""
|
|
709
|
+
mgr = _get_kb_manager()
|
|
710
|
+
if name not in mgr.list_knowledge_bases():
|
|
711
|
+
console.print(f"[red]Knowledge base '{name}' not found.[/]")
|
|
712
|
+
raise typer.Exit(code=1)
|
|
713
|
+
|
|
714
|
+
gh_sources = mgr.get_github_sources(name)
|
|
715
|
+
web_sources = mgr.get_web_sources(name)
|
|
716
|
+
if not gh_sources and not web_sources:
|
|
717
|
+
console.print("[yellow]No sources to sync.[/]")
|
|
718
|
+
raise typer.Exit(code=0)
|
|
719
|
+
|
|
720
|
+
from deeptutor.services.github_source.sync import sync_source as gh_sync
|
|
721
|
+
from deeptutor.services.web_source.sync import sync_source as web_sync
|
|
722
|
+
|
|
723
|
+
for src in gh_sources:
|
|
724
|
+
if not src.get("enabled", True):
|
|
725
|
+
continue
|
|
726
|
+
console.print(f"Syncing GitHub [bold]{src.get('repo')}[/] ...")
|
|
727
|
+
try:
|
|
728
|
+
r = asyncio.run(gh_sync(kb_name=name, source=src, base_dir=str(mgr.base_dir)))
|
|
729
|
+
except Exception as exc:
|
|
730
|
+
console.print(f" [red]Error: {exc}[/]")
|
|
731
|
+
continue
|
|
732
|
+
if r.skipped:
|
|
733
|
+
console.print(" [dim]Up to date.[/]")
|
|
734
|
+
elif r.ok:
|
|
735
|
+
console.print(f" [green]+{r.files_added} ~{r.files_updated} -{r.files_removed}[/]")
|
|
736
|
+
else:
|
|
737
|
+
console.print(f" [red]Failed: {r.error}[/]")
|
|
738
|
+
|
|
739
|
+
for source in web_sources:
|
|
740
|
+
if not source.get("enabled", True):
|
|
741
|
+
continue
|
|
742
|
+
console.print(f"Crawling [bold]{source.get('url')}[/] ...")
|
|
743
|
+
try:
|
|
744
|
+
result = asyncio.run(
|
|
745
|
+
web_sync(kb_name=name, source=source, base_dir=str(mgr.base_dir))
|
|
746
|
+
)
|
|
747
|
+
except Exception as exc:
|
|
748
|
+
console.print(f" [red]Error: {exc}[/]")
|
|
749
|
+
continue
|
|
750
|
+
if result.ok:
|
|
751
|
+
console.print(
|
|
752
|
+
f" [green]+{result.pages_added} ~{result.pages_updated} "
|
|
753
|
+
f"-{result.pages_removed} ({result.pages_unchanged} unchanged)[/]"
|
|
754
|
+
)
|
|
755
|
+
else:
|
|
756
|
+
console.print(f" [red]Failed: {result.error}[/]")
|