Skip to content

Commit 3d4449b

Browse files
committed
feat: enhance REPL functionality with improved plot capture and download utilities
1. support_tools.py -- Replaced the standalone _captured_plots list, _capture_matplotlib_plots(), and _apply_matplotlib_patches() (~90 lines) with a single module-level _global_plot_capture = PlotCapture() instance. The backward-compat functions get_captured_plots()/clear_captured_plots() now delegate to it. No more duplicated capture logic between the class and the old module-level functions. 2. execution.py -- Extracted _run_script_in_tempfile() shared helper. run_r_code and run_bash_script had identical temp-file-write → subprocess → cleanup boilerplate. Also consolidated _LANGUAGE_MARKERS dict as single source of truth for both detect_code_language() and strip_code_markers(). 3. download.py -- Extracted _download_file() to replace the inline download_with_progress() closure that was duplicated inside check_and_download_s3_files, and the near-identical download code in download_and_unzip. Also improved temp file cleanup with finally blocks. 4. formatting.py -- Replaced 20-line manual word-wrapping with textwrap.fill(). Removed a stray print(message) debug statement. 5. tool_bridge.py -- Replaced per-key loop with namespace.update(). Removed the dead builtins._BaseAgent_custom_functions hack (no callers). 6. __init__.py -- Stopped re-exporting private helpers _parse_docstring/_type_to_string. Tests import them directly from schema.py, so no breakage. One minor behavioral change (non-breaking): run_bash_script no longer passes shell=True to subprocess.run. Since the script has a #!/bin/bash shebang and is chmod 755, direct execution is actually more correct. The old env/cwd explicit args were also redundant since subprocess.run inherits them from the parent process.
1 parent 1bb0112 commit 3d4449b

6 files changed

Lines changed: 161 additions & 298 deletions

File tree

BaseAgent/tools/support_tools.py

Lines changed: 14 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import base64
2+
import importlib
3+
import inspect
24
import io
35
import sys
46
from io import StringIO
57

68
# Create a persistent namespace that will be shared across all executions
79
_persistent_namespace = {}
810

9-
# Global list to store captured plots (module-level fallback for backward compat)
10-
_captured_plots = []
11-
1211

1312
class PlotCapture:
1413
"""Per-agent matplotlib plot capture with isolated state.
@@ -101,6 +100,11 @@ def clear(self) -> None:
101100
self._plots = []
102101

103102

103+
# Module-level PlotCapture for backward-compat callers that use run_python_repl
104+
# without supplying a namespace (i.e., direct callers, not BaseAgent instances).
105+
_global_plot_capture = PlotCapture()
106+
107+
104108
def run_python_repl(command: str, namespace: dict | None = None) -> str:
105109
"""
106110
Executes the provided Python command in a persistent environment and returns the output.
@@ -128,7 +132,7 @@ def execute_in_repl(command: str) -> str:
128132
# (backward compat). Per-instance callers apply PlotCapture.apply_patches()
129133
# themselves before invoking run_python_repl.
130134
if namespace is None:
131-
_apply_matplotlib_patches()
135+
_global_plot_capture.apply_patches()
132136

133137
exec(command, _ns)
134138
output = mystdout.getvalue()
@@ -143,109 +147,18 @@ def execute_in_repl(command: str) -> str:
143147
return execute_in_repl(command)
144148

145149

