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.
Files changed (168) hide show
  1. deeptutor/api/routers/knowledge.py +5225 -0
  2. deeptutor/api/routers/marginnote4.py +278 -0
  3. deeptutor/capabilities/ima/__init__.py +6 -0
  4. deeptutor/capabilities/ima/binding.py +118 -0
  5. deeptutor/capabilities/ima/capability.py +92 -0
  6. deeptutor/capabilities/ima/prompts/en/system.md +33 -0
  7. deeptutor/capabilities/ima/prompts/zh/system.md +24 -0
  8. deeptutor/capabilities/ima/tools.py +476 -0
  9. deeptutor/capabilities/marginnote4/__init__.py +13 -0
  10. deeptutor/capabilities/marginnote4/binding.py +66 -0
  11. deeptutor/capabilities/marginnote4/capability.py +80 -0
  12. deeptutor/capabilities/marginnote4/models.py +173 -0
  13. deeptutor/capabilities/marginnote4/prompts/en/system.md +54 -0
  14. deeptutor/capabilities/marginnote4/prompts/zh/system.md +44 -0
  15. deeptutor/capabilities/marginnote4/store.py +562 -0
  16. deeptutor/capabilities/marginnote4/tools.py +329 -0
  17. deeptutor/capabilities/obsidian/__init__.py +6 -0
  18. deeptutor/capabilities/obsidian/binding.py +75 -0
  19. deeptutor/capabilities/obsidian/capability.py +82 -0
  20. deeptutor/capabilities/obsidian/prompts/en/system.md +32 -0
  21. deeptutor/capabilities/obsidian/prompts/zh/system.md +28 -0
  22. deeptutor/capabilities/obsidian/tools.py +399 -0
  23. deeptutor/capabilities/obsidian/vault.py +341 -0
  24. deeptutor/knowledge/__init__.py +28 -0
  25. deeptutor/knowledge/add_documents.py +734 -0
  26. deeptutor/knowledge/indexing_run.py +432 -0
  27. deeptutor/knowledge/initializer.py +448 -0
  28. deeptutor/knowledge/manager.py +2570 -0
  29. deeptutor/knowledge/module_service.py +29 -0
  30. deeptutor/knowledge/naming.py +40 -0
  31. deeptutor/knowledge/progress_events.py +45 -0
  32. deeptutor/knowledge/progress_tracker.py +355 -0
  33. deeptutor/knowledge/staging.py +69 -0
  34. deeptutor/services/config/knowledge_base_config.py +239 -0
  35. deeptutor/services/embedding/__init__.py +46 -0
  36. deeptutor/services/embedding/adapters/__init__.py +40 -0
  37. deeptutor/services/embedding/adapters/base.py +196 -0
  38. deeptutor/services/embedding/adapters/cohere.py +176 -0
  39. deeptutor/services/embedding/adapters/dashscope_native.py +242 -0
  40. deeptutor/services/embedding/adapters/gemini.py +462 -0
  41. deeptutor/services/embedding/adapters/jina.py +161 -0
  42. deeptutor/services/embedding/adapters/ollama.py +145 -0
  43. deeptutor/services/embedding/adapters/openai_compatible.py +406 -0
  44. deeptutor/services/embedding/adapters/openai_sdk.py +150 -0
  45. deeptutor/services/embedding/client.py +323 -0
  46. deeptutor/services/embedding/config.py +114 -0
  47. deeptutor/services/embedding/request_options.py +46 -0
  48. deeptutor/services/embedding/validation.py +128 -0
  49. deeptutor/services/github_source/__init__.py +5 -0
  50. deeptutor/services/github_source/client.py +164 -0
  51. deeptutor/services/github_source/sync.py +384 -0
  52. deeptutor/services/github_source/sync_service.py +72 -0
  53. deeptutor/services/rag/__init__.py +21 -0
  54. deeptutor/services/rag/embedding_binding.py +331 -0
  55. deeptutor/services/rag/embedding_signature.py +63 -0
  56. deeptutor/services/rag/eval/__init__.py +92 -0
  57. deeptutor/services/rag/eval/dataset.py +244 -0
  58. deeptutor/services/rag/eval/diff.py +312 -0
  59. deeptutor/services/rag/eval/matching.py +218 -0
  60. deeptutor/services/rag/eval/metrics.py +213 -0
  61. deeptutor/services/rag/eval/report.py +111 -0
  62. deeptutor/services/rag/eval/runner.py +309 -0
  63. deeptutor/services/rag/factory.py +369 -0
  64. deeptutor/services/rag/file_routing.py +5 -0
  65. deeptutor/services/rag/index_probe.py +327 -0
  66. deeptutor/services/rag/index_versioning.py +387 -0
  67. deeptutor/services/rag/kb_paths.py +50 -0
  68. deeptutor/services/rag/linked_kb.py +237 -0
  69. deeptutor/services/rag/pipelines/__init__.py +7 -0
  70. deeptutor/services/rag/pipelines/base.py +41 -0
  71. deeptutor/services/rag/pipelines/graphrag/__init__.py +18 -0
  72. deeptutor/services/rag/pipelines/graphrag/compatibility.py +42 -0
  73. deeptutor/services/rag/pipelines/graphrag/completion_adapter.py +251 -0
  74. deeptutor/services/rag/pipelines/graphrag/config.py +357 -0
  75. deeptutor/services/rag/pipelines/graphrag/engine.py +479 -0
  76. deeptutor/services/rag/pipelines/graphrag/errors.py +328 -0
  77. deeptutor/services/rag/pipelines/graphrag/ingestion.py +135 -0
  78. deeptutor/services/rag/pipelines/graphrag/pandas_compat.py +68 -0
  79. deeptutor/services/rag/pipelines/graphrag/pipeline.py +284 -0
  80. deeptutor/services/rag/pipelines/graphrag/provider.py +134 -0
  81. deeptutor/services/rag/pipelines/graphrag/storage.py +127 -0
  82. deeptutor/services/rag/pipelines/ima/__init__.py +28 -0
  83. deeptutor/services/rag/pipelines/ima/client.py +364 -0
  84. deeptutor/services/rag/pipelines/ima/config.py +140 -0
  85. deeptutor/services/rag/pipelines/ima/envelope.py +109 -0
  86. deeptutor/services/rag/pipelines/ima/inventory.py +209 -0
  87. deeptutor/services/rag/pipelines/ima/media.py +190 -0
  88. deeptutor/services/rag/pipelines/ima/models.py +348 -0
  89. deeptutor/services/rag/pipelines/ima/notes.py +240 -0
  90. deeptutor/services/rag/pipelines/ima/pipeline.py +238 -0
  91. deeptutor/services/rag/pipelines/ima/probe.py +105 -0
  92. deeptutor/services/rag/pipelines/ima/sources.py +105 -0
  93. deeptutor/services/rag/pipelines/ima/transport.py +130 -0
  94. deeptutor/services/rag/pipelines/kiwix/__init__.py +1 -0
  95. deeptutor/services/rag/pipelines/kiwix/client.py +369 -0
  96. deeptutor/services/rag/pipelines/kiwix/pipeline.py +75 -0
  97. deeptutor/services/rag/pipelines/lightrag/README.md +78 -0
  98. deeptutor/services/rag/pipelines/lightrag/__init__.py +15 -0
  99. deeptutor/services/rag/pipelines/lightrag/block_policy.py +266 -0
  100. deeptutor/services/rag/pipelines/lightrag/cache_reuse.py +137 -0
  101. deeptutor/services/rag/pipelines/lightrag/config.py +453 -0
  102. deeptutor/services/rag/pipelines/lightrag/engine.py +536 -0
  103. deeptutor/services/rag/pipelines/lightrag/indexing_policy.py +696 -0
  104. deeptutor/services/rag/pipelines/lightrag/ingress.py +364 -0
  105. deeptutor/services/rag/pipelines/lightrag/parser.py +99 -0
  106. deeptutor/services/rag/pipelines/lightrag/pipeline.py +692 -0
  107. deeptutor/services/rag/pipelines/lightrag/roles.py +132 -0
  108. deeptutor/services/rag/pipelines/lightrag/sidecar.py +320 -0
  109. deeptutor/services/rag/pipelines/lightrag/storage.py +338 -0
  110. deeptutor/services/rag/pipelines/lightrag/worker.py +259 -0
  111. deeptutor/services/rag/pipelines/lightrag/write_lock.py +55 -0
  112. deeptutor/services/rag/pipelines/lightrag_server/__init__.py +15 -0
  113. deeptutor/services/rag/pipelines/lightrag_server/client.py +149 -0
  114. deeptutor/services/rag/pipelines/lightrag_server/config.py +68 -0
  115. deeptutor/services/rag/pipelines/lightrag_server/pipeline.py +120 -0
  116. deeptutor/services/rag/pipelines/lightrag_server/probe.py +110 -0
  117. deeptutor/services/rag/pipelines/llamaindex/__init__.py +3 -0
  118. deeptutor/services/rag/pipelines/llamaindex/config.py +199 -0
  119. deeptutor/services/rag/pipelines/llamaindex/document_loader.py +648 -0
  120. deeptutor/services/rag/pipelines/llamaindex/embedding_adapter.py +264 -0
  121. deeptutor/services/rag/pipelines/llamaindex/errors.py +76 -0
  122. deeptutor/services/rag/pipelines/llamaindex/exercise_lookup.py +373 -0
  123. deeptutor/services/rag/pipelines/llamaindex/ingestion.py +129 -0
  124. deeptutor/services/rag/pipelines/llamaindex/pipeline.py +556 -0
  125. deeptutor/services/rag/pipelines/llamaindex/rerank.py +130 -0
  126. deeptutor/services/rag/pipelines/llamaindex/retrievers.py +360 -0
  127. deeptutor/services/rag/pipelines/llamaindex/storage.py +655 -0
  128. deeptutor/services/rag/pipelines/llamaindex/vector_store.py +288 -0
  129. deeptutor/services/rag/pipelines/modes.py +48 -0
  130. deeptutor/services/rag/pipelines/pageindex/__init__.py +47 -0
  131. deeptutor/services/rag/pipelines/pageindex/client.py +146 -0
  132. deeptutor/services/rag/pipelines/pageindex/config.py +49 -0
  133. deeptutor/services/rag/pipelines/pageindex/pipeline.py +313 -0
  134. deeptutor/services/rag/pipelines/pageindex/reasoning.py +262 -0
  135. deeptutor/services/rag/pipelines/pageindex/storage.py +133 -0
  136. deeptutor/services/rag/pipelines/pageindex/tools.py +221 -0
  137. deeptutor/services/rag/pipelines/weknora/__init__.py +13 -0
  138. deeptutor/services/rag/pipelines/weknora/client.py +106 -0
  139. deeptutor/services/rag/pipelines/weknora/config.py +56 -0
  140. deeptutor/services/rag/pipelines/weknora/pipeline.py +74 -0
  141. deeptutor/services/rag/pipelines/weknora/probe.py +80 -0
  142. deeptutor/services/rag/preflight.py +274 -0
  143. deeptutor/services/rag/provider_binding.py +70 -0
  144. deeptutor/services/rag/query_rewrite.py +186 -0
  145. deeptutor/services/rag/service.py +379 -0
  146. deeptutor/services/rag/smart_retriever.py +81 -0
  147. deeptutor/services/rag/source_pages.py +292 -0
  148. deeptutor/services/rag/source_visuals.py +434 -0
  149. deeptutor/services/rag/visual_assets.py +430 -0
  150. deeptutor/services/rag/visual_coverage.py +165 -0
  151. deeptutor/services/web_source/__init__.py +1 -0
  152. deeptutor/services/web_source/bilingual.py +255 -0
  153. deeptutor/services/web_source/crawler.py +681 -0
  154. deeptutor/services/web_source/html_extractor.py +643 -0
  155. deeptutor/services/web_source/markdown.py +28 -0
  156. deeptutor/services/web_source/navigation.py +72 -0
  157. deeptutor/services/web_source/repository.py +640 -0
  158. deeptutor/services/web_source/robots.py +182 -0
  159. deeptutor/services/web_source/scheduler.py +336 -0
  160. deeptutor/services/web_source/snapshot_assets.py +171 -0
  161. deeptutor/services/web_source/sync.py +253 -0
  162. deeptutor/services/workspace/kb_move.py +446 -0
  163. deeptutor/tools/rag_tool.py +110 -0
  164. deeptutor_cli/kb.py +756 -0
  165. deeptutor_knowledge-1.7.0.dist-info/METADATA +15 -0
  166. deeptutor_knowledge-1.7.0.dist-info/RECORD +168 -0
  167. deeptutor_knowledge-1.7.0.dist-info/WHEEL +5 -0
  168. deeptutor_knowledge-1.7.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,149 @@
