realtimex-deeptutor 0.5.0.post1__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 (276) hide show
  1. realtimex_deeptutor/__init__.py +67 -0
  2. realtimex_deeptutor-0.5.0.post1.dist-info/METADATA +1612 -0
  3. realtimex_deeptutor-0.5.0.post1.dist-info/RECORD +276 -0
  4. realtimex_deeptutor-0.5.0.post1.dist-info/WHEEL +5 -0
  5. realtimex_deeptutor-0.5.0.post1.dist-info/entry_points.txt +2 -0
  6. realtimex_deeptutor-0.5.0.post1.dist-info/licenses/LICENSE +661 -0
  7. realtimex_deeptutor-0.5.0.post1.dist-info/top_level.txt +2 -0
  8. src/__init__.py +40 -0
  9. src/agents/__init__.py +24 -0
  10. src/agents/base_agent.py +657 -0
  11. src/agents/chat/__init__.py +24 -0
  12. src/agents/chat/chat_agent.py +435 -0
  13. src/agents/chat/prompts/en/chat_agent.yaml +35 -0
  14. src/agents/chat/prompts/zh/chat_agent.yaml +35 -0
  15. src/agents/chat/session_manager.py +311 -0
  16. src/agents/co_writer/__init__.py +0 -0
  17. src/agents/co_writer/edit_agent.py +260 -0
  18. src/agents/co_writer/narrator_agent.py +423 -0
  19. src/agents/co_writer/prompts/en/edit_agent.yaml +113 -0
  20. src/agents/co_writer/prompts/en/narrator_agent.yaml +88 -0
  21. src/agents/co_writer/prompts/zh/edit_agent.yaml +113 -0
  22. src/agents/co_writer/prompts/zh/narrator_agent.yaml +88 -0
  23. src/agents/guide/__init__.py +16 -0
  24. src/agents/guide/agents/__init__.py +11 -0
  25. src/agents/guide/agents/chat_agent.py +104 -0
  26. src/agents/guide/agents/interactive_agent.py +223 -0
  27. src/agents/guide/agents/locate_agent.py +149 -0
  28. src/agents/guide/agents/summary_agent.py +150 -0
  29. src/agents/guide/guide_manager.py +500 -0
  30. src/agents/guide/prompts/en/chat_agent.yaml +41 -0
  31. src/agents/guide/prompts/en/interactive_agent.yaml +202 -0
  32. src/agents/guide/prompts/en/locate_agent.yaml +68 -0
  33. src/agents/guide/prompts/en/summary_agent.yaml +157 -0
  34. src/agents/guide/prompts/zh/chat_agent.yaml +41 -0
  35. src/agents/guide/prompts/zh/interactive_agent.yaml +626 -0
  36. src/agents/guide/prompts/zh/locate_agent.yaml +68 -0
  37. src/agents/guide/prompts/zh/summary_agent.yaml +157 -0
  38. src/agents/ideagen/__init__.py +12 -0
  39. src/agents/ideagen/idea_generation_workflow.py +426 -0
  40. src/agents/ideagen/material_organizer_agent.py +173 -0
  41. src/agents/ideagen/prompts/en/idea_generation.yaml +187 -0
  42. src/agents/ideagen/prompts/en/material_organizer.yaml +69 -0
  43. src/agents/ideagen/prompts/zh/idea_generation.yaml +187 -0
  44. src/agents/ideagen/prompts/zh/material_organizer.yaml +69 -0
  45. src/agents/question/__init__.py +24 -0
  46. src/agents/question/agents/__init__.py +18 -0
  47. src/agents/question/agents/generate_agent.py +381 -0
  48. src/agents/question/agents/relevance_analyzer.py +207 -0
  49. src/agents/question/agents/retrieve_agent.py +239 -0
  50. src/agents/question/coordinator.py +718 -0
  51. src/agents/question/example.py +109 -0
  52. src/agents/question/prompts/en/coordinator.yaml +75 -0
  53. src/agents/question/prompts/en/generate_agent.yaml +77 -0
  54. src/agents/question/prompts/en/relevance_analyzer.yaml +41 -0
  55. src/agents/question/prompts/en/retrieve_agent.yaml +32 -0
  56. src/agents/question/prompts/zh/coordinator.yaml +75 -0
  57. src/agents/question/prompts/zh/generate_agent.yaml +77 -0
  58. src/agents/question/prompts/zh/relevance_analyzer.yaml +39 -0
  59. src/agents/question/prompts/zh/retrieve_agent.yaml +30 -0
  60. src/agents/research/agents/__init__.py +23 -0
  61. src/agents/research/agents/decompose_agent.py +507 -0
  62. src/agents/research/agents/manager_agent.py +228 -0
  63. src/agents/research/agents/note_agent.py +180 -0
  64. src/agents/research/agents/rephrase_agent.py +263 -0
  65. src/agents/research/agents/reporting_agent.py +1333 -0
  66. src/agents/research/agents/research_agent.py +714 -0
  67. src/agents/research/data_structures.py +451 -0
  68. src/agents/research/main.py +188 -0
  69. src/agents/research/prompts/en/decompose_agent.yaml +89 -0
  70. src/agents/research/prompts/en/manager_agent.yaml +24 -0
  71. src/agents/research/prompts/en/note_agent.yaml +121 -0
  72. src/agents/research/prompts/en/rephrase_agent.yaml +58 -0
  73. src/agents/research/prompts/en/reporting_agent.yaml +380 -0
  74. src/agents/research/prompts/en/research_agent.yaml +173 -0
  75. src/agents/research/prompts/zh/decompose_agent.yaml +89 -0
  76. src/agents/research/prompts/zh/manager_agent.yaml +24 -0
  77. src/agents/research/prompts/zh/note_agent.yaml +121 -0
  78. src/agents/research/prompts/zh/rephrase_agent.yaml +58 -0
  79. src/agents/research/prompts/zh/reporting_agent.yaml +380 -0
  80. src/agents/research/prompts/zh/research_agent.yaml +173 -0
  81. src/agents/research/research_pipeline.py +1309 -0
  82. src/agents/research/utils/__init__.py +60 -0
  83. src/agents/research/utils/citation_manager.py +799 -0
  84. src/agents/research/utils/json_utils.py +98 -0
  85. src/agents/research/utils/token_tracker.py +297 -0
  86. src/agents/solve/__init__.py +80 -0
  87. src/agents/solve/analysis_loop/__init__.py +14 -0
  88. src/agents/solve/analysis_loop/investigate_agent.py +414 -0
  89. src/agents/solve/analysis_loop/note_agent.py +190 -0
  90. src/agents/solve/main_solver.py +862 -0
  91. src/agents/solve/memory/__init__.py +34 -0
  92. src/agents/solve/memory/citation_memory.py +353 -0
  93. src/agents/solve/memory/investigate_memory.py +226 -0
  94. src/agents/solve/memory/solve_memory.py +340 -0
  95. src/agents/solve/prompts/en/analysis_loop/investigate_agent.yaml +55 -0
  96. src/agents/solve/prompts/en/analysis_loop/note_agent.yaml +54 -0
  97. src/agents/solve/prompts/en/solve_loop/manager_agent.yaml +67 -0
  98. src/agents/solve/prompts/en/solve_loop/precision_answer_agent.yaml +62 -0
  99. src/agents/solve/prompts/en/solve_loop/response_agent.yaml +90 -0
  100. src/agents/solve/prompts/en/solve_loop/solve_agent.yaml +75 -0
  101. src/agents/solve/prompts/en/solve_loop/tool_agent.yaml +38 -0
  102. src/agents/solve/prompts/zh/analysis_loop/investigate_agent.yaml +53 -0
  103. src/agents/solve/prompts/zh/analysis_loop/note_agent.yaml +54 -0
  104. src/agents/solve/prompts/zh/solve_loop/manager_agent.yaml +66 -0
  105. src/agents/solve/prompts/zh/solve_loop/precision_answer_agent.yaml +62 -0
  106. src/agents/solve/prompts/zh/solve_loop/response_agent.yaml +90 -0
  107. src/agents/solve/prompts/zh/solve_loop/solve_agent.yaml +76 -0
  108. src/agents/solve/prompts/zh/solve_loop/tool_agent.yaml +41 -0
  109. src/agents/solve/solve_loop/__init__.py +22 -0
  110. src/agents/solve/solve_loop/citation_manager.py +74 -0
  111. src/agents/solve/solve_loop/manager_agent.py +274 -0
  112. src/agents/solve/solve_loop/precision_answer_agent.py +96 -0
  113. src/agents/solve/solve_loop/response_agent.py +301 -0
  114. src/agents/solve/solve_loop/solve_agent.py +325 -0
  115. src/agents/solve/solve_loop/tool_agent.py +470 -0
  116. src/agents/solve/utils/__init__.py +64 -0
  117. src/agents/solve/utils/config_validator.py +313 -0
  118. src/agents/solve/utils/display_manager.py +223 -0
  119. src/agents/solve/utils/error_handler.py +363 -0
  120. src/agents/solve/utils/json_utils.py +98 -0
  121. src/agents/solve/utils/performance_monitor.py +407 -0
  122. src/agents/solve/utils/token_tracker.py +541 -0
  123. src/api/__init__.py +0 -0
  124. src/api/main.py +240 -0
  125. src/api/routers/__init__.py +1 -0
  126. src/api/routers/agent_config.py +69 -0
  127. src/api/routers/chat.py +296 -0
  128. src/api/routers/co_writer.py +337 -0
  129. src/api/routers/config.py +627 -0
  130. src/api/routers/dashboard.py +18 -0
  131. src/api/routers/guide.py +337 -0
  132. src/api/routers/ideagen.py +436 -0
  133. src/api/routers/knowledge.py +821 -0
  134. src/api/routers/notebook.py +247 -0
  135. src/api/routers/question.py +537 -0
  136. src/api/routers/research.py +394 -0
  137. src/api/routers/settings.py +164 -0
  138. src/api/routers/solve.py +305 -0
  139. src/api/routers/system.py +252 -0
  140. src/api/run_server.py +61 -0
  141. src/api/utils/history.py +172 -0
  142. src/api/utils/log_interceptor.py +21 -0
  143. src/api/utils/notebook_manager.py +415 -0
  144. src/api/utils/progress_broadcaster.py +72 -0
  145. src/api/utils/task_id_manager.py +100 -0
  146. src/config/__init__.py +0 -0
  147. src/config/accessors.py +18 -0
  148. src/config/constants.py +34 -0
  149. src/config/defaults.py +18 -0
  150. src/config/schema.py +38 -0
  151. src/config/settings.py +50 -0
  152. src/core/errors.py +62 -0
  153. src/knowledge/__init__.py +23 -0
  154. src/knowledge/add_documents.py +606 -0
  155. src/knowledge/config.py +65 -0
  156. src/knowledge/example_add_documents.py +236 -0
  157. src/knowledge/extract_numbered_items.py +1039 -0
  158. src/knowledge/initializer.py +621 -0
  159. src/knowledge/kb.py +22 -0
  160. src/knowledge/manager.py +782 -0
  161. src/knowledge/progress_tracker.py +182 -0
  162. src/knowledge/start_kb.py +535 -0
  163. src/logging/__init__.py +103 -0
  164. src/logging/adapters/__init__.py +17 -0
  165. src/logging/adapters/lightrag.py +184 -0
  166. src/logging/adapters/llamaindex.py +141 -0
  167. src/logging/config.py +80 -0
  168. src/logging/handlers/__init__.py +20 -0
  169. src/logging/handlers/console.py +75 -0
  170. src/logging/handlers/file.py +201 -0
  171. src/logging/handlers/websocket.py +127 -0
  172. src/logging/logger.py +709 -0
  173. src/logging/stats/__init__.py +16 -0
  174. src/logging/stats/llm_stats.py +179 -0
  175. src/services/__init__.py +56 -0
  176. src/services/config/__init__.py +61 -0
  177. src/services/config/knowledge_base_config.py +210 -0
  178. src/services/config/loader.py +260 -0
  179. src/services/config/unified_config.py +603 -0
  180. src/services/embedding/__init__.py +45 -0
  181. src/services/embedding/adapters/__init__.py +22 -0
  182. src/services/embedding/adapters/base.py +106 -0
  183. src/services/embedding/adapters/cohere.py +127 -0
  184. src/services/embedding/adapters/jina.py +99 -0
  185. src/services/embedding/adapters/ollama.py +116 -0
  186. src/services/embedding/adapters/openai_compatible.py +96 -0
  187. src/services/embedding/client.py +159 -0
  188. src/services/embedding/config.py +156 -0
  189. src/services/embedding/provider.py +119 -0
  190. src/services/llm/__init__.py +152 -0
  191. src/services/llm/capabilities.py +313 -0
  192. src/services/llm/client.py +302 -0
  193. src/services/llm/cloud_provider.py +530 -0
  194. src/services/llm/config.py +200 -0
  195. src/services/llm/error_mapping.py +103 -0
  196. src/services/llm/exceptions.py +152 -0
  197. src/services/llm/factory.py +450 -0
  198. src/services/llm/local_provider.py +347 -0
  199. src/services/llm/providers/anthropic.py +95 -0
  200. src/services/llm/providers/base_provider.py +93 -0
  201. src/services/llm/providers/open_ai.py +83 -0
  202. src/services/llm/registry.py +71 -0
  203. src/services/llm/telemetry.py +40 -0
  204. src/services/llm/types.py +27 -0
  205. src/services/llm/utils.py +333 -0
  206. src/services/prompt/__init__.py +25 -0
  207. src/services/prompt/manager.py +206 -0
  208. src/services/rag/__init__.py +64 -0
  209. src/services/rag/components/__init__.py +29 -0
  210. src/services/rag/components/base.py +59 -0
  211. src/services/rag/components/chunkers/__init__.py +18 -0
  212. src/services/rag/components/chunkers/base.py +34 -0
  213. src/services/rag/components/chunkers/fixed.py +71 -0
  214. src/services/rag/components/chunkers/numbered_item.py +94 -0
  215. src/services/rag/components/chunkers/semantic.py +97 -0
  216. src/services/rag/components/embedders/__init__.py +14 -0
  217. src/services/rag/components/embedders/base.py +32 -0
  218. src/services/rag/components/embedders/openai.py +63 -0
  219. src/services/rag/components/indexers/__init__.py +18 -0
  220. src/services/rag/components/indexers/base.py +35 -0
  221. src/services/rag/components/indexers/graph.py +172 -0
  222. src/services/rag/components/indexers/lightrag.py +156 -0
  223. src/services/rag/components/indexers/vector.py +146 -0
  224. src/services/rag/components/parsers/__init__.py +18 -0
  225. src/services/rag/components/parsers/base.py +35 -0
  226. src/services/rag/components/parsers/markdown.py +52 -0
  227. src/services/rag/components/parsers/pdf.py +115 -0
  228. src/services/rag/components/parsers/text.py +86 -0
  229. src/services/rag/components/retrievers/__init__.py +18 -0
  230. src/services/rag/components/retrievers/base.py +34 -0
  231. src/services/rag/components/retrievers/dense.py +200 -0
  232. src/services/rag/components/retrievers/hybrid.py +164 -0
  233. src/services/rag/components/retrievers/lightrag.py +169 -0
  234. src/services/rag/components/routing.py +286 -0
  235. src/services/rag/factory.py +234 -0
  236. src/services/rag/pipeline.py +215 -0
  237. src/services/rag/pipelines/__init__.py +32 -0
  238. src/services/rag/pipelines/academic.py +44 -0
  239. src/services/rag/pipelines/lightrag.py +43 -0
  240. src/services/rag/pipelines/llamaindex.py +313 -0
  241. src/services/rag/pipelines/raganything.py +384 -0
  242. src/services/rag/service.py +244 -0
  243. src/services/rag/types.py +73 -0
  244. src/services/search/__init__.py +284 -0
  245. src/services/search/base.py +87 -0
  246. src/services/search/consolidation.py +398 -0
  247. src/services/search/providers/__init__.py +128 -0
  248. src/services/search/providers/baidu.py +188 -0
  249. src/services/search/providers/exa.py +194 -0
  250. src/services/search/providers/jina.py +161 -0
  251. src/services/search/providers/perplexity.py +153 -0
  252. src/services/search/providers/serper.py +209 -0
  253. src/services/search/providers/tavily.py +161 -0
  254. src/services/search/types.py +114 -0
  255. src/services/setup/__init__.py +34 -0
  256. src/services/setup/init.py +285 -0
  257. src/services/tts/__init__.py +16 -0
  258. src/services/tts/config.py +99 -0
  259. src/tools/__init__.py +91 -0
  260. src/tools/code_executor.py +536 -0
  261. src/tools/paper_search_tool.py +171 -0
  262. src/tools/query_item_tool.py +310 -0
  263. src/tools/question/__init__.py +15 -0
  264. src/tools/question/exam_mimic.py +616 -0
  265. src/tools/question/pdf_parser.py +211 -0
  266. src/tools/question/question_extractor.py +397 -0
  267. src/tools/rag_tool.py +173 -0
  268. src/tools/tex_chunker.py +339 -0
  269. src/tools/tex_downloader.py +253 -0
  270. src/tools/web_search.py +71 -0
  271. src/utils/config_manager.py +206 -0
  272. src/utils/document_validator.py +168 -0
  273. src/utils/error_rate_tracker.py +111 -0
  274. src/utils/error_utils.py +82 -0
  275. src/utils/json_parser.py +110 -0
  276. src/utils/network/circuit_breaker.py +79 -0