146-
def _capture_matplotlib_plots():
147-
"""
148-
Capture any matplotlib plots that might have been generated during execution.
149-
"""
150-
global _captured_plots
151-
try:
152-
import matplotlib.pyplot as plt
153-
154-
# Check if there are any active figures
155-
if plt.get_fignums():
156-
for fig_num in plt.get_fignums():
157-
fig = plt.figure(fig_num)
158-
159-
# Save figure to base64
160-
buffer = io.BytesIO()
161-
fig.savefig(buffer, format="png", dpi=150, bbox_inches="tight")
162-
buffer.seek(0)
163-
164-
# Convert to base64
165-
image_data = base64.b64encode(buffer.getvalue()).decode("utf-8")
166-
plot_data = f"data:image/png;base64,{image_data}"
167-
168-
# Add to captured plots if not already there
169-
if plot_data not in _captured_plots:
170-
_captured_plots.append(plot_data)
171-
172-
# Close the figure to free memory
173-
plt.close(fig)
174-
175-
except ImportError:
176-
# matplotlib not available
177-
pass
178-
except Exception as e:
179-
print(f"Warning: Could not capture matplotlib plots: {e}")
180-
181-
182-
def _apply_matplotlib_patches():
183-
"""
184-
Apply simple monkey patches to matplotlib functions to automatically capture plots.
185-
"""
186-
try:
187-
import matplotlib.pyplot as plt
188-
189-
# Only patch if matplotlib is available and not already patched
190-
if hasattr(plt, "_base_agent_patched"):
191-
return
192-
193-
# Store original functions
194-
original_show = plt.show
195-
original_savefig = plt.savefig
196-
197-
def show_with_capture(*args, **kwargs):
198-
"""Enhanced show function that captures plots before displaying them."""
199-
# Capture any plots before showing
200-
_capture_matplotlib_plots()
201-
# Print a message to indicate plot was generated
202-
print("Plot generated and displayed")
203-
# Call the original show function
204-
return original_show(*args, **kwargs)
205-
206-
def savefig_with_capture(*args, **kwargs):
207-
"""Enhanced savefig function that captures plots after saving them."""
208-
# Get the filename from args if provided
209-
filename = args[0] if args else kwargs.get("fname", "unknown")
210-
# Call the original savefig function
211-
result = original_savefig(*args, **kwargs)
212-
# Capture the plot after saving
213-
_capture_matplotlib_plots()
214-
# Print a message to indicate plot was saved
215-
print(f"Plot saved to: {filename}")
216-
return result
217-
218-
# Replace functions with enhanced versions
219-
plt.show = show_with_capture
220-
plt.savefig = savefig_with_capture
221-
222-
# Mark as patched to avoid double-patching
223-
plt._BaseAgent_patched = True
224-
225-
except ImportError:
226-
# matplotlib not available
227-
pass
228-
except Exception as e:
229-
print(f"Warning: Could not apply matplotlib patches: {e}")
230-
231-
232150
def get_captured_plots():
233-
"""
234-
Get all captured matplotlib plots.
151+
"""Get all captured matplotlib plots (backward-compat wrapper).
235152
236153
Returns:
237154
list: A list of captured matplotlib plots
238155
"""
239-
global _captured_plots
240-
return _captured_plots.copy()
156+
return _global_plot_capture.get_plots()
241157

242158

243159
def clear_captured_plots():
244-
"""
245-
Clear all captured matplotlib plots.
246-
"""
247-
global _captured_plots
248-
_captured_plots = []
160+
"""Clear all captured matplotlib plots (backward-compat wrapper)."""
161+
_global_plot_capture.clear()
249162

250163

251164
def read_function_source_code(function_name: str) -> str:
@@ -258,24 +171,14 @@ def read_function_source_code(function_name: str) -> str:
258171
str: The source code of the function
259172
260173
"""
261-
import importlib
262-
import inspect
263-
264174
# Split the function name into module path and function name
265175
parts = function_name.split(".")
266176
module_path = ".".join(parts[:-1])
267177
func_name = parts[-1]
268178

269179
try:
270-
# Import the module
271180
module = importlib.import_module(module_path)
272-
273-
# Get the function object from the module
274181
function = getattr(module, func_name)
275-
276-
# Get the source code of the function
277-
source_code = inspect.getsource(function)
278-
279-
return source_code
182+
return inspect.getsource(function)
280183
except (ImportError, AttributeError) as e:
281-
return f"Error: Could not find function '{function_name}'. Details: {str(e)}"
184+
return f"Error: Could not find function '{function_name}'. Details: {str(e)}"

BaseAgent/utils/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
from BaseAgent.utils.schema import (
99
extract_schema_from_function,
1010
function_to_api_schema,
11-
_parse_docstring,
12-
_type_to_string,
1311
enhance_description_with_llm,
1412
enhance_parameters_with_llm,
1513
)
@@ -37,8 +35,6 @@
3735
# schema
3836
"extract_schema_from_function",
3937
"function_to_api_schema",
40-
"_parse_docstring",
41-
"_type_to_string",
4238
"enhance_description_with_llm",
4339
"enhance_parameters_with_llm",
4440
# formatting

BaseAgent/utils/download.py

Lines changed: 63 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,44 @@
99
import tqdm
1010

1111

