Kevinshh commited on
Commit
5af7d8e
·
verified ·
1 Parent(s): 554c704

Upload chart_executor.py

Browse files
Files changed (1) hide show
  1. utils/chart_executor.py +170 -0
utils/chart_executor.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Code Executor for LLM-Generated Visualization Code
3
+
4
+ Safely executes matplotlib code generated by LLM to produce charts.
5
+ Converts matplotlib figures to Base64 for HTML embedding.
6
+ """
7
+
8
+ import sys
9
+ import io
10
+ import base64
11
+ import traceback
12
+ from typing import Dict, Optional, Any
13
+ import warnings
14
+
15
+
16
+ class ChartExecutor:
17
+ """
18
+ Executes LLM-generated matplotlib code and returns Base64 images.
19
+ """
20
+
21
+ def __init__(self):
22
+ self._setup_matplotlib()
23
+
24
+ def _setup_matplotlib(self):
25
+ """Setup matplotlib for non-interactive use."""
26
+ try:
27
+ import matplotlib
28
+ matplotlib.use('Agg')
29
+ import matplotlib.pyplot as plt
30
+ plt.rcParams['font.family'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans', 'sans-serif']
31
+ plt.rcParams['axes.unicode_minus'] = False
32
+ self.plt = plt
33
+ self.np = __import__('numpy')
34
+ self.available = True
35
+ except ImportError as e:
36
+ warnings.warn(f"Matplotlib not available: {e}")
37
+ self.available = False
38
+
39
+ def execute_chart_code(self, code: str, chart_name: str = "chart") -> Dict[str, Any]:
40
+ """
41
+ Execute matplotlib code and return Base64 image.
42
+
43
+ Args:
44
+ code: Python code string containing matplotlib commands
45
+ chart_name: Name for the chart
46
+
47
+ Returns:
48
+ Dict with 'success', 'image_base64' or 'error'
49
+ """
50
+ if not self.available:
51
+ return {
52
+ "success": False,
53
+ "error": "Matplotlib not available",
54
+ "image_base64": None
55
+ }
56
+
57
+ try:
58
+ # Create isolated namespace
59
+ namespace = {
60
+ 'plt': self.plt,
61
+ 'np': self.np,
62
+ 'matplotlib': __import__('matplotlib'),
63
+ }
64
+
65
+ # Redirect savefig to capture output
66
+ buf = io.BytesIO()
67
+
68
+ # Modify code to save to buffer instead of file
69
+ modified_code = self._modify_code_for_buffer(code)
70
+
71
+ # Execute code
72
+ exec(modified_code, namespace)
73
+
74
+ # Get the current figure
75
+ fig = self.plt.gcf()
76
+
77
+ # Save to buffer
78
+ fig.savefig(buf, format='png', dpi=150, bbox_inches='tight',
79
+ facecolor='white', edgecolor='none')
80
+ buf.seek(0)
81
+
82
+ # Convert to Base64
83
+ img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
84
+
85
+ # Clean up
86
+ self.plt.close('all')
87
+
88
+ return {
89
+ "success": True,
90
+ "image_base64": f"data:image/png;base64,{img_base64}",
91
+ "chart_name": chart_name
92
+ }
93
+
94
+ except Exception as e:
95
+ self.plt.close('all')
96
+ return {
97
+ "success": False,
98
+ "error": str(e),
99
+ "traceback": traceback.format_exc(),
100
+ "image_base64": None
101
+ }
102
+
103
+ def _modify_code_for_buffer(self, code: str) -> str:
104
+ """Remove savefig and show commands from code."""
105
+ lines = code.split('\n')
106
+ modified_lines = []
107
+
108
+ for line in lines:
109
+ # Skip savefig and show commands
110
+ stripped = line.strip()
111
+ if stripped.startswith('plt.savefig') or stripped.startswith('plt.show'):
112
+ continue
113
+ if 'savefig(' in stripped or '.show(' in stripped:
114
+ continue
115
+ modified_lines.append(line)
116
+
117
+ return '\n'.join(modified_lines)
118
+
119
+ def execute_multiple_charts(self, code_dict: Dict[str, str]) -> Dict[str, Dict]:
120
+ """
121
+ Execute multiple chart codes.
122
+
123
+ Args:
124
+ code_dict: Dict mapping chart names to code strings
125
+
126
+ Returns:
127
+ Dict mapping chart names to execution results
128
+ """
129
+ results = {}
130
+
131
+ for name, code in code_dict.items():
132
+ if code and isinstance(code, str) and 'plt' in code:
133
+ results[name] = self.execute_chart_code(code, name)
134
+ else:
135
+ results[name] = {
136
+ "success": False,
137
+ "error": "Invalid or empty code",
138
+ "image_base64": None
139
+ }
140
+
141
+ return results
142
+
143
+
144
+ def generate_fallback_chart_html(chart_name: str, description: str) -> str:
145
+ """
146
+ Generate a placeholder HTML when chart generation fails.
147
+ """
148
+ return f'''
149
+ <div style="border:2px dashed #ccc; padding:40px; text-align:center;
150
+ background:#f9f9f9; border-radius:8px; margin:15px 0;">
151
+ <div style="font-size:48px; color:#ccc;">📊</div>
152
+ <div style="font-size:16px; color:#666; margin-top:10px;">
153
+ {chart_name}
154
+ </div>
155
+ <div style="font-size:12px; color:#999; margin-top:5px;">
156
+ {description}
157
+ </div>
158
+ </div>
159
+ '''
160
+
161
+
162
+ # Singleton instance
163
+ _executor = None
164
+
165
+ def get_executor() -> ChartExecutor:
166
+ """Get or create the chart executor singleton."""
167
+ global _executor
168
+ if _executor is None:
169
+ _executor = ChartExecutor()
170
+ return _executor