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,111 @@
1
+ """
2
+ Error Rate Tracker - Track error rates per provider with alerting.
3
+ """
4
+
5
+ from collections import defaultdict, deque
6
+ import logging
7
+ import threading
8
+ import time
9
+ from typing import Callable, Dict, Optional
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ErrorRateTracker:
15
+ """
16
+ Tracks error rates per provider with sliding window.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ window_size: int = 60,
22
+ threshold: float = 0.5,
23
+ alert_callback: Optional[Callable[[str, float], None]] = None,
24
+ ):
25
+ self.window_size = window_size # seconds
26
+ self.threshold = threshold # failure rate threshold
27
+ self.alert_callback = alert_callback
28
+ self._lock = threading.RLock() # Use RLock to allow reentrant locking
29
+ self._errors: Dict[str, deque[float]] = defaultdict(deque)
30
+ self._total_calls: Dict[str, deque[float]] = defaultdict(deque)
31
+ self._alerted: Dict[str, bool] = defaultdict(bool) # to avoid repeated alerts
32
+
33
+ def record_call(self, provider: str, success: bool):
34
+ """Record a call for the provider."""
35
+ now = time.time()
36
+ with self._lock:
37
+ self._total_calls[provider].append(now)
38
+ if not success:
39
+ self._errors[provider].append(now)
40
+ self._cleanup_old_entries(provider, now)
41
+ self._check_alert(provider)
42
+
43
+ def get_error_rate(self, provider: str) -> float:
44
+ """Get current error rate for provider."""
45
+ now = time.time()
46
+ with self._lock:
47
+ self._cleanup_old_entries(provider, now)
48
+ total = len(self._total_calls[provider])
49
+ errors = len(self._errors[provider])
50
+ return errors / total if total > 0 else 0.0
51
+
52
+ def check_threshold(self, provider: str) -> bool:
53
+ """Check if error rate exceeds threshold."""
54
+ rate = self.get_error_rate(provider)
55
+ return rate > self.threshold
56
+
57
+ def _check_alert(self, provider: str):
58
+ """Check and trigger alert if needed."""
59
+ rate = self.get_error_rate(provider)
60
+ exceeds_threshold = rate > self.threshold
61
+ if exceeds_threshold and not self._alerted[provider]:
62
+ logger.warning(
63
+ f"Provider {provider} error rate {rate:.2%} exceeds threshold {self.threshold:.2%}"
64
+ )
65
+ if self.alert_callback:
66
+ self.alert_callback(provider, rate)
67
+ self._alerted[provider] = True
68
+ elif not exceeds_threshold:
69
+ self._alerted[provider] = False # reset when below threshold
70
+
71
+ def _cleanup_old_entries(self, provider: str, now: float):
72
+ """Remove entries older than window_size."""
73
+ cutoff = now - self.window_size
74
+ while self._total_calls[provider] and self._total_calls[provider][0] <= cutoff:
75
+ self._total_calls[provider].popleft()
76
+ while self._errors[provider] and self._errors[provider][0] <= cutoff:
77
+ self._errors[provider].popleft()
78
+
79
+
80
+ # Global instance
81
+ tracker = ErrorRateTracker()
82
+
83
+ # Set alert callback to circuit breaker
84
+ try:
85
+ from .network.circuit_breaker import alert_callback as cb
86
+
87
+ tracker.alert_callback = cb
88
+ except ImportError as e:
89
+ logging.getLogger(__name__).warning(
90
+ f"Circuit breaker module not available: {e}. Error rate tracking will work but circuit breaker integration is disabled."
91
+ )
92
+
93
+
94
+ def record_provider_call(provider: str, success: bool):
95
+ """Global function to record a call."""
96
+ tracker.record_call(provider, success)
97
+
98
+
99
+ def get_provider_error_rate(provider: str) -> float:
100
+ """Get error rate for provider."""
101
+ return tracker.get_error_rate(provider)
102
+
103
+
104
+ def check_provider_threshold(provider: str) -> bool:
105
+ """Check if provider exceeds threshold."""
106
+ return tracker.check_threshold(provider)
107
+
108
+
109
+ def set_alert_callback(callback: Callable[[str, float], None]):
110
+ """Set the alert callback for the tracker."""
111
+ tracker.alert_callback = callback
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Error Utilities - Error formatting and handling utilities
5
+ """
6
+
7
+ import json
8
+ from typing import Optional
9
+
10
+
11
+ def _find_json_block(message: str) -> Optional[str]:
12
+ """Extract potential JSON block from message by matching braces."""
13
+ start_idx = message.find("{")
14
+ if start_idx == -1:
15
+ return None
16
+
17
+ brace_count = 0
18
+ in_string = False
19
+ escape_next = False
20
+
21
+ for char_idx in range(start_idx, len(message)):
22
+ char = message[char_idx]
23
+
24
+ if escape_next:
25
+ escape_next = False
26
+ continue
27
+
28
+ if char == "\\":
29
+ escape_next = True
30
+ continue
31
+
32
+ if char == '"':
33
+ in_string = not in_string
34
+ continue
35
+
36
+ if not in_string:
37
+ if char == "{":
38
+ brace_count += 1
39
+ elif char == "}":
40
+ brace_count -= 1
41
+ if brace_count == 0:
42
+ return message[start_idx : char_idx + 1]
43
+
44
+ return None
45
+
46
+
47
+ def format_exception_message(exc: Exception) -> str:
48
+ """
49
+ Format exception message for better readability
50
+
51
+ Args:
52
+ exc: The exception to format
53
+
54
+ Returns:
55
+ Formatted error message
56
+ """
57
+ message = str(exc)
58
+
59
+ # Try to parse JSON error messages (common in API errors)
60
+ potential_json = _find_json_block(message)
61
+ if potential_json:
62
+ try:
63
+ error_data = json.loads(potential_json)
64
+
65
+ # Standard extraction logic
66
+ if isinstance(error_data, dict) and "error" in error_data:
67
+ error_info = error_data["error"]
68
+ if isinstance(error_info, dict):
69
+ parts = []
70
+ if "message" in error_info:
71
+ parts.append(f"Message: {error_info['message']}")
72
+ if "type" in error_info:
73
+ parts.append(f"Type: {error_info['type']}")
74
+ if "code" in error_info:
75
+ parts.append(f"Code: {error_info['code']}")
76
+ if parts:
77
+ return " | ".join(parts)
78
+ except (json.JSONDecodeError, AttributeError):
79
+ pass
80
+
81
+ # Return original message if parsing fails
82
+ return message
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Robust JSON parsing utilities with automatic repair and markdown extraction.
5
+
6
+ Provides safe JSON parsing that handles:
7
+ - Markdown code block wrapping (```json...```)
8
+ - Malformed JSON (missing commas, trailing commas, etc.)
9
+ - Unescaped newlines and control characters
10
+ - Empty responses
11
+ """
12
+
13
+ import json
14
+ import logging
15
+ import re
16
+ from typing import Any
17
+
18
+ try:
19
+ from json_repair import repair_json
20
+ except ImportError:
21
+ repair_json = None
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def parse_json_response(
27
+ response: str,
28
+ logger_instance: logging.Logger | None = None,
29
+ fallback: Any = None,
30
+ ) -> Any:
31
+ """
32
+ Safely parse JSON from LLM responses with automatic repair.
33
+
34
+ Implements a three-tier parsing strategy:
35
+ 1. Extract JSON from markdown code blocks if present
36
+ 2. Direct JSON parsing
37
+ 3. Automated repair using json-repair library with fallback
38
+
39
+ Args:
40
+ response: Raw string response from LLM
41
+ logger_instance: Logger instance for debugging (optional)
42
+ fallback: Value to return if all parsing fails (default: {})
43
+
44
+ Returns:
45
+ Parsed JSON object, or fallback value if parsing fails
46
+
47
+ Example:
48
+ >>> response = '```json\\n{"key": "value"}\\n```'
49
+ >>> data = parse_json_response(response)
50
+ >>> data
51
+ {'key': 'value'}
52
+ """
53
+ log = logger_instance or logger
54
+
55
+ if fallback is None:
56
+ fallback = {}
57
+
58
+ # Handle empty response
59
+ if not response or not response.strip():
60
+ log.warning("LLM returned empty response")
61
+ return fallback
62
+
63
+ # Extract from markdown code blocks if present
64
+ extracted_response = response
65
+ if "```" in response:
66
+ json_match = re.search(r"```(?:json)?\s*\n?(.*?)```", response, re.DOTALL)
67
+ if json_match:
68
+ extracted_response = json_match.group(1).strip()
69
+ log.debug("Extracted JSON from markdown code block")
70
+
71
+ # Strategy 1: Direct parsing
72
+ try:
73
+ return json.loads(extracted_response)
74
+ except json.JSONDecodeError as parse_error:
75
+ log.debug(f"Direct JSON parse failed: {parse_error}")
76
+
77
+ # Strategy 2: Try json-repair if available
78
+ if repair_json is None:
79
+ log.warning("json-repair library not installed, cannot repair malformed JSON")
80
+ log.debug(f"Response: {extracted_response[:200]}")
81
+ return fallback
82
+
83
+ try:
84
+ log.debug("Attempting JSON repair")
85
+ repaired = repair_json(extracted_response)
86
+ result = json.loads(repaired)
87
+ log.info("Successfully repaired malformed JSON")
88
+ return result
89
+ except Exception as repair_error:
90
+ log.error(f"JSON repair failed: {repair_error}")
91
+ log.debug(f"Response: {extracted_response[:200]}")
92
+ return fallback
93
+
94
+
95
+ def safe_json_loads(data: str, fallback: Any = None) -> Any:
96
+ """
97
+ Simple wrapper for safe JSON loading.
98
+
99
+ Args:
100
+ data: JSON string
101
+ fallback: Value to return on failure (default: {})
102
+
103
+ Returns:
104
+ Parsed JSON or fallback value
105
+ """
106
+ try:
107
+ return json.loads(data)
108
+ except json.JSONDecodeError as e:
109
+ logger.warning(f"JSON parse error: {e}")
110
+ return fallback if fallback is not None else {}
@@ -0,0 +1,79 @@
1
+ """
2
+ Circuit Breaker - Simple circuit breaker for providers.
3
+ """
4
+
5
+ import logging
6
+ import threading
7
+ import time
8
+ from typing import Dict
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class CircuitBreaker:
14
+ """
15
+ Simple circuit breaker that opens when error rate is high.
16
+ """
17
+
18
+ def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 60):
19
+ self.failure_threshold = failure_threshold
20
+ self.recovery_timeout = recovery_timeout
21
+ self.failure_count: Dict[str, int] = {}
22
+ self.last_failure_time: Dict[str, float] = {}
23
+ self.state: Dict[str, str] = {} # 'closed', 'open', 'half-open'
24
+ self.lock = threading.Lock()
25
+
26
+ def call(self, provider: str) -> bool:
27
+ """Check if call is allowed."""
28
+ with self.lock:
29
+ state = self.state.get(provider, "closed")
30
+ if state == "closed":
31
+ return True
32
+ elif state == "open":
33
+ if time.time() - self.last_failure_time.get(provider, 0) > self.recovery_timeout:
34
+ self.state[provider] = "half-open"
35
+ logger.info(f"Circuit breaker for {provider} entering half-open state")
36
+ return True
37
+ return False
38
+ elif state == "half-open":
39
+ return True
40
+
41
+ def record_success(self, provider: str):
42
+ """Record successful call."""
43
+ with self.lock:
44
+ if self.state.get(provider) == "half-open":
45
+ self.state[provider] = "closed"
46
+ self.failure_count[provider] = 0
47
+ logger.info(f"Circuit breaker for {provider} closed")
48
+ elif self.state.get(provider) == "closed":
49
+ self.failure_count[provider] = 0
50
+
51
+ def record_failure(self, provider: str):
52
+ """Record failed call."""
53
+ with self.lock:
54
+ self.failure_count[provider] = self.failure_count.get(provider, 0) + 1
55
+ self.last_failure_time[provider] = time.time()
56
+ if self.failure_count[provider] >= self.failure_threshold:
57
+ self.state[provider] = "open"
58
+ logger.warning(
59
+ f"Circuit breaker for {provider} opened due to {self.failure_count[provider]} failures"
60
+ )
61
+
62
+
63
+ # Global instance
64
+ circuit_breaker = CircuitBreaker()
65
+
66
+
67
+ def alert_callback(provider: str, rate: float):
68
+ """Alert callback to trigger circuit breaker."""
69
+ circuit_breaker.record_failure(provider)
70
+
71
+
72
+ def is_call_allowed(provider: str) -> bool:
73
+ """Check if call is allowed by circuit breaker."""
74
+ return circuit_breaker.call(provider)
75
+
76
+
77
+ def record_call_success(provider: str):
78
+ """Record successful call."""
79
+ circuit_breaker.record_success(provider)