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.
- realtimex_deeptutor/__init__.py +67 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/METADATA +1612 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/RECORD +276 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/WHEEL +5 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/entry_points.txt +2 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/licenses/LICENSE +661 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/top_level.txt +2 -0
- src/__init__.py +40 -0
- src/agents/__init__.py +24 -0
- src/agents/base_agent.py +657 -0
- src/agents/chat/__init__.py +24 -0
- src/agents/chat/chat_agent.py +435 -0
- src/agents/chat/prompts/en/chat_agent.yaml +35 -0
- src/agents/chat/prompts/zh/chat_agent.yaml +35 -0
- src/agents/chat/session_manager.py +311 -0
- src/agents/co_writer/__init__.py +0 -0
- src/agents/co_writer/edit_agent.py +260 -0
- src/agents/co_writer/narrator_agent.py +423 -0
- src/agents/co_writer/prompts/en/edit_agent.yaml +113 -0
- src/agents/co_writer/prompts/en/narrator_agent.yaml +88 -0
- src/agents/co_writer/prompts/zh/edit_agent.yaml +113 -0
- src/agents/co_writer/prompts/zh/narrator_agent.yaml +88 -0
- src/agents/guide/__init__.py +16 -0
- src/agents/guide/agents/__init__.py +11 -0
- src/agents/guide/agents/chat_agent.py +104 -0
- src/agents/guide/agents/interactive_agent.py +223 -0
- src/agents/guide/agents/locate_agent.py +149 -0
- src/agents/guide/agents/summary_agent.py +150 -0
- src/agents/guide/guide_manager.py +500 -0
- src/agents/guide/prompts/en/chat_agent.yaml +41 -0
- src/agents/guide/prompts/en/interactive_agent.yaml +202 -0
- src/agents/guide/prompts/en/locate_agent.yaml +68 -0
- src/agents/guide/prompts/en/summary_agent.yaml +157 -0
- src/agents/guide/prompts/zh/chat_agent.yaml +41 -0
- src/agents/guide/prompts/zh/interactive_agent.yaml +626 -0
- src/agents/guide/prompts/zh/locate_agent.yaml +68 -0
- src/agents/guide/prompts/zh/summary_agent.yaml +157 -0
- src/agents/ideagen/__init__.py +12 -0
- src/agents/ideagen/idea_generation_workflow.py +426 -0
- src/agents/ideagen/material_organizer_agent.py +173 -0
- src/agents/ideagen/prompts/en/idea_generation.yaml +187 -0
- src/agents/ideagen/prompts/en/material_organizer.yaml +69 -0
- src/agents/ideagen/prompts/zh/idea_generation.yaml +187 -0
- src/agents/ideagen/prompts/zh/material_organizer.yaml +69 -0
- src/agents/question/__init__.py +24 -0
- src/agents/question/agents/__init__.py +18 -0
- src/agents/question/agents/generate_agent.py +381 -0
- src/agents/question/agents/relevance_analyzer.py +207 -0
- src/agents/question/agents/retrieve_agent.py +239 -0
- src/agents/question/coordinator.py +718 -0
- src/agents/question/example.py +109 -0
- src/agents/question/prompts/en/coordinator.yaml +75 -0
- src/agents/question/prompts/en/generate_agent.yaml +77 -0
- src/agents/question/prompts/en/relevance_analyzer.yaml +41 -0
- src/agents/question/prompts/en/retrieve_agent.yaml +32 -0
- src/agents/question/prompts/zh/coordinator.yaml +75 -0
- src/agents/question/prompts/zh/generate_agent.yaml +77 -0
- src/agents/question/prompts/zh/relevance_analyzer.yaml +39 -0
- src/agents/question/prompts/zh/retrieve_agent.yaml +30 -0
- src/agents/research/agents/__init__.py +23 -0
- src/agents/research/agents/decompose_agent.py +507 -0
- src/agents/research/agents/manager_agent.py +228 -0
- src/agents/research/agents/note_agent.py +180 -0
- src/agents/research/agents/rephrase_agent.py +263 -0
- src/agents/research/agents/reporting_agent.py +1333 -0
- src/agents/research/agents/research_agent.py +714 -0
- src/agents/research/data_structures.py +451 -0
- src/agents/research/main.py +188 -0
- src/agents/research/prompts/en/decompose_agent.yaml +89 -0
- src/agents/research/prompts/en/manager_agent.yaml +24 -0
- src/agents/research/prompts/en/note_agent.yaml +121 -0
- src/agents/research/prompts/en/rephrase_agent.yaml +58 -0
- src/agents/research/prompts/en/reporting_agent.yaml +380 -0
- src/agents/research/prompts/en/research_agent.yaml +173 -0
- src/agents/research/prompts/zh/decompose_agent.yaml +89 -0
- src/agents/research/prompts/zh/manager_agent.yaml +24 -0
- src/agents/research/prompts/zh/note_agent.yaml +121 -0
- src/agents/research/prompts/zh/rephrase_agent.yaml +58 -0
- src/agents/research/prompts/zh/reporting_agent.yaml +380 -0
- src/agents/research/prompts/zh/research_agent.yaml +173 -0
- src/agents/research/research_pipeline.py +1309 -0
- src/agents/research/utils/__init__.py +60 -0
- src/agents/research/utils/citation_manager.py +799 -0
- src/agents/research/utils/json_utils.py +98 -0
- src/agents/research/utils/token_tracker.py +297 -0
- src/agents/solve/__init__.py +80 -0
- src/agents/solve/analysis_loop/__init__.py +14 -0
- src/agents/solve/analysis_loop/investigate_agent.py +414 -0
- src/agents/solve/analysis_loop/note_agent.py +190 -0
- src/agents/solve/main_solver.py +862 -0
- src/agents/solve/memory/__init__.py +34 -0
- src/agents/solve/memory/citation_memory.py +353 -0
- src/agents/solve/memory/investigate_memory.py +226 -0
- src/agents/solve/memory/solve_memory.py +340 -0
- src/agents/solve/prompts/en/analysis_loop/investigate_agent.yaml +55 -0
- src/agents/solve/prompts/en/analysis_loop/note_agent.yaml +54 -0
- src/agents/solve/prompts/en/solve_loop/manager_agent.yaml +67 -0
- src/agents/solve/prompts/en/solve_loop/precision_answer_agent.yaml +62 -0
- src/agents/solve/prompts/en/solve_loop/response_agent.yaml +90 -0
- src/agents/solve/prompts/en/solve_loop/solve_agent.yaml +75 -0
- src/agents/solve/prompts/en/solve_loop/tool_agent.yaml +38 -0
- src/agents/solve/prompts/zh/analysis_loop/investigate_agent.yaml +53 -0
- src/agents/solve/prompts/zh/analysis_loop/note_agent.yaml +54 -0
- src/agents/solve/prompts/zh/solve_loop/manager_agent.yaml +66 -0
- src/agents/solve/prompts/zh/solve_loop/precision_answer_agent.yaml +62 -0
- src/agents/solve/prompts/zh/solve_loop/response_agent.yaml +90 -0
- src/agents/solve/prompts/zh/solve_loop/solve_agent.yaml +76 -0
- src/agents/solve/prompts/zh/solve_loop/tool_agent.yaml +41 -0
- src/agents/solve/solve_loop/__init__.py +22 -0
- src/agents/solve/solve_loop/citation_manager.py +74 -0
- src/agents/solve/solve_loop/manager_agent.py +274 -0
- src/agents/solve/solve_loop/precision_answer_agent.py +96 -0
- src/agents/solve/solve_loop/response_agent.py +301 -0
- src/agents/solve/solve_loop/solve_agent.py +325 -0
- src/agents/solve/solve_loop/tool_agent.py +470 -0
- src/agents/solve/utils/__init__.py +64 -0
- src/agents/solve/utils/config_validator.py +313 -0
- src/agents/solve/utils/display_manager.py +223 -0
- src/agents/solve/utils/error_handler.py +363 -0
- src/agents/solve/utils/json_utils.py +98 -0
- src/agents/solve/utils/performance_monitor.py +407 -0
- src/agents/solve/utils/token_tracker.py +541 -0
- src/api/__init__.py +0 -0
- src/api/main.py +240 -0
- src/api/routers/__init__.py +1 -0
- src/api/routers/agent_config.py +69 -0
- src/api/routers/chat.py +296 -0
- src/api/routers/co_writer.py +337 -0
- src/api/routers/config.py +627 -0
- src/api/routers/dashboard.py +18 -0
- src/api/routers/guide.py +337 -0
- src/api/routers/ideagen.py +436 -0
- src/api/routers/knowledge.py +821 -0
- src/api/routers/notebook.py +247 -0
- src/api/routers/question.py +537 -0
- src/api/routers/research.py +394 -0
- src/api/routers/settings.py +164 -0
- src/api/routers/solve.py +305 -0
- src/api/routers/system.py +252 -0
- src/api/run_server.py +61 -0
- src/api/utils/history.py +172 -0
- src/api/utils/log_interceptor.py +21 -0
- src/api/utils/notebook_manager.py +415 -0
- src/api/utils/progress_broadcaster.py +72 -0
- src/api/utils/task_id_manager.py +100 -0
- src/config/__init__.py +0 -0
- src/config/accessors.py +18 -0
- src/config/constants.py +34 -0
- src/config/defaults.py +18 -0
- src/config/schema.py +38 -0
- src/config/settings.py +50 -0
- src/core/errors.py +62 -0
- src/knowledge/__init__.py +23 -0
- src/knowledge/add_documents.py +606 -0
- src/knowledge/config.py +65 -0
- src/knowledge/example_add_documents.py +236 -0
- src/knowledge/extract_numbered_items.py +1039 -0
- src/knowledge/initializer.py +621 -0
- src/knowledge/kb.py +22 -0
- src/knowledge/manager.py +782 -0
- src/knowledge/progress_tracker.py +182 -0
- src/knowledge/start_kb.py +535 -0
- src/logging/__init__.py +103 -0
- src/logging/adapters/__init__.py +17 -0
- src/logging/adapters/lightrag.py +184 -0
- src/logging/adapters/llamaindex.py +141 -0
- src/logging/config.py +80 -0
- src/logging/handlers/__init__.py +20 -0
- src/logging/handlers/console.py +75 -0
- src/logging/handlers/file.py +201 -0
- src/logging/handlers/websocket.py +127 -0
- src/logging/logger.py +709 -0
- src/logging/stats/__init__.py +16 -0
- src/logging/stats/llm_stats.py +179 -0
- src/services/__init__.py +56 -0
- src/services/config/__init__.py +61 -0
- src/services/config/knowledge_base_config.py +210 -0
- src/services/config/loader.py +260 -0
- src/services/config/unified_config.py +603 -0
- src/services/embedding/__init__.py +45 -0
- src/services/embedding/adapters/__init__.py +22 -0
- src/services/embedding/adapters/base.py +106 -0
- src/services/embedding/adapters/cohere.py +127 -0
- src/services/embedding/adapters/jina.py +99 -0
- src/services/embedding/adapters/ollama.py +116 -0
- src/services/embedding/adapters/openai_compatible.py +96 -0
- src/services/embedding/client.py +159 -0
- src/services/embedding/config.py +156 -0
- src/services/embedding/provider.py +119 -0
- src/services/llm/__init__.py +152 -0
- src/services/llm/capabilities.py +313 -0
- src/services/llm/client.py +302 -0
- src/services/llm/cloud_provider.py +530 -0
- src/services/llm/config.py +200 -0
- src/services/llm/error_mapping.py +103 -0
- src/services/llm/exceptions.py +152 -0
- src/services/llm/factory.py +450 -0
- src/services/llm/local_provider.py +347 -0
- src/services/llm/providers/anthropic.py +95 -0
- src/services/llm/providers/base_provider.py +93 -0
- src/services/llm/providers/open_ai.py +83 -0
- src/services/llm/registry.py +71 -0
- src/services/llm/telemetry.py +40 -0
- src/services/llm/types.py +27 -0
- src/services/llm/utils.py +333 -0
- src/services/prompt/__init__.py +25 -0
- src/services/prompt/manager.py +206 -0
- src/services/rag/__init__.py +64 -0
- src/services/rag/components/__init__.py +29 -0
- src/services/rag/components/base.py +59 -0
- src/services/rag/components/chunkers/__init__.py +18 -0
- src/services/rag/components/chunkers/base.py +34 -0
- src/services/rag/components/chunkers/fixed.py +71 -0
- src/services/rag/components/chunkers/numbered_item.py +94 -0
- src/services/rag/components/chunkers/semantic.py +97 -0
- src/services/rag/components/embedders/__init__.py +14 -0
- src/services/rag/components/embedders/base.py +32 -0
- src/services/rag/components/embedders/openai.py +63 -0
- src/services/rag/components/indexers/__init__.py +18 -0
- src/services/rag/components/indexers/base.py +35 -0
- src/services/rag/components/indexers/graph.py +172 -0
- src/services/rag/components/indexers/lightrag.py +156 -0
- src/services/rag/components/indexers/vector.py +146 -0
- src/services/rag/components/parsers/__init__.py +18 -0
- src/services/rag/components/parsers/base.py +35 -0
- src/services/rag/components/parsers/markdown.py +52 -0
- src/services/rag/components/parsers/pdf.py +115 -0
- src/services/rag/components/parsers/text.py +86 -0
- src/services/rag/components/retrievers/__init__.py +18 -0
- src/services/rag/components/retrievers/base.py +34 -0
- src/services/rag/components/retrievers/dense.py +200 -0
- src/services/rag/components/retrievers/hybrid.py +164 -0
- src/services/rag/components/retrievers/lightrag.py +169 -0
- src/services/rag/components/routing.py +286 -0
- src/services/rag/factory.py +234 -0
- src/services/rag/pipeline.py +215 -0
- src/services/rag/pipelines/__init__.py +32 -0
- src/services/rag/pipelines/academic.py +44 -0
- src/services/rag/pipelines/lightrag.py +43 -0
- src/services/rag/pipelines/llamaindex.py +313 -0
- src/services/rag/pipelines/raganything.py +384 -0
- src/services/rag/service.py +244 -0
- src/services/rag/types.py +73 -0
- src/services/search/__init__.py +284 -0
- src/services/search/base.py +87 -0
- src/services/search/consolidation.py +398 -0
- src/services/search/providers/__init__.py +128 -0
- src/services/search/providers/baidu.py +188 -0
- src/services/search/providers/exa.py +194 -0
- src/services/search/providers/jina.py +161 -0
- src/services/search/providers/perplexity.py +153 -0
- src/services/search/providers/serper.py +209 -0
- src/services/search/providers/tavily.py +161 -0
- src/services/search/types.py +114 -0
- src/services/setup/__init__.py +34 -0
- src/services/setup/init.py +285 -0
- src/services/tts/__init__.py +16 -0
- src/services/tts/config.py +99 -0
- src/tools/__init__.py +91 -0
- src/tools/code_executor.py +536 -0
- src/tools/paper_search_tool.py +171 -0
- src/tools/query_item_tool.py +310 -0
- src/tools/question/__init__.py +15 -0
- src/tools/question/exam_mimic.py +616 -0
- src/tools/question/pdf_parser.py +211 -0
- src/tools/question/question_extractor.py +397 -0
- src/tools/rag_tool.py +173 -0
- src/tools/tex_chunker.py +339 -0
- src/tools/tex_downloader.py +253 -0
- src/tools/web_search.py +71 -0
- src/utils/config_manager.py +206 -0
- src/utils/document_validator.py +168 -0
- src/utils/error_rate_tracker.py +111 -0
- src/utils/error_utils.py +82 -0
- src/utils/json_parser.py +110 -0
- 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
|
src/utils/error_utils.py
ADDED
|
@@ -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
|
src/utils/json_parser.py
ADDED
|
@@ -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)
|