12+
def _download_file(url: str, dest_path: str, desc: str) -> bool:
13+
"""Stream-download *url* to *dest_path* with a tqdm progress bar.
14+
15+
Args:
16+
url: URL to download.
17+
dest_path: Local path to write the downloaded bytes to.
18+
desc: Label shown in the progress bar.
19+
20+
Returns:
21+
``True`` on success, ``False`` on any error (error is printed).
22+
"""
23+
try:
24+
response = requests.get(url, stream=True)
25+
response.raise_for_status()
26+
total_size = int(response.headers.get("content-length", 0))
27+
with open(dest_path, "wb") as f:
28+
with tqdm.tqdm(
29+
total=total_size or None,
30+
unit="B",
31+
unit_scale=True,
32+
desc=desc,
33+
ncols=80,
34+
) as pbar:
35+
for chunk in response.iter_content(chunk_size=8192):
36+
if chunk:
37+
f.write(chunk)
38+
pbar.update(len(chunk))
39+
return True
40+
except Exception as e:
41+
print(f"✗ Failed to download {desc}: {e}")
42+
if os.path.exists(dest_path):
43+
try:
44+
os.remove(dest_path)
45+
except OSError:
46+
pass
47+
return False
48+
49+
1250
def check_or_create_path(path=None):
1351
# Set a default path if none is provided
1452
if path is None:
@@ -36,35 +74,30 @@ def download_and_unzip(url: str, dest_dir: str) -> str:
3674
The path to the extracted directory, or an error message.
3775
3876
"""
77+
tmp_zip_path = None
3978
try:
4079
os.makedirs(dest_dir, exist_ok=True)
4180
print(f"Downloading from {url} ...")
42-
with requests.get(url, stream=True) as r:
43-
r.raise_for_status()
44-
total_size = int(r.headers.get("content-length", 0))
45-
chunk_size = 8192
46-
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file:
47-
with tqdm.tqdm(
48-
total=total_size / (1024**3),
49-
unit="GB",
50-
unit_scale=True,
51-
desc="Downloading",
52-
ncols=80,
53-
) as pbar:
54-
for chunk in r.iter_content(chunk_size=chunk_size):
55-
if chunk:
56-
tmp_file.write(chunk)
57-
pbar.update(len(chunk) / (1024**3))
58-
tmp_zip_path = tmp_file.name
81+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_file:
82+
tmp_zip_path = tmp_file.name
83+
84+
if not _download_file(url, tmp_zip_path, "Downloading"):
85+
return f"Error: Failed to download {url}"
86+
5987
print(f"Downloaded to {tmp_zip_path}. Extracting...")
6088
with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref:
6189
zip_ref.extractall(dest_dir)
62-
os.unlink(tmp_zip_path)
6390
print(f"Extraction complete to {dest_dir}")
6491
return dest_dir
6592
except Exception as e:
6693
print(f"Error downloading or extracting zip: {e}")
6794
return f"Error: {e}"
95+
finally:
96+
if tmp_zip_path and os.path.exists(tmp_zip_path):
97+
try:
98+
os.unlink(tmp_zip_path)
99+
except OSError:
100+
pass
68101

69102

70103
def check_and_download_s3_files(
@@ -81,56 +114,19 @@ def check_and_download_s3_files(
81114
Returns:
82115
Dictionary mapping file names to download success status
83116
"""
84-
85117
os.makedirs(local_data_lake_path, exist_ok=True)
86118
download_results = {}
87119

88-
def download_with_progress(url: str, file_path: str, desc: str) -> bool:
89-
"""Download file with progress bar."""
90-
try:
91-
response = requests.get(url, stream=True)
92-
response.raise_for_status()
93-
94-
total_size = int(response.headers.get("content-length", 0))
95-
96-
with open(file_path, "wb") as f:
97-
if total_size > 0:
98-
with tqdm.tqdm(total=total_size, unit="B", unit_scale=True, desc=desc, ncols=80) as pbar:
99-
for chunk in response.iter_content(chunk_size=8192):
100-
if chunk:
101-
f.write(chunk)
102-
pbar.update(len(chunk))
103-
else:
104-
for chunk in response.iter_content(chunk_size=8192):
105-
if chunk:
106-
f.write(chunk)
107-
return True
108-
except Exception as e:
109-
print(f"✗ Failed to download {desc}: {e}")
110-
if os.path.exists(file_path):
111-
try:
112-
os.remove(file_path)
113-
except OSError:
114-
pass
115-
return False
116-
117-
def cleanup_file(file_path: str):
118-
"""Clean up file if it exists."""
119-
if os.path.exists(file_path):
120-
try:
121-
os.remove(file_path)
122-
except OSError:
123-
pass
124-
125120
# Handle benchmark folder (download as zip)
126121
if folder == "benchmark":
127122
print(f"Downloading entire {folder} folder structure...")
128123
s3_zip_url = urljoin(s3_bucket_url + "/", folder + ".zip")
124+
tmp_zip_path = None
125+
try:
126+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
127+
tmp_zip_path = tmp_zip.name
129128

130-
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp_zip:
131-
tmp_zip_path = tmp_zip.name
132-
133-
if download_with_progress(s3_zip_url, tmp_zip_path, f"{folder}.zip"):
129+
if _download_file(s3_zip_url, tmp_zip_path, f"{folder}.zip"):
134130
print(f"Extracting {folder}.zip...")
135131
try:
136132
with zipfile.ZipFile(tmp_zip_path, "r") as zip_ref:
@@ -140,10 +136,14 @@ def cleanup_file(file_path: str):
140136
except Exception as e:
141137
print(f"✗ Error extracting {folder}.zip: {e}")
142138
download_results = dict.fromkeys(expected_files, False)
143-
finally:
144-
cleanup_file(tmp_zip_path)
145139
else:
146140
download_results = dict.fromkeys(expected_files, False)
141+
finally:
142+
if tmp_zip_path and os.path.exists(tmp_zip_path):
143+
try:
144+
os.unlink(tmp_zip_path)
145+
except OSError:
146+
pass
147147

148148
return download_results
149149

@@ -158,7 +158,7 @@ def cleanup_file(file_path: str):
158158
s3_file_url = urljoin(s3_bucket_url + "/" + folder + "/", filename)
159159
print(f"Downloading {filename} from {folder}...")
160160

161-
if download_with_progress(s3_file_url, local_file_path, filename):
161+
if _download_file(s3_file_url, local_file_path, filename):
162162
print(f"✓ Successfully downloaded: {filename}")
163163
download_results[filename] = True
164164
else:

0 commit comments

Comments
 (0)