mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fixed declarative samples (#4051)
* Updated declarative kind mapping * Fixed required property handling * Updated inline yaml sample * Fixed remaining declarative samples * Added lazy initialization for PowerFx engine * Small fix
This commit is contained in:
committed by
GitHub
Unverified
parent
1b87a07377
commit
57da1bcfeb
@@ -452,7 +452,7 @@ class AgentFactory:
|
||||
name=prompt_agent.name,
|
||||
description=prompt_agent.description,
|
||||
instructions=prompt_agent.instructions,
|
||||
**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 @@ class AgentFactory:
|
||||
name=prompt_agent.name,
|
||||
description=prompt_agent.description,
|
||||
instructions=prompt_agent.instructions,
|
||||
**chat_options,
|
||||
default_options=chat_options, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
async def _create_agent_with_provider(self, prompt_agent: PromptAgent, mapping: ProviderTypeMapping) -> Agent:
|
||||
|
||||
@@ -5,20 +5,32 @@ import logging
|
||||
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 '='. "
|
||||
@@ -110,8 +123,12 @@ class Property(SerializationMixin):
|
||||
# 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)
|
||||
@@ -224,11 +241,21 @@ class PropertySchema(SerializationMixin):
|
||||
"""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
|
||||
|
||||
|
||||
|
||||
@@ -556,6 +556,58 @@ instructions: You are a helpful assistant.
|
||||
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."""
|
||||
|
||||
@@ -103,6 +103,50 @@ class TestProperty:
|
||||
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,29 @@ class TestPropertySchema:
|
||||
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": False, "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["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:
|
||||
"""Tests for Connection base class."""
|
||||
|
||||
@@ -7,6 +7,9 @@ from typing import Literal
|
||||
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:
|
||||
|
||||
@@ -3,6 +3,9 @@ import asyncio
|
||||
|
||||
from agent_framework.declarative import AgentFactory
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
"""
|
||||
This sample shows how to create an agent using an inline YAML string rather than a file.
|
||||
@@ -34,7 +37,7 @@ model:
|
||||
# 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)
|
||||
|
||||
@@ -28,7 +28,6 @@ import asyncio
|
||||
from agent_framework.declarative import AgentFactory
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Example 1: OpenAI.Responses with API key authentication
|
||||
|
||||
@@ -4,6 +4,9 @@ from pathlib import Path
|
||||
|
||||
from agent_framework.declarative import AgentFactory
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -15,7 +18,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)
|
||||
|
||||
@@ -3,6 +3,9 @@ import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework.declarative import AgentFactory
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
async def main():
|
||||
@@ -16,7 +19,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
|
||||
|
||||
Reference in New Issue
Block a user