From 8662728bbabdc2b6594e9bcb3872febc4afba5a0 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:46:41 -0800 Subject: [PATCH 1/6] Updated declarative kind mapping --- .../agent_framework_declarative/_loader.py | 4 +- .../agent_framework_declarative/_models.py | 8 ++- .../tests/test_declarative_loader.py | 52 ++++++++++++++++ .../tests/test_declarative_models.py | 62 +++++++++++++++++++ 4 files changed, 122 insertions(+), 4 deletions(-) diff --git a/python/packages/declarative/agent_framework_declarative/_loader.py b/python/packages/declarative/agent_framework_declarative/_loader.py index 001f6f511e..38ae550bf2 100644 --- a/python/packages/declarative/agent_framework_declarative/_loader.py +++ b/python/packages/declarative/agent_framework_declarative/_loader.py @@ -452,7 +452,7 @@ def create_agent_from_dict(self, agent_def: dict[str, Any]) -> Agent: name=prompt_agent.name, description=prompt_agent.description, instructions=prompt_agent.instructions, - **chat_options, + default_options=chat_options, ) async def create_agent_from_yaml_path_async(self, yaml_path: str | Path) -> Agent: @@ -569,7 +569,7 @@ async def create_agent_from_dict_async(self, agent_def: dict[str, Any]) -> Agent name=prompt_agent.name, description=prompt_agent.description, instructions=prompt_agent.instructions, - **chat_options, + default_options=chat_options, ) async def _create_agent_with_provider(self, prompt_agent: PromptAgent, mapping: ProviderTypeMapping) -> Agent: diff --git a/python/packages/declarative/agent_framework_declarative/_models.py b/python/packages/declarative/agent_framework_declarative/_models.py index 181b3307b7..8ccd8aa691 100644 --- a/python/packages/declarative/agent_framework_declarative/_models.py +++ b/python/packages/declarative/agent_framework_declarative/_models.py @@ -110,8 +110,12 @@ def from_dict( # We're being called on a subclass, use the normal from_dict return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies) # type: ignore[attr-defined, no-any-return] - # Filter out 'type' (if it exists) field which is not a Property parameter - value.pop("type", None) + # The YAML spec uses 'type' for the data type, but Property stores it as 'kind' + if "type" in value: + if "kind" not in value: + value["kind"] = value.pop("type") + else: + value.pop("type") kind = value.get("kind", "") if kind == "array": return ArrayProperty.from_dict(value, dependencies=dependencies) diff --git a/python/packages/declarative/tests/test_declarative_loader.py b/python/packages/declarative/tests/test_declarative_loader.py index a200a69310..bd58e0044a 100644 --- a/python/packages/declarative/tests/test_declarative_loader.py +++ b/python/packages/declarative/tests/test_declarative_loader.py @@ -556,6 +556,58 @@ def test_create_agent_from_dict_without_model_or_client_raises(self): with pytest.raises(DeclarativeLoaderError, match="ChatClient must be provided"): factory.create_agent_from_dict(agent_def) + def test_create_agent_from_dict_output_schema_in_default_options(self): + """Test that outputSchema is passed as response_format in Agent.default_options.""" + from unittest.mock import MagicMock + + from pydantic import BaseModel + + from agent_framework_declarative import AgentFactory + + agent_def = { + "kind": "Prompt", + "name": "TestAgent", + "instructions": "You are helpful.", + "outputSchema": { + "properties": { + "answer": {"type": "string", "required": True, "description": "The answer."}, + }, + }, + } + + mock_client = MagicMock() + factory = AgentFactory(client=mock_client) + agent = factory.create_agent_from_dict(agent_def) + + assert "response_format" in agent.default_options + assert isinstance(agent.default_options["response_format"], type) + assert issubclass(agent.default_options["response_format"], BaseModel) + + def test_create_agent_from_dict_chat_options_in_default_options(self): + """Test that chat options (temperature, top_p) are in Agent.default_options.""" + from unittest.mock import MagicMock + + from agent_framework_declarative import AgentFactory + + agent_def = { + "kind": "Prompt", + "name": "TestAgent", + "instructions": "You are helpful.", + "model": { + "options": { + "temperature": 0.7, + "topP": 0.9, + }, + }, + } + + mock_client = MagicMock() + factory = AgentFactory(client=mock_client) + agent = factory.create_agent_from_dict(agent_def) + + assert agent.default_options.get("temperature") == 0.7 + assert agent.default_options.get("top_p") == 0.9 + class TestAgentFactorySafeMode: """Tests for AgentFactory safe_mode parameter.""" diff --git a/python/packages/declarative/tests/test_declarative_models.py b/python/packages/declarative/tests/test_declarative_models.py index f7a56f2c96..3ac3e05d86 100644 --- a/python/packages/declarative/tests/test_declarative_models.py +++ b/python/packages/declarative/tests/test_declarative_models.py @@ -103,6 +103,50 @@ def test_property_from_dict(self): assert prop.description == "A test property" assert prop.required is True + def test_property_from_dict_type_maps_to_kind(self): + """Test that 'type' field in YAML is mapped to 'kind' internally.""" + data = { + "name": "test_prop", + "type": "string", + "description": "A test property", + "required": True, + } + prop = Property.from_dict(data) + assert prop.name == "test_prop" + assert prop.kind == "string" + + def test_property_from_dict_kind_takes_precedence_over_type(self): + """Test that 'kind' takes precedence when both 'type' and 'kind' are present.""" + data = { + "name": "test_prop", + "type": "integer", + "kind": "string", + } + prop = Property.from_dict(data) + assert prop.kind == "string" + + def test_property_from_dict_type_dispatches_to_array(self): + """Test that 'type: array' correctly dispatches to ArrayProperty.""" + data = { + "name": "test_array", + "type": "array", + "items": {"type": "string"}, + } + prop = Property.from_dict(data) + assert isinstance(prop, ArrayProperty) + assert prop.kind == "array" + + def test_property_from_dict_type_dispatches_to_object(self): + """Test that 'type: object' correctly dispatches to ObjectProperty.""" + data = { + "name": "test_object", + "type": "object", + "properties": {"field": {"type": "string"}}, + } + prop = Property.from_dict(data) + assert isinstance(prop, ObjectProperty) + assert prop.kind == "object" + class TestArrayProperty: """Tests for ArrayProperty class.""" @@ -230,6 +274,24 @@ def test_property_schema_with_dict_properties(self): assert age_prop.kind == "integer" assert age_prop.required is True + def test_property_schema_with_type_field_produces_correct_json_schema(self): + """Test that PropertySchema with 'type' fields (YAML spec format) produces valid JSON schema.""" + data = { + "properties": { + "language": {"type": "string", "required": True, "description": "The language."}, + "answer": {"type": "string", "required": True, "description": "The answer."}, + }, + } + schema = PropertySchema.from_dict(data) + assert len(schema.properties) == 2 + + lang_prop = next(p for p in schema.properties if p.name == "language") + assert lang_prop.kind == "string" + + json_schema = schema.to_json_schema() + assert json_schema["properties"]["language"]["type"] == "string" + assert json_schema["properties"]["answer"]["type"] == "string" + class TestConnection: """Tests for Connection base class.""" From c594058319be2df425e9853738a263319cfd6480 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 18 Feb 2026 12:59:58 -0800 Subject: [PATCH 2/6] Fixed required property handling --- .../declarative/agent_framework_declarative/_models.py | 10 ++++++++++ .../declarative/tests/test_declarative_models.py | 7 ++++++- .../samples/02-agents/declarative/get_weather_agent.py | 3 +++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/python/packages/declarative/agent_framework_declarative/_models.py b/python/packages/declarative/agent_framework_declarative/_models.py index 8ccd8aa691..1a95edf8f9 100644 --- a/python/packages/declarative/agent_framework_declarative/_models.py +++ b/python/packages/declarative/agent_framework_declarative/_models.py @@ -228,11 +228,21 @@ def to_json_schema(self) -> dict[str, Any]: """Get a schema out of this PropertySchema to create pydantic models.""" json_schema = self.to_dict(exclude={"type"}, exclude_none=True) new_props = {} + required_fields: list[str] = [] for prop in json_schema.get("properties", []): prop_name = prop.pop("name") prop["type"] = prop.pop("kind", None) + # Convert property-level 'required' boolean to a top-level 'required' array + if prop.pop("required", False): + required_fields.append(prop_name) + # Remove empty enum arrays + if not prop.get("enum"): + prop.pop("enum", None) new_props[prop_name] = prop + json_schema["type"] = "object" json_schema["properties"] = new_props + if required_fields: + json_schema["required"] = required_fields return json_schema diff --git a/python/packages/declarative/tests/test_declarative_models.py b/python/packages/declarative/tests/test_declarative_models.py index 3ac3e05d86..8768b5b01c 100644 --- a/python/packages/declarative/tests/test_declarative_models.py +++ b/python/packages/declarative/tests/test_declarative_models.py @@ -279,7 +279,7 @@ def test_property_schema_with_type_field_produces_correct_json_schema(self): data = { "properties": { "language": {"type": "string", "required": True, "description": "The language."}, - "answer": {"type": "string", "required": True, "description": "The answer."}, + "answer": {"type": "string", "required": False, "description": "The answer."}, }, } schema = PropertySchema.from_dict(data) @@ -289,8 +289,13 @@ def test_property_schema_with_type_field_produces_correct_json_schema(self): assert lang_prop.kind == "string" json_schema = schema.to_json_schema() + assert json_schema["type"] == "object" assert json_schema["properties"]["language"]["type"] == "string" assert json_schema["properties"]["answer"]["type"] == "string" + # required is a top-level array, not a per-property boolean + assert json_schema["required"] == ["language"] + assert "required" not in json_schema["properties"]["language"] + assert "required" not in json_schema["properties"]["answer"] class TestConnection: diff --git a/python/samples/02-agents/declarative/get_weather_agent.py b/python/samples/02-agents/declarative/get_weather_agent.py index af44382c00..49995851fc 100644 --- a/python/samples/02-agents/declarative/get_weather_agent.py +++ b/python/samples/02-agents/declarative/get_weather_agent.py @@ -7,6 +7,9 @@ from agent_framework.azure import AzureOpenAIResponsesClient from agent_framework.declarative import AgentFactory from azure.identity import AzureCliCredential +from dotenv import load_dotenv + +load_dotenv() def get_weather(location: str, unit: Literal["celsius", "fahrenheit"] = "celsius") -> str: From 7cd65866bb99b814b13219a4ae14e33e1e22221c Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:21:24 -0800 Subject: [PATCH 3/6] Updated inline yaml sample --- python/samples/02-agents/declarative/inline_yaml.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/samples/02-agents/declarative/inline_yaml.py b/python/samples/02-agents/declarative/inline_yaml.py index 7c2bfa6dbf..e7eb5a996f 100644 --- a/python/samples/02-agents/declarative/inline_yaml.py +++ b/python/samples/02-agents/declarative/inline_yaml.py @@ -1,6 +1,10 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio +from dotenv import load_dotenv + +load_dotenv() + from agent_framework.declarative import AgentFactory from azure.identity.aio import AzureCliCredential @@ -34,7 +38,7 @@ async def main(): # create the agent from the yaml async with ( AzureCliCredential() as credential, - AgentFactory(client_kwargs={"credential": credential}).create_agent_from_yaml(yaml_definition) as agent, + AgentFactory(client_kwargs={"credential": credential}, safe_mode=False).create_agent_from_yaml(yaml_definition) as agent, ): response = await agent.run("What can you do for me?") print("Agent response:", response.text) From 5c41a4e8314bfc699a67ef021e8d18daf1cbb965 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:35:42 -0800 Subject: [PATCH 4/6] Fixed remaining declarative samples --- python/samples/02-agents/declarative/mcp_tool_yaml.py | 4 ++-- .../samples/02-agents/declarative/microsoft_learn_agent.py | 6 +++++- .../samples/02-agents/declarative/openai_responses_agent.py | 6 +++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/python/samples/02-agents/declarative/mcp_tool_yaml.py b/python/samples/02-agents/declarative/mcp_tool_yaml.py index 43d42fcbd6..eecf6f7a74 100644 --- a/python/samples/02-agents/declarative/mcp_tool_yaml.py +++ b/python/samples/02-agents/declarative/mcp_tool_yaml.py @@ -25,12 +25,12 @@ import asyncio -from agent_framework.declarative import AgentFactory from dotenv import load_dotenv -# Load environment variables load_dotenv() +from agent_framework.declarative import AgentFactory + # Example 1: OpenAI.Responses with API key authentication # Uses inline API key - suitable for OpenAI provider which supports headers YAML_OPENAI_WITH_API_KEY = """ diff --git a/python/samples/02-agents/declarative/microsoft_learn_agent.py b/python/samples/02-agents/declarative/microsoft_learn_agent.py index 7a346096ea..66715924b7 100644 --- a/python/samples/02-agents/declarative/microsoft_learn_agent.py +++ b/python/samples/02-agents/declarative/microsoft_learn_agent.py @@ -2,6 +2,10 @@ import asyncio from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + from agent_framework.declarative import AgentFactory from azure.identity.aio import AzureCliCredential @@ -15,7 +19,7 @@ async def main(): # create the agent from the yaml async with ( AzureCliCredential() as credential, - AgentFactory(client_kwargs={"credential": credential}).create_agent_from_yaml_path(yaml_path) as agent, + AgentFactory(client_kwargs={"credential": credential}, safe_mode=False).create_agent_from_yaml_path(yaml_path) as agent, ): response = await agent.run("How do I create a storage account with private endpoint using bicep?") print("Agent response:", response.text) diff --git a/python/samples/02-agents/declarative/openai_responses_agent.py b/python/samples/02-agents/declarative/openai_responses_agent.py index 2931168587..d73dc34446 100644 --- a/python/samples/02-agents/declarative/openai_responses_agent.py +++ b/python/samples/02-agents/declarative/openai_responses_agent.py @@ -2,6 +2,10 @@ import asyncio from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + from agent_framework.declarative import AgentFactory @@ -16,7 +20,7 @@ async def main(): yaml_str = f.read() # create the agent from the yaml - agent = AgentFactory().create_agent_from_yaml(yaml_str) + agent = AgentFactory(safe_mode=False).create_agent_from_yaml(yaml_str) # use the agent response = await agent.run("Why is the sky blue, answer in Dutch?") # Use response.value with try/except for safe parsing From 358e1b212e26674dcaf8ddc5ad8c065ea4dced33 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:46:46 -0800 Subject: [PATCH 5/6] Added lazy initialization for PowerFx engine --- .../agent_framework_declarative/_models.py | 29 ++++++++++++++----- .../02-agents/declarative/inline_yaml.py | 5 ++-- .../02-agents/declarative/mcp_tool_yaml.py | 3 +- .../declarative/microsoft_learn_agent.py | 5 ++-- .../declarative/openai_responses_agent.py | 3 +- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/python/packages/declarative/agent_framework_declarative/_models.py b/python/packages/declarative/agent_framework_declarative/_models.py index 1a95edf8f9..6aa359d7f7 100644 --- a/python/packages/declarative/agent_framework_declarative/_models.py +++ b/python/packages/declarative/agent_framework_declarative/_models.py @@ -5,20 +5,32 @@ import os from collections.abc import MutableMapping from contextvars import ContextVar -from typing import Any, Literal, TypeVar, Union +from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, overload from agent_framework._serialization import SerializationMixin -try: +if TYPE_CHECKING: from powerfx import Engine - engine: Engine | None = Engine() -except (ImportError, RuntimeError): - # ImportError: powerfx package not installed - # RuntimeError: .NET runtime not available or misconfigured - engine = None +_engine_initialized = False +_engine: Engine | None = None + + +def _get_engine() -> Engine | None: + """Lazily initialize the PowerFx engine on first use.""" + global _engine_initialized, _engine + if not _engine_initialized: + _engine_initialized = True + try: + from powerfx import Engine + + _engine = Engine() + except (ImportError, RuntimeError): + # ImportError: powerfx package not installed + # RuntimeError: .NET runtime not available or misconfigured + pass + return _engine -from typing import overload logger = logging.getLogger("agent_framework.declarative") @@ -47,6 +59,7 @@ def _try_powerfx_eval(value: str | None, log_value: bool = True) -> str | None: return value if not value.startswith("="): return value + engine = _get_engine() if engine is None: logger.warning( "PowerFx engine not available for evaluating values starting with '='. " diff --git a/python/samples/02-agents/declarative/inline_yaml.py b/python/samples/02-agents/declarative/inline_yaml.py index e7eb5a996f..2e8561086b 100644 --- a/python/samples/02-agents/declarative/inline_yaml.py +++ b/python/samples/02-agents/declarative/inline_yaml.py @@ -1,13 +1,12 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio +from agent_framework.declarative import AgentFactory +from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv load_dotenv() -from agent_framework.declarative import AgentFactory -from azure.identity.aio import AzureCliCredential - """ This sample shows how to create an agent using an inline YAML string rather than a file. diff --git a/python/samples/02-agents/declarative/mcp_tool_yaml.py b/python/samples/02-agents/declarative/mcp_tool_yaml.py index eecf6f7a74..27128b499a 100644 --- a/python/samples/02-agents/declarative/mcp_tool_yaml.py +++ b/python/samples/02-agents/declarative/mcp_tool_yaml.py @@ -25,12 +25,11 @@ import asyncio +from agent_framework.declarative import AgentFactory from dotenv import load_dotenv load_dotenv() -from agent_framework.declarative import AgentFactory - # Example 1: OpenAI.Responses with API key authentication # Uses inline API key - suitable for OpenAI provider which supports headers YAML_OPENAI_WITH_API_KEY = """ diff --git a/python/samples/02-agents/declarative/microsoft_learn_agent.py b/python/samples/02-agents/declarative/microsoft_learn_agent.py index 66715924b7..affd1563cb 100644 --- a/python/samples/02-agents/declarative/microsoft_learn_agent.py +++ b/python/samples/02-agents/declarative/microsoft_learn_agent.py @@ -2,13 +2,12 @@ import asyncio from pathlib import Path +from agent_framework.declarative import AgentFactory +from azure.identity.aio import AzureCliCredential from dotenv import load_dotenv load_dotenv() -from agent_framework.declarative import AgentFactory -from azure.identity.aio import AzureCliCredential - async def main(): """Create an agent from a declarative yaml specification and run it.""" diff --git a/python/samples/02-agents/declarative/openai_responses_agent.py b/python/samples/02-agents/declarative/openai_responses_agent.py index d73dc34446..3c8c0861fa 100644 --- a/python/samples/02-agents/declarative/openai_responses_agent.py +++ b/python/samples/02-agents/declarative/openai_responses_agent.py @@ -2,12 +2,11 @@ import asyncio from pathlib import Path +from agent_framework.declarative import AgentFactory from dotenv import load_dotenv load_dotenv() -from agent_framework.declarative import AgentFactory - async def main(): """Create an agent from a declarative yaml specification and run it.""" From 126a9633eb050a8651d2f9cb982a74c04a3aecb7 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:51:59 -0800 Subject: [PATCH 6/6] Small fix --- .../declarative/agent_framework_declarative/_loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/packages/declarative/agent_framework_declarative/_loader.py b/python/packages/declarative/agent_framework_declarative/_loader.py index 38ae550bf2..55817be8ea 100644 --- a/python/packages/declarative/agent_framework_declarative/_loader.py +++ b/python/packages/declarative/agent_framework_declarative/_loader.py @@ -452,7 +452,7 @@ def create_agent_from_dict(self, agent_def: dict[str, Any]) -> Agent: name=prompt_agent.name, description=prompt_agent.description, instructions=prompt_agent.instructions, - default_options=chat_options, + default_options=chat_options, # type: ignore[arg-type] ) async def create_agent_from_yaml_path_async(self, yaml_path: str | Path) -> Agent: @@ -569,7 +569,7 @@ async def create_agent_from_dict_async(self, agent_def: dict[str, Any]) -> Agent name=prompt_agent.name, description=prompt_agent.description, instructions=prompt_agent.instructions, - default_options=chat_options, + default_options=chat_options, # type: ignore[arg-type] ) async def _create_agent_with_provider(self, prompt_agent: PromptAgent, mapping: ProviderTypeMapping) -> Agent: