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,41 @@
1
+ """Structural contract every RAG pipeline implements.
2
+
3
+ The RAG service and factory have always relied on duck typing across pipelines
4
+ (``initialize`` / ``add_documents`` / ``search`` / ``delete``). This Protocol
5
+ makes that contract explicit so a new engine — e.g. the PageIndex cloud
6
+ pipeline — can be type-checked against the same shape the LlamaIndex pipeline
7
+ already satisfies. It is intentionally minimal: ``RAGService`` still probes for
8
+ optional methods with ``hasattr`` to stay tolerant of partial implementations.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Dict, List, Protocol, runtime_checkable
14
+
15
+
16
+ @runtime_checkable
17
+ class RAGPipeline(Protocol):
18
+ """A knowledge-base index/retrieve engine bound to a KB by its provider."""
19
+
20
+ async def initialize(self, kb_name: str, file_paths: List[str], **kwargs: Any) -> bool:
21
+ """Build a fresh index for ``kb_name`` from ``file_paths``."""
22
+ ...
23
+
24
+ async def add_documents(self, kb_name: str, file_paths: List[str], **kwargs: Any) -> bool:
25
+ """Incrementally add ``file_paths`` to ``kb_name``'s existing index."""
26
+ ...
27
+
28
+ async def search(self, query: str, kb_name: str, **kwargs: Any) -> Dict[str, Any]:
29
+ """Retrieve grounded context for ``query`` from ``kb_name``.
30
+
31
+ Returns a dict with at least ``query``, ``content``/``answer``,
32
+ ``sources`` and ``provider`` keys.
33
+ """
34
+ ...
35
+
36
+ async def delete(self, kb_name: str, **kwargs: Any) -> bool:
37
+ """Delete ``kb_name`` and any engine-side resources."""
38
+ ...
39
+
40
+
41
+ __all__ = ["RAGPipeline"]
@@ -0,0 +1,18 @@
1
+ """GraphRAG (microsoft/graphrag) local RAG pipeline.
2
+
3
+ A KB indexed with the ``graphrag`` provider builds a local knowledge graph
4
+ (entities, relationships, communities, community reports) from text and serves
5
+ GraphRAG's global/local/drift/basic retrieval. DeepTutor parses documents to
6
+ text first and bridges its own LLM/embedding config into GraphRAG's settings, so
7
+ GraphRAG never parses documents or owns model credentials of its own.
8
+
9
+ GraphRAG is an optional dependency (``pip install 'deeptutor[graphrag]'``). The
10
+ pipeline module imports cleanly without it installed; only actual indexing /
11
+ retrieval requires the package.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from .pipeline import GraphRagPipeline
17
+
18
+ __all__ = ["GraphRagPipeline"]
@@ -0,0 +1,42 @@
1
+ """Resolve and test GraphRAG completion-model candidates without activating them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from deeptutor.services.config import (
6
+ get_model_catalog_service,
7
+ resolve_llm_runtime_config,
8
+ )
9
+
10
+ from . import engine
11
+
12
+
13
+ async def probe_configured_completion_model(profile_id: str, model_id: str) -> dict:
14
+ """Probe one configured model by ID while leaving the active catalog unchanged.
15
+
16
+ Args:
17
+ profile_id: Server-side model-catalog profile ID.
18
+ model_id: Server-side model ID within the selected profile.
19
+
20
+ Returns:
21
+ A secret-free GraphRAG compatibility result.
22
+
23
+ Raises:
24
+ ValueError: If the profile/model selection does not exist.
25
+ """
26
+ service = get_model_catalog_service()
27
+ llm_cfg = resolve_llm_runtime_config(
28
+ catalog=service.load(),
29
+ service=service,
30
+ llm_selection={"profile_id": profile_id, "model_id": model_id},
31
+ )
32
+ return await engine.probe_completion_model(llm_cfg)
33
+
34
+
35
+ async def probe_active_completion_model() -> dict:
36
+ """Probe the globally active chat model without changing catalog state."""
37
+ service = get_model_catalog_service()
38
+ llm_cfg = resolve_llm_runtime_config(catalog=service.load(), service=service)
39
+ return await engine.probe_completion_model(llm_cfg)
40
+
41
+
42
+ __all__ = ["probe_active_completion_model", "probe_configured_completion_model"]
@@ -0,0 +1,251 @@
1
+ """GraphRAG completion adapter with a strict JSON-object compatibility fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ import json
7
+ import threading
8
+ from typing import Any
9
+ from urllib.parse import urlparse
10
+
11
+ from pydantic import BaseModel, ValidationError
12
+
13
+ from .errors import (
14
+ MODEL_INCOMPATIBLE_MESSAGE,
15
+ MODEL_OUTPUT_TRUNCATED_MESSAGE,
16
+ GraphRagStructuredOutputError,
17
+ GraphRagStructuredOutputTruncatedError,
18
+ is_unsupported_schema_error,
19
+ )
20
+ from .provider import COMPLETION_TYPE
21
+
22
+ _CAPABILITY_LOCK = threading.RLock()
23
+ _JSON_OBJECT_CAPABILITIES: set[tuple[str, str, str]] = set()
24
+ _ADAPTER_CLASS: type | None = None
25
+
26
+
27
+ def _capability_key(model_config: Any) -> tuple[str, str, str]:
28
+ return (
29
+ str(getattr(model_config, "model_provider", "") or "").lower(),
30
+ str(getattr(model_config, "model", "") or "").lower(),
31
+ str(getattr(model_config, "api_base", "") or "").rstrip("/").lower(),
32
+ )
33
+
34
+
35
+ def _uses_json_object(model_config: Any) -> bool:
36
+ with _CAPABILITY_LOCK:
37
+ return _capability_key(model_config) in _JSON_OBJECT_CAPABILITIES
38
+
39
+
40
+ def _remember_json_object(model_config: Any) -> None:
41
+ with _CAPABILITY_LOCK:
42
+ _JSON_OBJECT_CAPABILITIES.add(_capability_key(model_config))
43
+
44
+
45
+ def _uses_prompt_only_structured_output(model_config: Any) -> bool:
46
+ """Use the common messages contract for third-party Anthropic-compatible APIs.
47
+
48
+ LiteLLM implements Pydantic response formats on the Anthropic transport with
49
+ a forced synthetic tool. Third-party endpoints may implement Anthropic's
50
+ messages API without matching those tool semantics. A schema instruction
51
+ plus local Pydantic validation keeps the GraphRAG contract strict while
52
+ avoiding assumptions beyond the configured endpoint's base protocol.
53
+ """
54
+ provider = str(getattr(model_config, "model_provider", "") or "").lower()
55
+ if provider != "anthropic":
56
+ return False
57
+
58
+ api_base = str(getattr(model_config, "api_base", "") or "").strip()
59
+ if not api_base:
60
+ return False
61
+ parsed = urlparse(api_base if "://" in api_base else f"https://{api_base}")
62
+ hostname = (parsed.hostname or "").lower().rstrip(".")
63
+ return hostname != "api.anthropic.com" and not hostname.endswith(".anthropic.com")
64
+
65
+
66
+ def clear_capability_cache() -> None:
67
+ """Clear process-local compatibility discoveries; intended for deterministic tests."""
68
+ with _CAPABILITY_LOCK:
69
+ _JSON_OBJECT_CAPABILITIES.clear()
70
+
71
+
72
+ def _is_schema_model(response_format: Any) -> bool:
73
+ return isinstance(response_format, type) and issubclass(response_format, BaseModel)
74
+
75
+
76
+ def _schema_instruction(response_format: type[BaseModel]) -> str:
77
+ schema = json.dumps(
78
+ response_format.model_json_schema(),
79
+ ensure_ascii=False,
80
+ separators=(",", ":"),
81
+ )
82
+ return (
83
+ "Return only one valid JSON object. Do not use Markdown fences or explanatory text. "
84
+ f"The JSON object must match this JSON schema exactly: {schema}"
85
+ )
86
+
87
+
88
+ def _messages_with_schema(messages: Any, response_format: type[BaseModel]) -> Any:
89
+ instruction = _schema_instruction(response_format)
90
+ if isinstance(messages, str):
91
+ return f"{messages}\n\n{instruction}"
92
+ if not isinstance(messages, list):
93
+ return messages
94
+
95
+ copied = list(messages)
96
+ for index in range(len(copied) - 1, -1, -1):
97
+ message = copied[index]
98
+ if not isinstance(message, dict) or message.get("role") != "user":
99
+ continue
100
+ content = message.get("content")
101
+ if isinstance(content, str):
102
+ copied[index] = {**message, "content": f"{content}\n\n{instruction}"}
103
+ return copied
104
+ copied.append({"role": "user", "content": instruction})
105
+ return copied
106
+
107
+
108
+ def _format_fallback_kwargs(kwargs: dict[str, Any], response_format: type[BaseModel]) -> dict:
109
+ fallback = dict(kwargs)
110
+ fallback["messages"] = _messages_with_schema(fallback["messages"], response_format)
111
+ fallback.pop("response_format", None)
112
+ return fallback
113
+
114
+
115
+ def _format_response(response: Any, response_format: type[BaseModel]) -> Any:
116
+ from graphrag_llm.utils import structure_completion_response
117
+
118
+ try:
119
+ response.formatted_response = structure_completion_response(
120
+ response.content,
121
+ response_format,
122
+ )
123
+ except (json.JSONDecodeError, TypeError, ValidationError, ValueError) as error:
124
+ choices = getattr(response, "choices", None)
125
+ finish_reason = getattr(choices[0], "finish_reason", None) if choices else None
126
+ if finish_reason in {"length", "max_tokens"}:
127
+ raise GraphRagStructuredOutputTruncatedError(MODEL_OUTPUT_TRUNCATED_MESSAGE) from error
128
+ raise GraphRagStructuredOutputError(MODEL_INCOMPATIBLE_MESSAGE) from error
129
+ return response
130
+
131
+
132
+ def _native_validation_error(error: BaseException) -> bool:
133
+ return isinstance(error, (json.JSONDecodeError, ValidationError))
134
+
135
+
136
+ def _fallback_sync(instance: Any, kwargs: dict[str, Any], response_format: type[BaseModel]) -> Any:
137
+ fallback = _format_fallback_kwargs(kwargs, response_format)
138
+ if fallback.get("stream"):
139
+ raise ValueError("response_format is not supported for streaming completions.")
140
+ messages = fallback.pop("messages")
141
+ if isinstance(messages, str):
142
+ messages = [{"role": "user", "content": messages}]
143
+ request_metrics = fallback.pop("metrics", None) or {}
144
+ if not instance._track_metrics:
145
+ request_metrics = None
146
+ try:
147
+ response = instance._completion(
148
+ messages=messages,
149
+ metrics=request_metrics,
150
+ response_format_json_object=not _uses_prompt_only_structured_output(
151
+ instance._model_config
152
+ ),
153
+ **fallback,
154
+ )
155
+ return _format_response(response, response_format)
156
+ finally:
157
+ if request_metrics is not None:
158
+ instance._metrics_store.update_metrics(metrics=request_metrics)
159
+
160
+
161
+ async def _fallback_async(
162
+ instance: Any,
163
+ kwargs: dict[str, Any],
164
+ response_format: type[BaseModel],
165
+ ) -> Any:
166
+ fallback = _format_fallback_kwargs(kwargs, response_format)
167
+ if fallback.get("stream"):
168
+ raise ValueError("response_format is not supported for streaming completions.")
169
+ messages = fallback.pop("messages")
170
+ if isinstance(messages, str):
171
+ messages = [{"role": "user", "content": messages}]
172
+ request_metrics = fallback.pop("metrics", None) or {}
173
+ if not instance._track_metrics:
174
+ request_metrics = None
175
+ try:
176
+ response = await instance._completion_async(
177
+ messages=messages,
178
+ metrics=request_metrics,
179
+ response_format_json_object=not _uses_prompt_only_structured_output(
180
+ instance._model_config
181
+ ),
182
+ **fallback,
183
+ )
184
+ return _format_response(response, response_format)
185
+ finally:
186
+ if request_metrics is not None:
187
+ instance._metrics_store.update_metrics(metrics=request_metrics)
188
+
189
+
190
+ def _get_adapter_class() -> type:
191
+ global _ADAPTER_CLASS
192
+ with _CAPABILITY_LOCK:
193
+ if _ADAPTER_CLASS is not None:
194
+ return _ADAPTER_CLASS
195
+
196
+ from graphrag_llm.completion.lite_llm_completion import LiteLLMCompletion
197
+
198
+ class DeepTutorLiteLLMCompletion(LiteLLMCompletion):
199
+ """LiteLLM completion with a narrow GraphRAG structured-output fallback."""
200
+
201
+ def completion(self, /, **kwargs: Any) -> Any:
202
+ response_format = kwargs.get("response_format")
203
+ if not _is_schema_model(response_format):
204
+ return super().completion(**kwargs)
205
+ if _uses_prompt_only_structured_output(self._model_config):
206
+ return _fallback_sync(self, kwargs, response_format)
207
+ if _uses_json_object(self._model_config):
208
+ return _fallback_sync(self, kwargs, response_format)
209
+ try:
210
+ return super().completion(**kwargs)
211
+ except Exception as error:
212
+ explicit_unsupported = is_unsupported_schema_error(error)
213
+ if not explicit_unsupported and not _native_validation_error(error):
214
+ raise
215
+ formatted = _fallback_sync(self, kwargs, response_format)
216
+ if explicit_unsupported:
217
+ _remember_json_object(self._model_config)
218
+ return formatted
219
+
220
+ async def completion_async(self, /, **kwargs: Any) -> Any:
221
+ response_format = kwargs.get("response_format")
222
+ if not _is_schema_model(response_format):
223
+ return await super().completion_async(**kwargs)
224
+ if _uses_prompt_only_structured_output(self._model_config):
225
+ return await _fallback_async(self, kwargs, response_format)
226
+ if _uses_json_object(self._model_config):
227
+ return await _fallback_async(self, kwargs, response_format)
228
+ try:
229
+ return await super().completion_async(**kwargs)
230
+ except Exception as error:
231
+ explicit_unsupported = is_unsupported_schema_error(error)
232
+ if not explicit_unsupported and not _native_validation_error(error):
233
+ raise
234
+ formatted = await _fallback_async(self, kwargs, response_format)
235
+ if explicit_unsupported:
236
+ _remember_json_object(self._model_config)
237
+ return formatted
238
+
239
+ _ADAPTER_CLASS = DeepTutorLiteLLMCompletion
240
+ return _ADAPTER_CLASS
241
+
242
+
243
+ def register_completion_adapter() -> None:
244
+ """Register DeepTutor's completion type through GraphRAG's public factory API."""
245
+ from graphrag_llm.completion import register_completion
246
+
247
+ initializer: Callable[..., Any] = _get_adapter_class()
248
+ register_completion(COMPLETION_TYPE, initializer, scope="singleton")
249
+
250
+
251
+ __all__ = ["clear_capability_cache", "register_completion_adapter"]