Skip to content

Commit c385c2d

Browse files
Python: make declarative YAML path loading portable (microsoft#8260)
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
1 parent 272fc72 commit c385c2d

4 files changed

Lines changed: 88 additions & 5 deletions

File tree

‎python/packages/declarative/agent_framework_declarative/_loader.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import sys
67
from collections.abc import Callable, Mapping
78
from pathlib import Path
@@ -338,7 +339,7 @@ def create_agent_from_yaml_path(self, yaml_path: str | Path) -> Agent:
338339
yaml_path = Path(yaml_path)
339340
if not yaml_path.exists():
340341
raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}")
341-
with open(yaml_path) as f:
342+
with open(yaml_path, encoding="utf-8") as f:
342343
yaml_str = f.read()
343344
return self.create_agent_from_yaml(yaml_str)
344345

@@ -508,9 +509,10 @@ async def create_agent_from_yaml_path_async(self, yaml_path: str | Path) -> Agen
508509
"""
509510
if not isinstance(yaml_path, Path):
510511
yaml_path = Path(yaml_path)
511-
if not yaml_path.exists():
512-
raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}")
513-
yaml_str = yaml_path.read_text()
512+
try:
513+
yaml_str = await asyncio.to_thread(yaml_path.read_text, encoding="utf-8")
514+
except FileNotFoundError as exc:
515+
raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}") from exc
514516
return await self.create_agent_from_yaml_async(yaml_str)
515517

516518
async def create_agent_from_yaml_async(self, yaml_str: str) -> Agent:

‎python/packages/declarative/agent_framework_declarative/_workflows/_factory.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ def create_workflow_from_yaml_path(
241241
if not yaml_path.exists():
242242
raise FileNotFoundError(f"Workflow YAML file not found: {yaml_path}")
243243

244-
with open(yaml_path) as f:
244+
with open(yaml_path, encoding="utf-8") as f:
245245
yaml_content = f.read()
246246

247247
return self.create_workflow_from_yaml(yaml_content, base_path=yaml_path.parent)

‎python/packages/declarative/tests/test_declarative_loader.py‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import builtins
44
import sys
5+
import threading
56
from pathlib import Path
67
from typing import Any
78
from unittest.mock import AsyncMock, MagicMock, patch
@@ -1051,6 +1052,30 @@ def test_create_agent_from_yaml_path_with_path_object(self, tmp_path):
10511052

10521053
assert agent.name == "PathAgent"
10531054

1055+
def test_create_agent_from_yaml_path_reads_utf8(self, tmp_path):
1056+
"""Test create_agent_from_yaml_path reads YAML as UTF-8."""
1057+
from unittest.mock import MagicMock
1058+
1059+
from agent_framework_declarative import AgentFactory
1060+
1061+
yaml_file = tmp_path / "unicode_agent.yaml"
1062+
yaml_file.write_text(
1063+
"""
1064+
kind: Prompt
1065+
name: UnicodeAgent
1066+
instructions: 政务助手 🏛️
1067+
""",
1068+
encoding="utf-8",
1069+
)
1070+
1071+
mock_client = MagicMock()
1072+
factory = AgentFactory(client=mock_client)
1073+
with patch("builtins.open", wraps=builtins.open) as mock_open:
1074+
agent = factory.create_agent_from_yaml_path(yaml_file)
1075+
1076+
mock_open.assert_called_once_with(yaml_file, encoding="utf-8")
1077+
assert agent.name == "UnicodeAgent"
1078+
10541079

10551080
class TestAgentFactoryAsyncMethods:
10561081
"""Tests for AgentFactory async methods."""
@@ -1138,6 +1163,41 @@ async def test_create_agent_from_yaml_path_async_with_string_path(self, tmp_path
11381163

11391164
assert agent.name == "AsyncPathAgent"
11401165

1166+
async def test_create_agent_from_yaml_path_async_reads_utf8_off_event_loop(self, tmp_path):
1167+
"""Test async path loading reads UTF-8 without blocking the event-loop thread."""
1168+
from unittest.mock import MagicMock
1169+
1170+
from agent_framework_declarative import AgentFactory
1171+
1172+
yaml_file = tmp_path / "async_unicode_agent.yaml"
1173+
yaml_file.write_text(
1174+
"""
1175+
kind: Prompt
1176+
name: AsyncUnicodeAgent
1177+
instructions: 政务助手 🏛️
1178+
""",
1179+
encoding="utf-8",
1180+
)
1181+
1182+
event_loop_thread_id = threading.get_ident()
1183+
read_thread_id: int | None = None
1184+
original_read_text = Path.read_text
1185+
1186+
def tracked_read_text(path: Path, *args, **kwargs):
1187+
nonlocal read_thread_id
1188+
read_thread_id = threading.get_ident()
1189+
assert kwargs["encoding"] == "utf-8"
1190+
return original_read_text(path, *args, **kwargs)
1191+
1192+
mock_client = MagicMock()
1193+
factory = AgentFactory(client=mock_client)
1194+
with patch.object(Path, "read_text", autospec=True, side_effect=tracked_read_text):
1195+
agent = await factory.create_agent_from_yaml_path_async(yaml_file)
1196+
1197+
assert read_thread_id is not None
1198+
assert read_thread_id != event_loop_thread_id
1199+
assert agent.name == "AsyncUnicodeAgent"
1200+
11411201

11421202
class TestAgentFactoryProviderLookup:
11431203
"""Tests for provider configuration lookup."""

‎python/packages/declarative/tests/test_workflow_factory.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,27 @@ def test_load_from_file(self, tmp_path):
545545
assert workflow is not None
546546
assert workflow.name == "file-workflow"
547547

548+
def test_load_from_file_reads_utf8(self, tmp_path):
549+
"""Test loading a workflow file explicitly uses UTF-8."""
550+
workflow_file = tmp_path / "UnicodeWorkflow.yaml"
551+
workflow_file.write_text(
552+
"""
553+
name: unicode-workflow
554+
actions:
555+
- kind: SetValue
556+
path: Local.message
557+
value: 政务助手 🏛️
558+
""",
559+
encoding="utf-8",
560+
)
561+
562+
factory = WorkflowFactory()
563+
with patch("builtins.open", wraps=open) as mock_open:
564+
workflow = factory.create_workflow_from_yaml_path(workflow_file)
565+
566+
mock_open.assert_called_once_with(workflow_file, encoding="utf-8")
567+
assert workflow.name == "unicode-workflow"
568+
548569

549570
@_requires_powerfx
550571
class TestDisplayNameMetadata:

0 commit comments

Comments
 (0)