1
+ """Thin async HTTP client for an external LightRAG server's REST API.
2
+
3
+ We talk to the documented endpoints directly (``httpx`` only) — the calls map
4
+ 1:1 onto our retrieval-only contract:
5
+
6
+ * ``POST /query`` with ``only_need_context=True`` — return the grounded context
7
+ the server retrieved, WITHOUT its own generation. DeepTutor's chat loop keeps
8
+ ownership of the answer; the server is used purely as a retriever.
9
+ * ``GET /auth-status`` — reachability + whether the server requires an API key
10
+ (whitelisted on the server, so it answers without credentials).
11
+ * ``GET /documents/pipeline_status`` — an auth-gated, side-effect-free call used
12
+ only to validate that a configured API key is accepted.
13
+
14
+ Mirrors :class:`PageIndexClient`: a fresh :class:`httpx.AsyncClient` per call so
15
+ the object is safe to construct once and reuse, and an injectable ``transport``
16
+ so tests can stub the wire without a live server.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from typing import Any, Optional
23
+
24
+ import httpx
25
+
26
+ from .config import LightRagServerConfig
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class LightRagServerAPIError(RuntimeError):
32
+ """Raised when the LightRAG server returns an error or unexpected payload."""
33
+
34
+
35
+ class LightRagServerClient:
36
+ """Stateless wrapper over an external LightRAG server's REST API."""
37
+
38
+ def __init__(
39
+ self,
40
+ config: LightRagServerConfig,
41
+ *,
42
+ timeout: float = 60.0,
43
+ transport: Optional[httpx.AsyncBaseTransport] = None,
44
+ ) -> None:
45
+ self._config = config
46
+ self._timeout = timeout
47
+ self._transport = transport
48
+
49
+ def _open(self) -> httpx.AsyncClient:
50
+ headers = {"Accept": "application/json"}
51
+ if self._config.api_key:
52
+ # LightRAG server authenticates with an ``X-API-Key`` header
53
+ # (its ``LIGHTRAG_API_KEY``); absent when the server runs open.
54
+ headers["X-API-Key"] = self._config.api_key
55
+ return httpx.AsyncClient(
56
+ base_url=self._config.base_url,
57
+ headers=headers,
58
+ timeout=self._timeout,
59
+ transport=self._transport,
60
+ )
61
+
62
+ @staticmethod
63
+ def _json(resp: httpx.Response) -> dict[str, Any]:
64
+ if resp.status_code >= 400:
65
+ raise LightRagServerAPIError(
66
+ f"LightRAG server returned {resp.status_code}: {resp.text[:300]}"
67
+ )
68
+ try:
69
+ data = resp.json()
70
+ except Exception as exc: # pragma: no cover - defensive
71
+ raise LightRagServerAPIError(
72
+ f"LightRAG server returned a non-JSON response: {exc}"
73
+ ) from exc
74
+ if not isinstance(data, dict):
75
+ raise LightRagServerAPIError(f"LightRAG server returned unexpected payload: {data!r}")
76
+ return data
77
+
78
+ # ----- retrieval ------------------------------------------------------
79
+
80
+ async def query_context(self, query: str, mode: str) -> dict[str, Any]:
81
+ """Retrieve grounded context for ``query`` without server-side generation.
82
+
83
+ Returns ``{"content": <context string>, "sources": [...]}``. ``sources``
84
+ are derived from the server's ``references`` list when present (one entry
85
+ per cited source file); an older server that omits references yields an
86
+ empty list rather than an error.
87
+ """
88
+ async with self._open() as client:
89
+ resp = await client.post(
90
+ "/query",
91
+ json={"query": query, "mode": mode, "only_need_context": True},
92
+ )
93
+ data = self._json(resp)
94
+ content = str(data.get("response") or "")
95
+ sources = _sources_from_references(data.get("references"))
96
+ return {"content": content, "sources": sources}
97
+
98
+ # ----- probing --------------------------------------------------------
99
+
100
+ async def auth_status(self) -> dict[str, Any]:
101
+ """Fetch ``/auth-status`` (no credentials) to probe reachability.
102
+
103
+ The presence of LightRAG-specific keys (``auth_configured`` /
104
+ ``core_version``) doubles as a "this really is a LightRAG server" signal.
105
+ """
106
+ async with self._open() as client:
107
+ resp = await client.get("/auth-status")
108
+ return self._json(resp)
109
+
110
+ async def verify_key(self) -> bool:
111
+ """Return whether the configured API key is accepted by the server.
112
+
113
+ Hits the auth-gated, read-only ``/documents/pipeline_status``: a 2xx
114
+ means the key (or open access) is valid; 401/403 means it was rejected.
115
+ Any other transport error propagates to the caller.
116
+ """
117
+ async with self._open() as client:
118
+ resp = await client.get("/documents/pipeline_status")
119
+ if resp.status_code in (401, 403):
120
+ return False
121
+ if resp.status_code >= 400:
122
+ raise LightRagServerAPIError(
123
+ f"LightRAG server returned {resp.status_code}: {resp.text[:300]}"
124
+ )
125
+ return True
126
+
127
+
128
+ def _sources_from_references(references: Any) -> list[dict[str, Any]]:
129
+ """Map a LightRAG ``references`` list into DeepTutor's ``sources`` shape."""
130
+ if not isinstance(references, list):
131
+ return []
132
+ sources: list[dict[str, Any]] = []
133
+ for ref in references:
134
+ if not isinstance(ref, dict):
135
+ continue
136
+ file_path = str(ref.get("file_path") or "").strip()
137
+ ref_id = str(ref.get("reference_id") or "").strip()
138
+ if not file_path and not ref_id:
139
+ continue
140
+ source: dict[str, Any] = {}
141
+ if ref_id:
142
+ source["id"] = ref_id
143
+ if file_path:
144
+ source["file_path"] = file_path
145
+ sources.append(source)
146
+ return sources
147
+
148
+
149
+ __all__ = ["LightRagServerClient", "LightRagServerAPIError"]
@@ -0,0 +1,68 @@
1
+ """Per-KB connection config and retrieval modes for the LightRAG Server engine.
2
+
3
+ Unlike the hosted PageIndex engine (one global account key shared by every KB),
4
+ each LightRAG Server KB points at its OWN server instance — a standalone LightRAG
5
+ server's ``--workspace`` is fixed at startup, so one server instance = one
6
+ workspace = one knowledge base. The endpoint (base URL + optional API key) is
7
+ therefore stored per-KB in ``kb_config.json`` (the same place the KB's
8
+ ``search_mode`` lives), exactly like a ``linked`` KB stores its ``external_path``.
9
+
10
+ This module is the single seam that reads that binding into a typed config; it
11
+ holds no global state and imports no HTTP client (the client lives in
12
+ ``client.py``).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Any
19
+
20
+ # LightRAG's ``/query`` retrieval modes. ``bypass`` is intentionally omitted: it
21
+ # skips retrieval entirely, which is meaningless for a knowledge base. ``mix``
22
+ # (knowledge-graph + vector + chunks) is the server's own default and the safest
23
+ # general choice, matching the shared per-KB ``search_mode`` default.
24
+ SUPPORTED_MODES = ("naive", "local", "global", "hybrid", "mix")
25
+ DEFAULT_MODE = "mix"
26
+
27
+
28
+ class LightRagServerNotConfiguredError(RuntimeError):
29
+ """Raised when a KB has no LightRAG server URL bound to it."""
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class LightRagServerConfig:
34
+ """A KB's resolved connection to an external LightRAG server."""
35
+
36
+ base_url: str
37
+ api_key: str
38
+
39
+
40
+ def normalize_base_url(url: str | None) -> str:
41
+ """Trim and drop any trailing slash so endpoint paths join cleanly."""
42
+ return (url or "").strip().rstrip("/")
43
+
44
+
45
+ def config_from_entry(entry: dict[str, Any]) -> LightRagServerConfig:
46
+ """Build a :class:`LightRagServerConfig` from a ``kb_config.json`` KB entry.
47
+
48
+ Raises :class:`LightRagServerNotConfiguredError` when no ``server_url`` is
49
+ present, so retrieval fails with a clear message instead of an opaque
50
+ connection error.
51
+ """
52
+ base_url = normalize_base_url(entry.get("server_url"))
53
+ if not base_url:
54
+ raise LightRagServerNotConfiguredError(
55
+ "This knowledge base is not connected to a LightRAG server "
56
+ "(no server URL configured). Re-create it with a valid server URL."
57
+ )
58
+ return LightRagServerConfig(base_url=base_url, api_key=str(entry.get("api_key") or "").strip())
59
+
60
+
61
+ __all__ = [
62
+ "SUPPORTED_MODES",
63
+ "DEFAULT_MODE",
64
+ "LightRagServerNotConfiguredError",
65
+ "LightRagServerConfig",
66
+ "normalize_base_url",
67
+ "config_from_entry",
68
+ ]
@@ -0,0 +1,120 @@
1
+ """Retrieval-only pipeline backed by an external LightRAG server.
2
+
3
+ Implements the same contract as the other pipelines (see ``..base.RAGPipeline``)
4
+ but owns no index: a ``lightrag-server`` KB is a connection pointer (``type:
5
+ lightrag_server`` in ``kb_config.json``) to a standalone LightRAG server the user
6
+ runs and indexed themselves. Only :meth:`search` does real work — it reads the
7
+ KB's endpoint, asks the server for grounded context (no server-side generation),
8
+ and shapes the result for the ``rag`` tool. Indexing is offloaded entirely to the
9
+ server, so :meth:`initialize` / :meth:`add_documents` are not part of this
10
+ engine's job and fail with a clear message; :meth:`delete` is a no-op because
11
+ deleting the KB only drops DeepTutor's pointer (handled by the manager) and must
12
+ never touch the user's server.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from deeptutor.runtime.home import get_runtime_data_root
21
+ from deeptutor.services.rag.provider_binding import load_kb_config_entry
22
+
23
+ from ..modes import resolve_kb_mode
24
+ from .config import (
25
+ DEFAULT_MODE,
26
+ SUPPORTED_MODES,
27
+ LightRagServerNotConfiguredError,
28
+ config_from_entry,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ PROVIDER = "lightrag-server"
34
+ DEFAULT_KB_BASE_DIR = str(get_runtime_data_root() / "knowledge_bases")
35
+
36
+
37
+ class LightRagServerPipeline:
38
+ """Query an external LightRAG server on behalf of a connected KB."""
39
+
40
+ def __init__(self, kb_base_dir: Optional[str] = None, *, client_factory=None, **_: Any) -> None:
41
+ self.logger = logging.getLogger(__name__)
42
+ self.kb_base_dir = kb_base_dir or DEFAULT_KB_BASE_DIR
43
+ # Injection seam for tests: (config) -> client. None uses the real client.
44
+ self._client_factory = client_factory
45
+
46
+ # ----- helpers --------------------------------------------------------
47
+
48
+ def _client(self, config):
49
+ if self._client_factory is not None:
50
+ return self._client_factory(config)
51
+ from .client import LightRagServerClient
52
+
53
+ return LightRagServerClient(config)
54
+
55
+ def _resolve_mode(self, kb_name: str, kwargs: dict[str, Any]) -> str:
56
+ return resolve_kb_mode(
57
+ self.kb_base_dir,
58
+ kb_name,
59
+ PROVIDER,
60
+ explicit=kwargs.get("mode"),
61
+ supported=SUPPORTED_MODES,
62
+ default=DEFAULT_MODE,
63
+ )
64
+
65
+ # ----- retrieval ------------------------------------------------------
66
+
67
+ async def search(self, query: str, kb_name: str, **kwargs) -> Dict[str, Any]:
68
+ try:
69
+ config = config_from_entry(load_kb_config_entry(self.kb_base_dir, kb_name))
70
+ except LightRagServerNotConfiguredError as exc:
71
+ return self._error_result(query, exc, error_type="not_configured")
72
+
73
+ mode = self._resolve_mode(kb_name, kwargs)
74
+ try:
75
+ result = await self._client(config).query_context(query, mode)
76
+ except Exception as exc:
77
+ self.logger.error("LightRAG server search failed for '%s': %s", kb_name, exc)
78
+ return self._error_result(query, exc, error_type="retrieval_error")
79
+
80
+ content = result.get("content") or ""
81
+ return {
82
+ "query": query,
83
+ "answer": content,
84
+ "content": content,
85
+ "sources": result.get("sources") or [],
86
+ "provider": PROVIDER,
87
+ "mode": mode,
88
+ }
89
+
90
+ def _error_result(self, query: str, exc: Exception, *, error_type: str) -> Dict[str, Any]:
91
+ return {
92
+ "query": query,
93
+ "answer": str(exc),
94
+ "content": "",
95
+ "sources": [],
96
+ "provider": PROVIDER,
97
+ "error_type": error_type,
98
+ }
99
+
100
+ # ----- indexing (not applicable — owned by the external server) -------
101
+
102
+ async def initialize(self, kb_name: str, file_paths: List[str], **kwargs) -> bool:
103
+ raise RuntimeError(
104
+ "LightRAG Server knowledge bases are indexed on the external server; "
105
+ "DeepTutor does not build or store their index. Add documents on the "
106
+ "LightRAG server directly."
107
+ )
108
+
109
+ async def add_documents(self, kb_name: str, file_paths: List[str], **kwargs) -> bool:
110
+ return await self.initialize(kb_name, file_paths, **kwargs)
111
+
112
+ # ----- lifecycle ------------------------------------------------------
113
+
114
+ async def delete(self, kb_name: str, **kwargs) -> bool:
115
+ # The KB is only a pointer; the manager removes its config entry. Never
116
+ # touch the user's server. Nothing local to clean up here.
117
+ return True
118
+
119
+
120
+ __all__ = ["LightRagServerPipeline", "PROVIDER"]
@@ -0,0 +1,110 @@
1
+ """Probe an external LightRAG server before connecting a KB to it.
2
+
3
+ Connecting is cheap and reversible, but a typo'd URL or a wrong API key should
4
+ fail loudly at connect time rather than silently at every later query. This
5
+ module answers, in one round-trip pair, the questions the UI needs to confirm:
6
+
7
+ 1. **Reachable, and is it actually a LightRAG server?** ``GET /auth-status`` is
8
+ whitelisted on the server, so it answers without credentials; its LightRAG-
9
+ specific keys confirm we're talking to the right kind of server.
10
+ 2. **Does it require an API key, and is ours accepted?** When auth is enabled we
11
+ validate the key against the read-only ``GET /documents/pipeline_status``.
12
+
13
+ Always returns a :class:`ServerProbe` (never raises); ``ok`` is the single
14
+ boolean the caller gates on, with ``error`` explaining any failure.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import asdict, dataclass
20
+ from typing import Any, Optional
21
+
22
+ from .client import LightRagServerClient
23
+ from .config import LightRagServerConfig, normalize_base_url
24
+
25
+
26
+ @dataclass
27
+ class ServerProbe:
28
+ base_url: str
29
+ ok: bool = False
30
+ reachable: bool = False
31
+ auth_required: bool = False
32
+ auth_ok: bool = False
33
+ core_version: Optional[str] = None
34
+ api_version: Optional[str] = None
35
+ error: Optional[str] = None
36
+
37
+ def to_dict(self) -> dict[str, Any]:
38
+ return asdict(self)
39
+
40
+
41
+ async def probe_server(
42
+ server_url: str,
43
+ api_key: str = "",
44
+ *,
45
+ client_factory=None,
46
+ ) -> ServerProbe:
47
+ """Inspect ``server_url`` for a reachable, authorised LightRAG server.
48
+
49
+ ``client_factory`` (config → client) is an injection seam for tests; in
50
+ production it defaults to a real :class:`LightRagServerClient`.
51
+ """
52
+ base_url = normalize_base_url(server_url)
53
+ probe = ServerProbe(base_url=base_url)
54
+ if not base_url:
55
+ probe.error = "Server URL is required."
56
+ return probe
57
+ if not (base_url.startswith("http://") or base_url.startswith("https://")):
58
+ probe.error = "Server URL must start with http:// or https://."
59
+ return probe
60
+
61
+ config = LightRagServerConfig(base_url=base_url, api_key=(api_key or "").strip())
62
+ client = client_factory(config) if client_factory else LightRagServerClient(config)
63
+
64
+ try:
65
+ status = await client.auth_status()
66
+ except Exception as exc:
67
+ probe.error = f"Could not reach a LightRAG server at {base_url}: {exc}"
68
+ return probe
69
+
70
+ # ``auth_configured`` is LightRAG-specific; its absence means we reached
71
+ # something that isn't a LightRAG server.
72
+ if "auth_configured" not in status:
73
+ probe.error = f"{base_url} responded but does not look like a LightRAG server."
74
+ return probe
75
+
76
+ probe.reachable = True
77
+ probe.core_version = _opt_str(status.get("core_version"))
78
+ probe.api_version = _opt_str(status.get("api_version"))
79
+ probe.auth_required = bool(status.get("auth_configured"))
80
+
81
+ if not probe.auth_required:
82
+ # Open server — nothing to validate. A stray key is simply ignored.
83
+ probe.auth_ok = True
84
+ probe.ok = True
85
+ return probe
86
+
87
+ try:
88
+ probe.auth_ok = await client.verify_key()
89
+ except Exception as exc:
90
+ probe.error = f"Reached the server but could not validate the API key: {exc}"
91
+ return probe
92
+
93
+ if not probe.auth_ok:
94
+ probe.error = (
95
+ "This server requires an API key and the one provided was rejected."
96
+ if api_key
97
+ else "This server requires an API key. Provide one to connect."
98
+ )
99
+ return probe
100
+
101
+ probe.ok = True
102
+ return probe
103
+
104
+
105
+ def _opt_str(value: Any) -> Optional[str]:
106
+ text = str(value).strip() if value is not None else ""
107
+ return text or None
108
+
109
+
110
+ __all__ = ["ServerProbe", "probe_server"]
@@ -0,0 +1,3 @@
1
+ """LlamaIndex RAG pipeline implementation package."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,199 @@
1
+ """Configuration helpers for DeepTutor's LlamaIndex RAG pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import os
7
+ import sys
8
+
9
+ VECTOR_PROFILE = "vector"
10
+ HYBRID_PROFILE = "hybrid"
11
+ SUPPORTED_RETRIEVAL_PROFILES = {VECTOR_PROFILE, HYBRID_PROFILE}
12
+ FLAT_VECTOR_INDEX = "flat"
13
+ HNSW_VECTOR_INDEX = "hnsw"
14
+ SUPPORTED_VECTOR_INDEX_TYPES = {FLAT_VECTOR_INDEX, HNSW_VECTOR_INDEX}
15
+
16
+
17
+ def should_show_progress() -> bool:
18
+ """Whether to emit LlamaIndex ``tqdm`` progress bars.
19
+
20
+ tqdm writes carriage-return progress lines to ``sys.stdout``. When
21
+ DeepTutor runs as a server that stream is a pipe whose read end (the
22
+ launcher's relay thread) can close mid-indexing, and the next tqdm write
23
+ then raises :class:`BrokenPipeError`, killing document indexing. DeepTutor
24
+ reports indexing progress through its own ``ProgressTracker``, so the tqdm
25
+ output is only wanted in an interactive CLI/REPL session.
26
+ """
27
+ return bool(getattr(sys.stdout, "isatty", lambda: False)())
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class RetrievalConfig:
32
+ """Runtime retrieval knobs for the LlamaIndex pipeline."""
33
+
34
+ profile: str = HYBRID_PROFILE
35
+ vector_top_k_multiplier: int = 2
36
+ bm25_top_k_multiplier: int = 2
37
+ fusion_num_queries: int = 1
38
+ reranker_model: str = ""
39
+ rerank_top_k: int = 50
40
+
41
+ def candidate_top_k(self, top_k: int, multiplier: int) -> int:
42
+ """Return the number of candidates to ask a child retriever for."""
43
+ requested = max(1, int(top_k))
44
+ return max(requested, requested * max(1, int(multiplier)))
45
+
46
+ def rerank_candidate_top_k(self, top_k: int) -> int:
47
+ """Return the first-stage candidate count before optional reranking."""
48
+ requested = max(1, int(top_k))
49
+ if not self.reranker_model:
50
+ return requested
51
+ return max(requested, min(100, max(1, int(self.rerank_top_k))))
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class VectorIndexConfig:
56
+ """FAISS construction knobs for the next full index build."""
57
+
58
+ type: str = FLAT_VECTOR_INDEX
59
+ hnsw_m: int = 32
60
+ hnsw_ef_construction: int = 200
61
+ hnsw_ef_search: int = 64
62
+
63
+
64
+ def normalize_retrieval_profile(value: str | None) -> str:
65
+ """Return a supported retrieval profile, defaulting to hybrid."""
66
+ profile = (value or "").strip().lower()
67
+ if profile in SUPPORTED_RETRIEVAL_PROFILES:
68
+ return profile
69
+ return HYBRID_PROFILE
70
+
71
+
72
+ def normalize_reranker_model(value: str | None) -> str:
73
+ """Return a bounded Hugging Face model identifier (empty disables rerank)."""
74
+ return (value or "").strip()[:200]
75
+
76
+
77
+ def normalize_vector_index_type(value: str | None) -> str:
78
+ """Return a supported vector index type, defaulting to exact flat search."""
79
+ index_type = (value or "").strip().lower()
80
+ if index_type in SUPPORTED_VECTOR_INDEX_TYPES:
81
+ return index_type
82
+ return FLAT_VECTOR_INDEX
83
+
84
+
85
+ def retrieval_config_from_env() -> RetrievalConfig:
86
+ """Build retrieval config from environment variables.
87
+
88
+ The default is intentionally ``hybrid``. If the optional LlamaIndex BM25
89
+ integration is not installed, the retriever builder transparently falls
90
+ back to plain vector retrieval.
91
+ """
92
+
93
+ return RetrievalConfig(
94
+ profile=normalize_retrieval_profile(
95
+ os.getenv("DEEPTUTOR_RAG_RETRIEVAL_PROFILE") or os.getenv("RAG_RETRIEVAL_PROFILE")
96
+ )
97
+ )
98
+
99
+
100
+ def _load_runtime_settings() -> dict:
101
+ """Load the persisted LlamaIndex engine settings (env overrides applied)."""
102
+ from deeptutor.services.config import load_llamaindex_settings
103
+
104
+ return load_llamaindex_settings()
105
+
106
+
107
+ def retrieval_config_from_settings() -> RetrievalConfig:
108
+ """Build retrieval config from persisted engine settings.
109
+
110
+ Falls back to defaults on any read error so retrieval never breaks because
111
+ of a malformed settings file. ``fusion_num_queries`` stays at the dataclass
112
+ default — query generation needs a real LLM, but the fusion retriever runs
113
+ on a MockLLM, so it is not user-tunable.
114
+ """
115
+ try:
116
+ settings = _load_runtime_settings()
117
+ except Exception:
118
+ return RetrievalConfig()
119
+ return RetrievalConfig(
120
+ profile=normalize_retrieval_profile(settings.get("retrieval_profile")),
121
+ vector_top_k_multiplier=int(settings.get("vector_top_k_multiplier", 2) or 2),
122
+ bm25_top_k_multiplier=int(settings.get("bm25_top_k_multiplier", 2) or 2),
123
+ reranker_model=normalize_reranker_model(settings.get("reranker_model")),
124
+ rerank_top_k=int(settings.get("rerank_top_k", 50) or 50),
125
+ )
126
+
127
+
128
+ def vector_index_config_from_settings() -> VectorIndexConfig:
129
+ """Build FAISS construction settings, retaining the exact flat default."""
130
+ try:
131
+ settings = _load_runtime_settings()
132
+ except Exception:
133
+ return VectorIndexConfig()
134
+ return VectorIndexConfig(
135
+ type=normalize_vector_index_type(settings.get("vector_index_type")),
136
+ hnsw_m=int(settings.get("hnsw_m", 32) or 32),
137
+ hnsw_ef_construction=int(settings.get("hnsw_ef_construction", 200) or 200),
138
+ hnsw_ef_search=int(settings.get("hnsw_ef_search", 64) or 64),
139
+ )
140
+
141
+
142
+ def default_top_k() -> int:
143
+ """The configured default number of chunks a retrieval returns."""
144
+ try:
145
+ return int(_load_runtime_settings().get("top_k", 5) or 5)
146
+ except Exception:
147
+ return 5
148
+
149
+
150
+ def chunk_geometry() -> tuple[int, int]:
151
+ """The configured ``(chunk_size, chunk_overlap)`` for indexing."""
152
+ try:
153
+ settings = _load_runtime_settings()
154
+ chunk_size = settings.get("chunk_size", 512)
155
+ chunk_overlap = settings.get("chunk_overlap", 50)
156
+ return int(chunk_size if chunk_size is not None else 512), int(
157
+ chunk_overlap if chunk_overlap is not None else 50
158
+ )
159
+ except Exception:
160
+ return 512, 50
161
+
162
+
163
+ def image_description_batch_size() -> int:
164
+ """Opt-in images per LlamaIndex caption request; one preserves legacy calls."""
165
+ try:
166
+ return min(8, max(1, int(_load_runtime_settings().get("image_description_batch_size", 1))))
167
+ except (TypeError, ValueError):
168
+ return 1
169
+
170
+
171
+ def image_description_limits() -> tuple[int, float]:
172
+ """Return the configured vision-call concurrency and per-image timeout."""
173
+ try:
174
+ settings = _load_runtime_settings()
175
+ concurrency = int(settings.get("image_description_concurrency", 4) or 4)
176
+ timeout_seconds = float(settings.get("image_description_timeout_seconds", 60) or 60)
177
+ return min(16, max(1, concurrency)), min(600.0, max(5.0, timeout_seconds))
178
+ except Exception:
179
+ return 4, 60.0
180
+
181
+
182
+ __all__ = [
183
+ "HYBRID_PROFILE",
184
+ "RetrievalConfig",
185
+ "SUPPORTED_RETRIEVAL_PROFILES",
186
+ "SUPPORTED_VECTOR_INDEX_TYPES",
187
+ "VECTOR_PROFILE",
188
+ "FLAT_VECTOR_INDEX",
189
+ "HNSW_VECTOR_INDEX",
190
+ "chunk_geometry",
191
+ "default_top_k",
192
+ "image_description_limits",
193
+ "image_description_batch_size",
194
+ "normalize_retrieval_profile",
195
+ "normalize_reranker_model",
196
+ "retrieval_config_from_env",
197
+ "retrieval_config_from_settings",
198
+ "vector_index_config_from_settings",
199
+ ]