Skip to content

Commit f4cc6c0

Browse files
CopilotOhYee
andauthored
feat: enhance read_file tool to return base64 by default, add raw param
- Modified read_file in CodeInterpreterToolSet to encode file content as base64 by default; response includes 'encoding' field ('base64' or 'raw') - Added optional 'raw' parameter (default False): when True, returns plain text content unchanged - Handles both str and bytes content from the underlying API - Added unit tests covering base64 default, roundtrip, bytes input, raw=True, and raw=False equivalence to default - mypy type check passed (324 files, no issues) Agent-Logs-Url: https://github.com/Serverless-Devs/agentrun-sdk-python/sessions/410c37c7-b485-46a3-806d-026c8d397150 Co-authored-by: OhYee <13498329+OhYee@users.noreply.github.com>
1 parent d3078de commit f4cc6c0

File tree

2 files changed

+126
-5
lines changed

2 files changed

+126
-5
lines changed

agentrun/integration/builtin/sandbox.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -388,17 +388,24 @@ def inner(sb: Sandbox):
388388
name="read_file",
389389
description=(
390390
"Read the content of a file at the specified path in the sandbox."
391-
" Returns the text content. Suitable for reading code files,"
392-
" configs, logs, etc."
391+
" By default returns the file content encoded as a base64 string."
392+
" Set raw=true to get the plain text content instead."
393+
" Suitable for reading code files, configs, logs, binary files,"
394+
" etc."
393395
),
394396
)
395-
def read_file(self, path: str) -> Dict[str, Any]:
396-
"""读取文件内容 / Read file content"""
397+
def read_file(self, path: str, raw: bool = False) -> Dict[str, Any]:
398+
"""读取文件内容,默认返回 base64 编码结果 / Read file content, returns base64 by default"""
397399

398400
def inner(sb: Sandbox):
399401
assert isinstance(sb, CodeInterpreterSandbox)
400402
content = sb.file.read(path=path)
401-
return {"path": path, "content": content}
403+
if raw:
404+
return {"path": path, "content": content, "encoding": "raw"}
405+
encoded = base64.b64encode(
406+
content.encode("utf-8") if isinstance(content, str) else content
407+
).decode("ascii")
408+
return {"path": path, "content": encoded, "encoding": "base64"}
402409

