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,200 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ LLM Configuration
4
+ =================
5
+
6
+ Configuration management for LLM services.
7
+ Simplified version - loads from unified config service or falls back to .env.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ import logging
12
+ import os
13
+ from pathlib import Path
14
+ import re
15
+ from typing import Optional
16
+
17
+ from dotenv import load_dotenv
18
+
19
+ from .exceptions import LLMConfigError
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Load environment variables
24
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
25
+ load_dotenv(PROJECT_ROOT / "DeepTutor.env", override=False)
26
+ load_dotenv(PROJECT_ROOT / ".env", override=False)
27
+
28
+
29
+ @dataclass
30
+ class LLMConfig:
31
+ """LLM configuration dataclass."""
32
+
33
+ model: str
34
+ api_key: str
35
+ base_url: Optional[str] = None
36
+ binding: str = "openai"
37
+ api_version: Optional[str] = None
38
+ max_tokens: int = 4096
39
+ temperature: float = 0.7
40
+
41
+
42
+ def _strip_value(value: Optional[str]) -> Optional[str]:
43
+ """Remove leading/trailing whitespace and quotes from string."""
44
+ if value is None:
45
+ return None
46
+ return value.strip().strip("\"'")
47
+
48
+
49
+ def _get_llm_config_from_env() -> LLMConfig:
50
+ """Get LLM configuration from environment variables."""
51
+ binding = _strip_value(os.getenv("LLM_BINDING", "openai"))
52
+ model = _strip_value(os.getenv("LLM_MODEL"))
53
+ api_key = _strip_value(os.getenv("LLM_API_KEY"))
54
+ base_url = _strip_value(os.getenv("LLM_HOST"))
55
+ api_version = _strip_value(os.getenv("LLM_API_VERSION"))
56
+
57
+ # Validate required configuration
58
+ if not model:
59
+ raise LLMConfigError(
60
+ "LLM_MODEL not set, please configure it in .env file or add a configuration in Settings"
61
+ )
62
+ if not base_url:
63
+ raise LLMConfigError(
64
+ "LLM_HOST not set, please configure it in .env file or add a configuration in Settings"
65
+ )
66
+
67
+ return LLMConfig(
68
+ binding=binding,
69
+ model=model,
70
+ api_key=api_key or "",
71
+ base_url=base_url,
72
+ api_version=api_version,
73
+ )
74
+
75
+
76
+ def get_llm_config() -> LLMConfig:
77
+ """
78
+ Load LLM configuration.
79
+
80
+ Priority:
81
+ 1. Active configuration from unified config service
82
+ 2. Environment variables (.env)
83
+
84
+ Returns:
85
+ LLMConfig: Configuration dataclass
86
+
87
+ Raises:
88
+ LLMConfigError: If required configuration is missing
89
+ """
90
+ # 1. Try to get active config from unified config service
91
+ try:
92
+ from src.services.config import get_active_llm_config
93
+
94
+ config = get_active_llm_config()
95
+ if config:
96
+ return LLMConfig(
97
+ binding=config.get("provider", "openai"),
98
+ model=config["model"],
99
+ api_key=config.get("api_key", ""),
100
+ base_url=config.get("base_url"),
101
+ api_version=config.get("api_version"),
102
+ )
103
+ except ImportError:
104
+ # Unified config service not yet available, fall back to env
105
+ pass
106
+ except Exception as e:
107
+ logger.warning(f"Failed to load from unified config: {e}")
108
+
109
+ # 2. Fallback to environment variables
110
+ return _get_llm_config_from_env()
111
+
112
+
113
+ async def get_llm_config_async() -> LLMConfig:
114
+ """
115
+ Async version of get_llm_config for non-blocking configuration loading.
116
+
117
+ Returns:
118
+ LLMConfig: Configuration dataclass
119
+
120
+ Raises:
121
+ LLMConfigError: If required configuration is missing
122
+ """
123
+ # 1. Try to get active config from unified config service
124
+ try:
125
+ from src.services.config import get_active_llm_config
126
+
127
+ config = get_active_llm_config()
128
+ if config:
129
+ return LLMConfig(
130
+ binding=config.get("provider", "openai"),
131
+ model=config["model"],
132
+ api_key=config.get("api_key", ""),
133
+ base_url=config.get("base_url"),
134
+ api_version=config.get("api_version"),
135
+ )
136
+ except ImportError:
137
+ pass
138
+ except Exception as e:
139
+ logger.warning(f"Failed to load from unified config: {e}")
140
+
141
+ # 2. Fallback to environment variables
142
+ return _get_llm_config_from_env()
143
+
144
+
145
+ def uses_max_completion_tokens(model: str) -> bool:
146
+ """
147
+ Check if the model uses max_completion_tokens instead of max_tokens.
148
+
149
+ Newer OpenAI models (o1, o3, gpt-4o, gpt-5.x, etc.) require max_completion_tokens
150
+ while older models use max_tokens.
151
+
152
+ Args:
153
+ model: The model name
154
+
155
+ Returns:
156
+ True if the model requires max_completion_tokens, False otherwise
157
+ """
158
+ model_lower = model.lower()
159
+
160
+ # Models that require max_completion_tokens:
161
+ # - o1, o3 series (reasoning models)
162
+ # - gpt-4o series
163
+ # - gpt-5.x and later
164
+ patterns = [
165
+ r"^o[13]", # o1, o3 models
166
+ r"^gpt-4o", # gpt-4o models
167
+ r"^gpt-[5-9]", # gpt-5.x and later
168
+ r"^gpt-\d{2,}", # gpt-10+ (future proofing)
169
+ ]
170
+
171
+ for pattern in patterns:
172
+ if re.match(pattern, model_lower):
173
+ return True
174
+
175
+ return False
176
+
177
+
178
+ def get_token_limit_kwargs(model: str, max_tokens: int) -> dict:
179
+ """
180
+ Get the appropriate token limit parameter for the model.
181
+
182
+ Args:
183
+ model: The model name
184
+ max_tokens: The desired token limit
185
+
186
+ Returns:
187
+ Dictionary with either {"max_tokens": value} or {"max_completion_tokens": value}
188
+ """
189
+ if uses_max_completion_tokens(model):
190
+ return {"max_completion_tokens": max_tokens}
191
+ return {"max_tokens": max_tokens}
192
+
193
+
194
+ __all__ = [
195
+ "LLMConfig",
196
+ "get_llm_config",
197
+ "get_llm_config_async",
198
+ "uses_max_completion_tokens",
199
+ "get_token_limit_kwargs",
200
+ ]
@@ -0,0 +1,103 @@
1
+ """
2
+ Error Mapping - Map provider-specific errors to unified exceptions.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ import logging
9
+ from typing import Callable, List, Optional, Type
10
+
11
+ # Import unified exceptions from exceptions.py
12
+ from .exceptions import (
13
+ LLMAPIError,
14
+ LLMAuthenticationError,
15
+ LLMError,
16
+ LLMRateLimitError,
17
+ ProviderContextWindowError,
18
+ )
19
+
20
+ try:
21
+ import openai # type: ignore
22
+
23
+ _HAS_OPENAI = True
24
+ except ImportError: # pragma: no cover
25
+ openai = None # type: ignore
26
+ _HAS_OPENAI = False
27
+
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ ErrorClassifier = Callable[[Exception], bool]
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class MappingRule:
37
+ classifier: ErrorClassifier
38
+ factory: Callable[[Exception, Optional[str]], LLMError]
39
+
40
+
41
+ def _instance_of(*types: Type[BaseException]) -> ErrorClassifier:
42
+ return lambda exc: isinstance(exc, types)
43
+
44
+
45
+ def _message_contains(*needles: str) -> ErrorClassifier:
46
+ def _classifier(exc: Exception) -> bool:
47
+ msg = str(exc).lower()
48
+ return any(needle in msg for needle in needles)
49
+
50
+ return _classifier
51
+
52
+
53
+ _GLOBAL_RULES: List[MappingRule] = [
54
+ MappingRule(
55
+ classifier=_message_contains("rate limit", "429", "quota"),
56
+ factory=lambda exc, provider: LLMRateLimitError(str(exc), provider=provider),
57
+ ),
58
+ MappingRule(
59
+ classifier=_message_contains("context length", "maximum context"),
60
+ factory=lambda exc, provider: ProviderContextWindowError(str(exc), provider=provider),
61
+ ),
62
+ ]
63
+
64
+ if _HAS_OPENAI:
65
+ _GLOBAL_RULES[:0] = [
66
+ MappingRule(
67
+ classifier=_instance_of(openai.AuthenticationError),
68
+ factory=lambda exc, provider: LLMAuthenticationError(str(exc), provider=provider),
69
+ ),
70
+ MappingRule(
71
+ classifier=_instance_of(openai.RateLimitError),
72
+ factory=lambda exc, provider: LLMRateLimitError(str(exc), provider=provider),
73
+ ),
74
+ ]
75
+
76
+ # Attempt to load Anthropic and Google rules if SDKs are present
77
+ try:
78
+ import anthropic
79
+
80
+ _GLOBAL_RULES.append(
81
+ MappingRule(
82
+ classifier=_instance_of(anthropic.RateLimitError),
83
+ factory=lambda exc, provider: LLMRateLimitError(str(exc), provider=provider),
84
+ )
85
+ )
86
+ except ImportError:
87
+ pass
88
+
89
+
90
+ def map_error(exc: Exception, provider: Optional[str] = None) -> LLMError:
91
+ """Map provider-specific errors to unified internal exceptions."""
92
+ # Heuristic check for status codes before rules
93
+ status_code = getattr(exc, "status_code", None)
94
+ if status_code == 401:
95
+ return LLMAuthenticationError(str(exc), provider=provider)
96
+ if status_code == 429:
97
+ return LLMRateLimitError(str(exc), provider=provider)
98
+
99
+ for rule in _GLOBAL_RULES:
100
+ if rule.classifier(exc):
101
+ return rule.factory(exc, provider)
102
+
103
+ return LLMAPIError(str(exc), status_code=status_code, provider=provider)
@@ -0,0 +1,152 @@
1
+ """
2
+ LLM Service Exceptions
3
+ ======================
4
+
5
+ Custom exception classes for the LLM service.
6
+ Provides a consistent exception hierarchy for better error handling.
7
+ Maintains parity with upstream dev branch.
8
+ """
9
+
10
+ from typing import Any, Dict, Optional
11
+
12
+
13
+ class LLMError(Exception):
14
+ """Base exception for all LLM-related errors."""
15
+
16
+ def __init__(
17
+ self, message: str, details: Optional[Dict[str, Any]] = None, provider: Optional[str] = None
18
+ ):
19
+ super().__init__(message)
20
+ self.message = message
21
+ self.details = details or {}
22
+ self.provider = provider
23
+
24
+ def __str__(self) -> str:
25
+ provider_prefix = f"[{self.provider}] " if self.provider else ""
26
+ if self.details:
27
+ return f"{provider_prefix}{self.message} (details: {self.details})"
28
+ return f"{provider_prefix}{self.message}"
29
+
30
+
31
+ class LLMConfigError(LLMError):
32
+ """Raised when there's an error in LLM configuration."""
33
+
34
+ pass
35
+
36
+
37
+ class LLMProviderError(LLMError):
38
+ """Raised when there's an error with the LLM provider."""
39
+
40
+ pass
41
+
42
+
43
+ class LLMAPIError(LLMError):
44
+ """
45
+ Raised when an API call to an LLM provider fails.
46
+ Standardizes status_code and provider name.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ message: str,
52
+ status_code: Optional[int] = None,
53
+ provider: Optional[str] = None,
54
+ details: Optional[Dict[str, Any]] = None,
55
+ ):
56
+ super().__init__(message, details, provider)
57
+ self.status_code = status_code
58
+
59
+ def __str__(self) -> str:
60
+ parts = []
61
+ if self.provider:
62
+ parts.append(f"[{self.provider}]")
63
+ if self.status_code:
64
+ parts.append(f"HTTP {self.status_code}")
65
+ parts.append(self.message)
66
+ return " ".join(parts)
67
+
68
+
69
+ class LLMTimeoutError(LLMAPIError):
70
+ """Raised when an API call times out."""
71
+
72
+ def __init__(
73
+ self,
74
+ message: str = "Request timed out",
75
+ timeout: Optional[float] = None,
76
+ provider: Optional[str] = None,
77
+ ):
78
+ super().__init__(message, status_code=408, provider=provider)
79
+ self.timeout = timeout
80
+
81
+
82
+ class LLMRateLimitError(LLMAPIError):
83
+ """Raised when rate limited by the API."""
84
+
85
+ def __init__(
86
+ self,
87
+ message: str = "Rate limit exceeded",
88
+ retry_after: Optional[float] = None,
89
+ provider: Optional[str] = None,
90
+ ):
91
+ super().__init__(message, status_code=429, provider=provider)
92
+ self.retry_after = retry_after
93
+
94
+
95
+ class LLMAuthenticationError(LLMAPIError):
96
+ """Raised when authentication fails (invalid API key, etc.)."""
97
+
98
+ def __init__(
99
+ self,
100
+ message: str = "Authentication failed",
101
+ provider: Optional[str] = None,
102
+ ):
103
+ super().__init__(message, status_code=401, provider=provider)
104
+
105
+
106
+ class LLMModelNotFoundError(LLMAPIError):
107
+ """Raised when the requested model is not found."""
108
+
109
+ def __init__(
110
+ self,
111
+ message: str = "Model not found",
112
+ model: Optional[str] = None,
113
+ provider: Optional[str] = None,
114
+ ):
115
+ super().__init__(message, status_code=404, provider=provider)
116
+ self.model = model
117
+
118
+
119
+ class LLMParseError(LLMError):
120
+ """Raised when parsing LLM output fails."""
121
+
122
+ def __init__(
123
+ self,
124
+ message: str = "Failed to parse LLM output",
125
+ provider: Optional[str] = None,
126
+ details: Optional[Dict[str, Any]] = None,
127
+ ):
128
+ super().__init__(message, details=details, provider=provider)
129
+
130
+
131
+ # Multi-provider specific aliases for mapping rules
132
+ class ProviderQuotaExceededError(LLMRateLimitError):
133
+ pass
134
+
135
+
136
+ class ProviderContextWindowError(LLMAPIError):
137
+ pass
138
+
139
+
140
+ __all__ = [
141
+ "LLMError",
142
+ "LLMConfigError",
143
+ "LLMProviderError",
144
+ "LLMAPIError",
145
+ "LLMTimeoutError",
146
+ "LLMRateLimitError",
147
+ "LLMAuthenticationError",
148
+ "LLMModelNotFoundError",
149
+ "LLMParseError",
150
+ "ProviderQuotaExceededError",
151
+ "ProviderContextWindowError",
152
+ ]