Skip to content

Commit 0cb4c81

Browse files
mc-marcochengxuanyang15
authored andcommitted
fix(skills): enforce utf-8 encoding when materializing skill files on Windows
Merge #5820 ### Link to Issue or Description of Change **1. Link to an existing issue (if applicable):** - Closes: #5819 - Related: #5819 **2. Or, if no issue exists, describe the change:** **Problem:** When running the ADK on Windows, executing a skill script via `_SkillScriptCodeExecutor` fails with a `UnicodeEncodeError` if the skill's resources (references, assets, or scripts) contain non-ASCII characters. The generated wrapper script writes these files without specifying an encoding, causing Windows to fall back to its system locale encoding (e.g., `cp1252`). **Solution:** By explicitly setting `encoding='utf-8'` when `mode == 'w'` in the generated wrapper script, we ensure that text files are correctly written regardless of the system's default locale encoding. Binary assets (`mode == 'wb'`) continue to be handled properly without an encoding argument. Co-authored-by: Xuan Yang <xygoogle@google.com> COPYBARA_INTEGRATE_REVIEW=#5820 from mc-marcocheng:fix/skill-toolset-utf8 eecabb6 PiperOrigin-RevId: 933844065
1 parent 81b8067 commit 0cb4c81

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

src/google/adk/tools/skill_toolset.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,10 @@ def _build_wrapper_code(
669669
" full_path = os.path.join(os.path.abspath(td), norm_rel)",
670670
" os.makedirs(os.path.dirname(full_path), exist_ok=True)",
671671
" mode = 'wb' if isinstance(content, bytes) else 'w'",
672-
" with open(full_path, mode) as f:",
672+
(
673+
" with open(full_path, mode, encoding='utf-8' if mode == 'w'"
674+
" else None) as f:"
675+
),
673676
" f.write(content)",
674677
" os.chdir(td)",
675678
" try:",

tests/unittests/tools/test_skill_toolset.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,31 @@ async def test_execute_script_shell_success(mock_skill1):
817817
assert "__shell_result__" in code_input.code
818818

819819

820+
@pytest.mark.asyncio
821+
async def test_build_wrapper_code_with_unicode(mock_skill1):
822+
"""Verify that generated code uses utf-8 encoding for materializing files."""
823+
# Add unicode content to mock_skill1 resources
824+
unicode_content = "你好"
825+
mock_skill1.resources.list_references.return_value = ["unicode.txt"]
826+
mock_skill1.resources.get_reference.side_effect = lambda name: (
827+
unicode_content if name == "unicode.txt" else None
828+
)
829+
830+
executor = _make_mock_executor()
831+
toolset = skill_toolset.SkillToolset([mock_skill1], code_executor=executor)
832+
tool = skill_toolset.RunSkillScriptTool(toolset)
833+
ctx = _make_tool_context_with_agent()
834+
await tool.run_async(
835+
args={"skill_name": "skill1", "file_path": "run.py"},
836+
tool_context=ctx,
837+
)
838+
839+
call_args = executor.execute_code.call_args
840+
code_input = call_args[0][1]
841+
assert "encoding='utf-8' if mode == 'w' else None" in code_input.code
842+
assert unicode_content in code_input.code
843+
844+
820845
@pytest.mark.asyncio
821846
async def test_execute_script_with_input_args_python(mock_skill1):
822847
executor = _make_mock_executor(stdout="done\n")
@@ -1251,6 +1276,35 @@ async def test_integration_python_stdout():
12511276
assert result["stderr"] == ""
12521277

12531278

1279+
@pytest.mark.asyncio
1280+
async def test_integration_python_unicode_materialization():
1281+
"""Real executor: Python script with unicode resources."""
1282+
script = models.Script(
1283+
src=(
1284+
"with open('references/unicode.txt', 'r', encoding='utf-8') as f:"
1285+
" print(f.read())"
1286+
)
1287+
)
1288+
skill = _make_skill_with_script("test_skill", "unicode.py", script)
1289+
skill.resources.get_reference.side_effect = lambda n: (
1290+
"你好,世界" if n == "unicode.txt" else None
1291+
)
1292+
skill.resources.list_references.return_value = ["unicode.txt"]
1293+
toolset = _make_real_executor_toolset([skill])
1294+
tool = skill_toolset.RunSkillScriptTool(toolset)
1295+
ctx = _make_tool_context_with_agent()
1296+
result = await tool.run_async(
1297+
args={
1298+
"skill_name": "test_skill",
1299+
"file_path": "unicode.py",
1300+
},
1301+
tool_context=ctx,
1302+
)
1303+
assert "status" in result, f"Result missing status: {result}"
1304+
assert result["status"] == "success"
1305+
assert "你好,世界" in result["stdout"]
1306+
1307+
12541308
@pytest.mark.asyncio
12551309
async def test_integration_python_imports_sibling_script_module():
12561310
"""Real executor: Python scripts can import helpers from scripts/."""

0 commit comments

Comments
 (0)