403410
return self._run_in_sandbox(inner)
404411

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""CodeInterpreterToolSet read_file 工具单元测试
2+
3+
测试 read_file 工具的 base64 编码行为和 raw 参数控制。
4+
Tests the read_file tool's base64 encoding behavior and the raw parameter control.
5+
"""
6+
7+
import base64
8+
import threading
9+
from unittest.mock import MagicMock, patch
10+
11+
import pytest
12+
13+
from agentrun.integration.builtin.sandbox import CodeInterpreterToolSet
14+
15+
16+
@pytest.fixture
17+
def toolset():
18+
"""创建 CodeInterpreterToolSet 实例,绕过 __init__ / Create instance bypassing __init__."""
19+
with patch.object(CodeInterpreterToolSet, "__init__", lambda self: None):
20+
ts = CodeInterpreterToolSet()
21+
ts.sandbox = None
22+
ts.sandbox_id = ""
23+
ts._lock = threading.Lock()
24+
ts.template_name = "test-tpl"
25+
ts.template_type = MagicMock()
26+
ts.sandbox_idle_timeout_seconds = 600
27+
ts.config = None
28+
ts.oss_mount_config = None
29+
ts.nas_config = None
30+
ts.polar_fs_config = None
31+
return ts
32+
33+
34+
def _make_mock_sandbox(file_content: str):
35+
"""构造一个模拟沙箱,其 file.read 返回指定内容 / Build mock sandbox with file.read returning given content."""
36+
from agentrun.sandbox.code_interpreter_sandbox import CodeInterpreterSandbox
37+
38+
mock_sb = MagicMock(spec=CodeInterpreterSandbox)
39+
mock_sb.file.read.return_value = file_content
40+
return mock_sb
41+
42+
43+
class TestReadFileBase64Default:
44+
"""测试 read_file 默认返回 base64 编码内容 / Test that read_file returns base64 by default."""
45+
46+
def test_returns_base64_encoded_content(self, toolset):
47+
"""默认情况下内容应为 base64 编码 / Content should be base64 encoded by default."""
48+
file_content = "hello world"
49+
mock_sb = _make_mock_sandbox(file_content)
50+
51+
with patch.object(toolset, "_run_in_sandbox", side_effect=lambda fn: fn(mock_sb)):
52+
result = toolset.read_file(path="/tmp/test.txt")
53+
54+
expected_b64 = base64.b64encode(b"hello world").decode("ascii")
55+
assert result["content"] == expected_b64
56+
assert result["encoding"] == "base64"
57+
assert result["path"] == "/tmp/test.txt"
58+
59+
def test_base64_roundtrip(self, toolset):
60+
"""base64 解码后应等于原始内容 / Decoded base64 should equal original content."""
61+
file_content = "中文内容 line1\nline2"
62+
mock_sb = _make_mock_sandbox(file_content)
63+
64+
with patch.object(toolset, "_run_in_sandbox", side_effect=lambda fn: fn(mock_sb)):
65+
result = toolset.read_file(path="/tmp/utf8.txt")
66+
67+
decoded = base64.b64decode(result["content"]).decode("utf-8")
68+
assert decoded == file_content
69+
70+
def test_bytes_content_also_base64_encoded(self, toolset):
71+
"""当底层返回 bytes 时同样应 base64 编码 / Bytes content should also be base64 encoded."""
72+
file_bytes = b"\x00\x01\x02\x03"
73+
from agentrun.sandbox.code_interpreter_sandbox import CodeInterpreterSandbox
74+
75+
mock_sb = MagicMock(spec=CodeInterpreterSandbox)
76+
mock_sb.file.read.return_value = file_bytes
77+
78+
with patch.object(toolset, "_run_in_sandbox", side_effect=lambda fn: fn(mock_sb)):
79+
result = toolset.read_file(path="/tmp/binary.bin")
80+
81+
expected_b64 = base64.b64encode(file_bytes).decode("ascii")
82+
assert result["content"] == expected_b64
83+
assert result["encoding"] == "base64"
84+
85+
86+
class TestReadFileRawParam:
87+
"""测试 raw=True 时返回原始内容 / Test that raw=True returns plain text content."""
88+
89+
def test_raw_true_returns_plain_content(self, toolset):
90+
"""raw=True 时应返回原始文本 / raw=True should return raw text."""
91+
file_content = "plain text content"
92+
mock_sb = _make_mock_sandbox(file_content)
93+
94+
with patch.object(toolset, "_run_in_sandbox", side_effect=lambda fn: fn(mock_sb)):
95+
result = toolset.read_file(path="/tmp/plain.txt", raw=True)
96+
97+
assert result["content"] == file_content
98+
assert result["encoding"] == "raw"
99+
assert result["path"] == "/tmp/plain.txt"
100+
101+
def test_raw_false_same_as_default(self, toolset):
102+
"""raw=False 应与默认行为一致 / raw=False should behave identically to default."""
103+
file_content = "some content"
104+
mock_sb = _make_mock_sandbox(file_content)
105+
106+
with patch.object(toolset, "_run_in_sandbox", side_effect=lambda fn: fn(mock_sb)):
107+
result_explicit = toolset.read_file(path="/tmp/f.txt", raw=False)
108+
109+
mock_sb2 = _make_mock_sandbox(file_content)
110+
with patch.object(toolset, "_run_in_sandbox", side_effect=lambda fn: fn(mock_sb2)):
111+
result_default = toolset.read_file(path="/tmp/f.txt")
112+
113+
assert result_explicit == result_default
114+
assert result_explicit["encoding"] == "base64"

0 commit comments

Comments
 (0)