@@ -0,0 +1,313 @@
1
+ """
2
+ Provider Capabilities
3
+ =====================
4
+
5
+ Centralized configuration for LLM provider capabilities.
6
+ This replaces scattered hardcoded checks throughout the codebase.
7
+
8
+ Usage:
9
+ from src.services.llm.capabilities import get_capability, supports_response_format
10
+
11
+ # Check if a provider supports response_format
12
+ if supports_response_format(binding, model):
13
+ kwargs["response_format"] = {"type": "json_object"}
14
+
15
+ # Generic capability check
16
+ if get_capability(binding, "streaming", default=True):
17
+ # use streaming
18
+ """
19
+
20
+ from typing import Any, Optional
21
+
22
+ # Provider capabilities configuration
23
+ # Keys are binding names (lowercase), values are capability dictionaries
24
+ PROVIDER_CAPABILITIES: dict[str, dict[str, Any]] = {
25
+ # OpenAI and OpenAI-compatible providers
26
+ "openai": {
27
+ "supports_response_format": True,
28
+ "supports_streaming": True,
29
+ "supports_tools": True,
30
+ "system_in_messages": True, # System prompt goes in messages array
31
+ "newer_models_use_max_completion_tokens": True,
32
+ },
33
+ "azure_openai": {
34
+ "supports_response_format": True,
35
+ "supports_streaming": True,
36
+ "supports_tools": True,
37
+ "system_in_messages": True,
38
+ "newer_models_use_max_completion_tokens": True,
39
+ "requires_api_version": True,
40
+ },
41
+ # Anthropic
42
+ "anthropic": {
43
+ "supports_response_format": False, # Anthropic uses different format
44
+ "supports_streaming": True,
45
+ "supports_tools": True,
46
+ "system_in_messages": False, # System is a separate parameter
47
+ "has_thinking_tags": False,
48
+ },
49
+ "claude": { # Alias for anthropic
50
+ "supports_response_format": False,
51
+ "supports_streaming": True,
52
+ "supports_tools": True,
53
+ "system_in_messages": False,
54
+ "has_thinking_tags": False,
55
+ },
56
+ # DeepSeek
57
+ "deepseek": {
58
+ "supports_response_format": False, # DeepSeek doesn't support strict JSON schema yet
59
+ "supports_streaming": True,
60
+ "supports_tools": True,
61
+ "system_in_messages": True,
62
+ "has_thinking_tags": True, # DeepSeek reasoner has thinking tags
63
+ },
64
+ # OpenRouter (aggregator, generally OpenAI-compatible)
65
+ "openrouter": {
66
+ "supports_response_format": True, # Depends on underlying model
67
+ "supports_streaming": True,
68
+ "supports_tools": True,
69
+ "system_in_messages": True,
70
+ },
71
+ # Groq (fast inference)
72
+ "groq": {
73
+ "supports_response_format": True,
74
+ "supports_streaming": True,
75
+ "supports_tools": True,
76
+ "system_in_messages": True,
77
+ },
78
+ # Together AI
79
+ "together": {
80
+ "supports_response_format": True,
81
+ "supports_streaming": True,
82
+ "supports_tools": True,
83
+ "system_in_messages": True,
84
+ },
85
+ "together_ai": { # Alias
86
+ "supports_response_format": True,
87
+ "supports_streaming": True,
88
+ "supports_tools": True,
89
+ "system_in_messages": True,
90
+ },
91
+ # Mistral
92
+ "mistral": {
93
+ "supports_response_format": True,
94
+ "supports_streaming": True,
95
+ "supports_tools": True,
96
+ "system_in_messages": True,
97
+ },
98
+ # Local providers (generally OpenAI-compatible)
99
+ "ollama": {
100
+ "supports_response_format": True, # Ollama supports JSON mode
101
+ "supports_streaming": True,
102
+ "supports_tools": False, # Limited tool support
103
+ "system_in_messages": True,
104
+ },
105
+ "lm_studio": {
106
+ "supports_response_format": True,
107
+ "supports_streaming": True,
108
+ "supports_tools": False,
109
+ "system_in_messages": True,
110
+ },
111
+ "vllm": {
112
+ "supports_response_format": True,
113
+ "supports_streaming": True,
114
+ "supports_tools": False,
115
+ "system_in_messages": True,
116
+ },
117
+ "llama_cpp": {
118
+ "supports_response_format": True, # llama.cpp server supports JSON grammar
119
+ "supports_streaming": True,
120
+ "supports_tools": False,
121
+ "system_in_messages": True,
122
+ },
123
+ }
124
+
125
+ # Default capabilities for unknown providers (assume OpenAI-compatible)
126
+ DEFAULT_CAPABILITIES: dict[str, Any] = {
127
+ "supports_response_format": True,
128
+ "supports_streaming": True,
129
+ "supports_tools": False,
130
+ "system_in_messages": True,
131
+ "has_thinking_tags": False,
132
+ }
133
+
134
+ # Model-specific overrides
135
+ # Format: {model_pattern: {capability: value}}
136
+ # Patterns are matched with case-insensitive startswith
137
+ MODEL_OVERRIDES: dict[str, dict[str, Any]] = {
138
+ "deepseek": {
139
+ "supports_response_format": False,
140
+ "has_thinking_tags": True,
141
+ },
142
+ "deepseek-reasoner": {
143
+ "supports_response_format": False,
144
+ "has_thinking_tags": True,
145
+ },
146
+ "qwen": {
147
+ # Qwen models may have thinking tags
148
+ "has_thinking_tags": True,
149
+ },
150
+ "qwq": {
151
+ # QwQ is Qwen's reasoning model with thinking tags
152
+ "has_thinking_tags": True,
153
+ },
154
+ # Claude models through OpenRouter or other providers
155
+ "claude": {
156
+ "supports_response_format": False,
157
+ "system_in_messages": False,
158
+ },
159
+ # Anthropic models
160
+ "anthropic/": {
161
+ "supports_response_format": False,
162
+ "system_in_messages": False,
163
+ },
164
+ }
165
+
166
+
167
+ def get_capability(
168
+ binding: str,
169
+ capability: str,
170
+ model: Optional[str] = None,
171
+ default: Any = None,
172
+ ) -> Any:
173
+ """
174
+ Get a capability value for a provider/model combination.
175
+
176
+ Checks in order:
177
+ 1. Model-specific overrides (matched by prefix)
178
+ 2. Provider/binding capabilities
179
+ 3. Default capabilities for unknown providers
180
+ 4. Explicit default value
181
+
182
+ Args:
183
+ binding: Provider binding name (e.g., "openai", "anthropic", "deepseek")
184
+ capability: Capability name (e.g., "supports_response_format")
185
+ model: Optional model name for model-specific overrides
186
+ default: Default value if capability is not defined
187
+
188
+ Returns:
189
+ Capability value or default
190
+ """
191
+ binding_lower = (binding or "openai").lower()
192
+
193
+ # 1. Check model-specific overrides first
194
+ if model:
195
+ model_lower = model.lower()
196
+ # Sort by pattern length descending to match most specific first
197
+ for pattern, overrides in sorted(MODEL_OVERRIDES.items(), key=lambda x: -len(x[0])):
198
+ if model_lower.startswith(pattern):
199
+ if capability in overrides:
200
+ return overrides[capability]
201
+
202
+ # 2. Check provider capabilities
203
+ provider_caps = PROVIDER_CAPABILITIES.get(binding_lower, {})
204
+ if capability in provider_caps:
205
+ return provider_caps[capability]
206
+
207
+ # 3. Check default capabilities for unknown providers
208
+ if capability in DEFAULT_CAPABILITIES:
209
+ return DEFAULT_CAPABILITIES[capability]
210
+
211
+ # 4. Return explicit default
212
+ return default
213
+
214
+
215
+ def supports_response_format(binding: str, model: Optional[str] = None) -> bool:
216
+ """
217
+ Check if the provider/model supports response_format parameter.
218
+
219
+ This is a convenience function for the most common capability check.
220
+
221
+ Args:
222
+ binding: Provider binding name
223
+ model: Optional model name for model-specific overrides
224
+
225
+ Returns:
226
+ True if response_format is supported
227
+ """
228
+ return get_capability(binding, "supports_response_format", model, default=True)
229
+
230
+
231
+ def supports_streaming(binding: str, model: Optional[str] = None) -> bool:
232
+ """
233
+ Check if the provider/model supports streaming responses.
234
+
235
+ Args:
236
+ binding: Provider binding name
237
+ model: Optional model name
238
+
239
+ Returns:
240
+ True if streaming is supported
241
+ """
242
+ return get_capability(binding, "supports_streaming", model, default=True)
243
+
244
+
245
+ def system_in_messages(binding: str, model: Optional[str] = None) -> bool:
246
+ """
247
+ Check if system prompt should be in messages array (OpenAI style)
248
+ or as a separate parameter (Anthropic style).
249
+
250
+ Args:
251
+ binding: Provider binding name
252
+ model: Optional model name
253
+
254
+ Returns:
255
+ True if system prompt goes in messages array
256
+ """
257
+ return get_capability(binding, "system_in_messages", model, default=True)
258
+
259
+
260
+ def has_thinking_tags(binding: str, model: Optional[str] = None) -> bool:
261
+ """
262
+ Check if the model output may contain thinking tags (<think>...</think>).
263
+
264
+ Args:
265
+ binding: Provider binding name
266
+ model: Optional model name
267
+
268
+ Returns:
269
+ True if thinking tags should be filtered
270
+ """
271
+ return get_capability(binding, "has_thinking_tags", model, default=False)
272
+
273
+
274
+ def supports_tools(binding: str, model: Optional[str] = None) -> bool:
275
+ """
276
+ Check if the provider/model supports function calling / tools.
277
+
278
+ Args:
279
+ binding: Provider binding name
280
+ model: Optional model name
281
+
282
+ Returns:
283
+ True if tools/function calling is supported
284
+ """
285
+ return get_capability(binding, "supports_tools", model, default=False)
286
+
287
+
288
+ def requires_api_version(binding: str, model: Optional[str] = None) -> bool:
289
+ """
290
+ Check if the provider requires an API version parameter (e.g., Azure OpenAI).
291
+
292
+ Args:
293
+ binding: Provider binding name
294
+ model: Optional model name
295
+
296
+ Returns:
297
+ True if api_version is required
298
+ """
299
+ return get_capability(binding, "requires_api_version", model, default=False)
300
+
301
+
302
+ __all__ = [
303
+ "PROVIDER_CAPABILITIES",
304
+ "MODEL_OVERRIDES",
305
+ "DEFAULT_CAPABILITIES",
306
+ "get_capability",
307
+ "supports_response_format",
308
+ "supports_streaming",
309
+ "system_in_messages",
310
+ "has_thinking_tags",
311
+ "supports_tools",
312
+ "requires_api_version",
313
+ ]
@@ -0,0 +1,302 @@
1
+ """
2
+ LLM Client
3
+ ==========
4
+
5
+ Unified LLM client for all DeepTutor services.
6
+
7
+ Note: This is a legacy interface. Prefer using the factory functions directly:
8
+ from src.services.llm import complete, stream
9
+ """
10
+
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from src.logging import get_logger
14
+
15
+ from .capabilities import system_in_messages
16
+ from .config import LLMConfig, get_llm_config
17
+
18
+
19
+ class LLMClient:
20
+ """
21
+ Unified LLM client for all services.
22
+
23
+ Wraps the LLM Factory with a class-based interface.
24
+ Prefer using factory functions (complete, stream) directly for new code.
25
+ """
26
+
27
+ def __init__(self, config: Optional[LLMConfig] = None):
28
+ """
29
+ Initialize LLM client.
30
+
31
+ Args:
32
+ config: LLM configuration. If None, loads from environment.
33
+ """
34
+ self.config = config or get_llm_config()
35
+ self.logger = get_logger("LLMClient")
36
+
37
+ async def complete(
38
+ self,
39
+ prompt: str,
40
+ system_prompt: Optional[str] = None,
41
+ history: Optional[List[Dict[str, str]]] = None,
42
+ **kwargs: Any,
43
+ ) -> str:
44
+ """
45
+ Call LLM completion via Factory.
46
+
47
+ Args:
48
+ prompt: User prompt
49
+ system_prompt: Optional system prompt
50
+ history: Optional conversation history
51
+ **kwargs: Additional arguments passed to the API
52
+
53
+ Returns:
54
+ LLM response text
55
+ """
56
+ from . import factory
57
+
58
+ # Delegate to factory for unified routing and retry handling
59
+ return await factory.complete(
60
+ prompt=prompt,
61
+ system_prompt=system_prompt or "You are a helpful assistant.",
62
+ model=self.config.model,
63
+ api_key=self.config.api_key,
64
+ base_url=self.config.base_url,
65
+ api_version=getattr(self.config, "api_version", None),
66
+ binding=getattr(self.config, "binding", "openai"),
67
+ **kwargs,
68
+ )
69
+
70
+ def complete_sync(
71
+ self,
72
+ prompt: str,
73
+ system_prompt: Optional[str] = None,
74
+ history: Optional[List[Dict[str, str]]] = None,
75
+ **kwargs: Any,
76
+ ) -> str:
77
+ """
78
+ Synchronous wrapper for complete().
79
+
80
+ Use this when you need to call from non-async context.
81
+ """
82
+ import asyncio
83
+
84
+ try:
85
+ asyncio.get_running_loop()
86
+ except RuntimeError:
87
+ # No running event loop -> safe to run synchronously.
88
+ return asyncio.run(self.complete(prompt, system_prompt, history, **kwargs))
89
+
90
+ raise RuntimeError(
91
+ "LLMClient.complete_sync() cannot be called from a running event loop. "
92
+ "Use `await llm.complete(...)` instead."
93
+ )
94
+
95
+ def get_model_func(self):
96
+ """
97
+ Get a function compatible with LightRAG's llm_model_func parameter.
98
+
99
+ Returns:
100
+ Callable that can be used as llm_model_func
101
+ """
102
+ binding = getattr(self.config, "binding", "openai")
103
+
104
+ # Use capabilities to determine if provider uses OpenAI-style messages
105
+ uses_openai_style = system_in_messages(binding, self.config.model)
106
+
107
+ # For non-OpenAI-compatible providers (e.g., Anthropic), use Factory
108
+ if not uses_openai_style:
109
+ from . import factory
110
+
111
+ def llm_model_func_via_factory(
112
+ prompt: str,
113
+ system_prompt: Optional[str] = None,
114
+ history_messages: Optional[List[Dict]] = None,
115
+ **kwargs: Any,
116
+ ):
117
+ return factory.complete(
118
+ prompt=prompt,
119
+ system_prompt=system_prompt or "You are a helpful assistant.",
120
+ model=self.config.model,
121
+ api_key=self.config.api_key,
122
+ base_url=self.config.base_url,
123
+ binding=binding,
124
+ history_messages=history_messages,
125
+ **kwargs,
126
+ )
127
+
128
+ return llm_model_func_via_factory
129
+
130
+ # OpenAI-compatible bindings use lightrag (has caching)
131
+ from lightrag.llm.openai import openai_complete_if_cache
132
+
133
+ def llm_model_func(
134
+ prompt: str,
135
+ system_prompt: Optional[str] = None,
136
+ history_messages: Optional[List[Dict]] = None,
137
+ **kwargs: Any,
138
+ ):
139
+ # Only pass api_version if set (for Azure OpenAI)
140
+ lightrag_kwargs = {
141
+ "system_prompt": system_prompt,
142
+ "history_messages": history_messages or [],
143
+ "api_key": self.config.api_key,
144
+ "base_url": self.config.base_url,
145
+ **kwargs,
146
+ }
147
+ api_version = getattr(self.config, "api_version", None)
148
+ if api_version:
149
+ lightrag_kwargs["api_version"] = api_version
150
+ return openai_complete_if_cache(
151
+ self.config.model,
152
+ prompt,
153
+ **lightrag_kwargs,
154
+ )
155
+
156
+ return llm_model_func
157
+
158
+ def get_vision_model_func(self):
159
+ """
160
+ Get a function compatible with RAG-Anything's vision_model_func parameter.
161
+
162
+ Returns:
163
+ Callable that can be used as vision_model_func
164
+ """
165
+ binding = getattr(self.config, "binding", "openai")
166
+
167
+ # Use capabilities to determine if provider uses OpenAI-style messages
168
+ uses_openai_style = system_in_messages(binding, self.config.model)
169
+
170
+ # For non-OpenAI-compatible providers, use Factory
171
+ if not uses_openai_style:
172
+ from . import factory
173
+
174
+ def vision_model_func_via_factory(
175
+ prompt: str,
176
+ system_prompt: Optional[str] = None,
177
+ history_messages: Optional[List[Dict]] = None,
178
+ image_data: Optional[str] = None,
179
+ messages: Optional[List[Dict]] = None,
180
+ **kwargs: Any,
181
+ ):
182
+ # Use factory for unified handling
183
+ return factory.complete(
184
+ prompt=prompt,
185
+ system_prompt=system_prompt or "You are a helpful assistant.",
186
+ model=self.config.model,
187
+ api_key=self.config.api_key,
188
+ base_url=self.config.base_url,
189
+ binding=binding,
190
+ messages=messages,
191
+ history_messages=history_messages,
192
+ image_data=image_data,
193
+ **kwargs,
194
+ )
195
+
196
+ return vision_model_func_via_factory
197
+
198
+ # OpenAI-compatible bindings
199
+ from lightrag.llm.openai import openai_complete_if_cache
200
+
201
+ # Get api_version once for reuse
202
+ api_version = getattr(self.config, "api_version", None)
203
+
204
+ def vision_model_func(
205
+ prompt: str,
206
+ system_prompt: Optional[str] = None,
207
+ history_messages: Optional[List[Dict]] = None,
208
+ image_data: Optional[str] = None,
209
+ messages: Optional[List[Dict]] = None,
210
+ **kwargs: Any,
211
+ ):
212
+ # Handle multimodal messages
213
+ if messages:
214
+ clean_kwargs = {
215
+ k: v
216
+ for k, v in kwargs.items()
217
+ if k not in ["messages", "prompt", "system_prompt", "history_messages"]
218
+ }
219
+ lightrag_kwargs = {
220
+ "messages": messages,
221
+ "api_key": self.config.api_key,
222
+ "base_url": self.config.base_url,
223
+ **clean_kwargs,
224
+ }
225
+ if api_version:
226
+ lightrag_kwargs["api_version"] = api_version
227
+ return openai_complete_if_cache(
228
+ self.config.model,
229
+ prompt="",
230
+ **lightrag_kwargs,
231
+ )
232
+
233
+ # Handle image data
234
+ if image_data:
235
+ # Build image message
236
+ image_message = {
237
+ "role": "user",
238
+ "content": [
239
+ {"type": "text", "text": prompt},
240
+ {
241
+ "type": "image_url",
242
+ "image_url": {"url": f"data:image/jpeg;base64,{image_data}"},
243
+ },
244
+ ],
245
+ }
246
+ lightrag_kwargs = {
247
+ "messages": [image_message],
248
+ "api_key": self.config.api_key,
249
+ "base_url": self.config.base_url,
250
+ **kwargs,
251
+ }
252
+ if api_version:
253
+ lightrag_kwargs["api_version"] = api_version
254
+ return openai_complete_if_cache(
255
+ self.config.model,
256
+ prompt="",
257
+ **lightrag_kwargs,
258
+ )
259
+
260
+ # Fallback to regular completion
261
+ lightrag_kwargs = {
262
+ "system_prompt": system_prompt,
263
+ "history_messages": history_messages or [],
264
+ "api_key": self.config.api_key,
265
+ "base_url": self.config.base_url,
266
+ **kwargs,
267
+ }
268
+ if api_version:
269
+ lightrag_kwargs["api_version"] = api_version
270
+ return openai_complete_if_cache(
271
+ self.config.model,
272
+ prompt,
273
+ **lightrag_kwargs,
274
+ )
275
+
276
+ return vision_model_func
277
+
278
+
279
+ # Singleton instance
280
+ _client: Optional[LLMClient] = None
281
+
282
+
283
+ def get_llm_client(config: Optional[LLMConfig] = None) -> LLMClient:
284
+ """
285
+ Get or create the singleton LLM client.
286
+
287
+ Args:
288
+ config: Optional configuration. Only used on first call.
289
+
290
+ Returns:
291
+ LLMClient instance
292
+ """
293
+ global _client
294
+ if _client is None:
295
+ _client = LLMClient(config)
296
+ return _client
297
+
298
+
299
+ def reset_llm_client():
300
+ """Reset the singleton LLM client."""
301
+ global _client
302
+ _client = None