Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import asyncio
import sys
from collections.abc import Callable, Mapping
from pathlib import Path
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 60 additions & 0 deletions python/packages/declarative/tests/test_declarative_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down
21 changes: 21 additions & 0 deletions python/packages/declarative/tests/test_workflow_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading