diff --git a/python/packages/declarative/agent_framework_declarative/_loader.py b/python/packages/declarative/agent_framework_declarative/_loader.py index 5be02a9ad8..5e7e0b8cf2 100644 --- a/python/packages/declarative/agent_framework_declarative/_loader.py +++ b/python/packages/declarative/agent_framework_declarative/_loader.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import sys from collections.abc import Callable, Mapping from pathlib import Path @@ -338,7 +339,7 @@ def create_agent_from_yaml_path(self, yaml_path: str | Path) -> Agent: yaml_path = Path(yaml_path) if not yaml_path.exists(): raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}") - with open(yaml_path) as f: + with open(yaml_path, encoding="utf-8") as f: yaml_str = f.read() return self.create_agent_from_yaml(yaml_str) @@ -508,9 +509,10 @@ async def create_agent_from_yaml_path_async(self, yaml_path: str | Path) -> Agen """ if not isinstance(yaml_path, Path): yaml_path = Path(yaml_path) - if not yaml_path.exists(): - raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}") - yaml_str = yaml_path.read_text() + try: + yaml_str = await asyncio.to_thread(yaml_path.read_text, encoding="utf-8") + except FileNotFoundError as exc: + raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}") from exc return await self.create_agent_from_yaml_async(yaml_str) async def create_agent_from_yaml_async(self, yaml_str: str) -> Agent: diff --git a/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py b/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py index 363b4d77f3..419065a130 100644 --- a/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py +++ b/python/packages/declarative/agent_framework_declarative/_workflows/_factory.py @@ -241,7 +241,7 @@ def create_workflow_from_yaml_path( if not yaml_path.exists(): raise FileNotFoundError(f"Workflow YAML file not found: {yaml_path}") - with open(yaml_path) as f: + with open(yaml_path, encoding="utf-8") as f: yaml_content = f.read() return self.create_workflow_from_yaml(yaml_content, base_path=yaml_path.parent) diff --git a/python/packages/declarative/tests/test_declarative_loader.py b/python/packages/declarative/tests/test_declarative_loader.py index cac6d4f53e..bde08e6939 100644 --- a/python/packages/declarative/tests/test_declarative_loader.py +++ b/python/packages/declarative/tests/test_declarative_loader.py @@ -2,6 +2,7 @@ import builtins import sys +import threading from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -1051,6 +1052,30 @@ def test_create_agent_from_yaml_path_with_path_object(self, tmp_path): assert agent.name == "PathAgent" + def test_create_agent_from_yaml_path_reads_utf8(self, tmp_path): + """Test create_agent_from_yaml_path reads YAML as UTF-8.""" + from unittest.mock import MagicMock + + from agent_framework_declarative import AgentFactory + + yaml_file = tmp_path / "unicode_agent.yaml" + yaml_file.write_text( + """ +kind: Prompt +name: UnicodeAgent +instructions: 政务助手 🏛️ +""", + encoding="utf-8", + ) + + mock_client = MagicMock() + factory = AgentFactory(client=mock_client) + with patch("builtins.open", wraps=builtins.open) as mock_open: + agent = factory.create_agent_from_yaml_path(yaml_file) + + mock_open.assert_called_once_with(yaml_file, encoding="utf-8") + assert agent.name == "UnicodeAgent" + class TestAgentFactoryAsyncMethods: """Tests for AgentFactory async methods.""" @@ -1138,6 +1163,41 @@ async def test_create_agent_from_yaml_path_async_with_string_path(self, tmp_path assert agent.name == "AsyncPathAgent" + async def test_create_agent_from_yaml_path_async_reads_utf8_off_event_loop(self, tmp_path): + """Test async path loading reads UTF-8 without blocking the event-loop thread.""" + from unittest.mock import MagicMock + + from agent_framework_declarative import AgentFactory + + yaml_file = tmp_path / "async_unicode_agent.yaml" + yaml_file.write_text( + """ +kind: Prompt +name: AsyncUnicodeAgent +instructions: 政务助手 🏛️ +""", + encoding="utf-8", + ) + + event_loop_thread_id = threading.get_ident() + read_thread_id: int | None = None + original_read_text = Path.read_text + + def tracked_read_text(path: Path, *args, **kwargs): + nonlocal read_thread_id + read_thread_id = threading.get_ident() + assert kwargs["encoding"] == "utf-8" + return original_read_text(path, *args, **kwargs) + + mock_client = MagicMock() + factory = AgentFactory(client=mock_client) + with patch.object(Path, "read_text", autospec=True, side_effect=tracked_read_text): + agent = await factory.create_agent_from_yaml_path_async(yaml_file) + + assert read_thread_id is not None + assert read_thread_id != event_loop_thread_id + assert agent.name == "AsyncUnicodeAgent" + class TestAgentFactoryProviderLookup: """Tests for provider configuration lookup.""" diff --git a/python/packages/declarative/tests/test_workflow_factory.py b/python/packages/declarative/tests/test_workflow_factory.py index b170753794..eee4b6b673 100644 --- a/python/packages/declarative/tests/test_workflow_factory.py +++ b/python/packages/declarative/tests/test_workflow_factory.py @@ -545,6 +545,27 @@ def test_load_from_file(self, tmp_path): assert workflow is not None assert workflow.name == "file-workflow" + def test_load_from_file_reads_utf8(self, tmp_path): + """Test loading a workflow file explicitly uses UTF-8.""" + workflow_file = tmp_path / "UnicodeWorkflow.yaml" + workflow_file.write_text( + """ +name: unicode-workflow +actions: + - kind: SetValue + path: Local.message + value: 政务助手 🏛️ +""", + encoding="utf-8", + ) + + factory = WorkflowFactory() + with patch("builtins.open", wraps=open) as mock_open: + workflow = factory.create_workflow_from_yaml_path(workflow_file) + + mock_open.assert_called_once_with(workflow_file, encoding="utf-8") + assert workflow.name == "unicode-workflow" + @_requires_powerfx class TestDisplayNameMetadata: