Python: added inline yaml sample (#2582)

* added inline yaml sample

* fixed some typos and added intro comment

* added description params and pass through to client

* add azure assistants

* fix tests

* observabiltiy mypy fix

* for some reason mypy doesn't accept a subclass

---------

Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2025-12-04 02:30:56 +01:00
committed by GitHub
Unverified
parent 42ffe59592
commit d774b64df0
12 changed files with 136 additions and 40 deletions
@@ -118,6 +118,7 @@ class AzureAIAgentClient(BaseChatClient):
agents_client: AgentsClient | None = None,
agent_id: str | None = None,
agent_name: str | None = None,
agent_description: str | None = None,
thread_id: str | None = None,
project_endpoint: str | None = None,
model_deployment_name: str | None = None,
@@ -135,6 +136,7 @@ class AzureAIAgentClient(BaseChatClient):
a new agent will be created (and deleted after the request). If neither agents_client
nor agent_id is provided, both will be created and managed automatically.
agent_name: The name to use when creating new agents.
agent_description: The description to use when creating new agents.
thread_id: Default thread ID to use for conversations. Can be overridden by
conversation_id property when making a request.
project_endpoint: The Azure AI Project endpoint URL.
@@ -215,6 +217,7 @@ class AzureAIAgentClient(BaseChatClient):
self.credential = async_credential
self.agent_id = agent_id
self.agent_name = agent_name
self.agent_description = agent_description
self.model_id = azure_ai_settings.model_deployment_name
self.thread_id = thread_id
self.should_cleanup_agent = should_cleanup_agent # Track whether we should delete the agent
@@ -311,6 +314,7 @@ class AzureAIAgentClient(BaseChatClient):
args: dict[str, Any] = {
"model": run_options["model"],
"name": agent_name,
"description": self.agent_description,
}
if "tools" in run_options:
args["tools"] = run_options["tools"]
@@ -1038,16 +1042,19 @@ class AzureAIAgentClient(BaseChatClient):
return run_id, tool_outputs, tool_approvals
def _update_agent_name(self, agent_name: str | None) -> None:
def _update_agent_name_and_description(self, agent_name: str | None, description: str | None) -> None:
"""Update the agent name in the chat client.
Args:
agent_name: The new name for the agent.
description: The new description for the agent.
"""
# This is a no-op in the base class, but can be overridden by subclasses
# to update the agent name in the client.
if agent_name and not self.agent_name:
self.agent_name = agent_name
if description and not self.agent_description:
self.agent_description = description
def service_url(self) -> str:
"""Get the service URL for the chat client.
@@ -62,6 +62,7 @@ class AzureAIClient(OpenAIBaseResponsesClient):
project_client: AIProjectClient | None = None,
agent_name: str | None = None,
agent_version: str | None = None,
agent_description: str | None = None,
conversation_id: str | None = None,
project_endpoint: str | None = None,
model_deployment_name: str | None = None,
@@ -77,6 +78,7 @@ class AzureAIClient(OpenAIBaseResponsesClient):
project_client: An existing AIProjectClient to use. If not provided, one will be created.
agent_name: The name to use when creating new agents or using existing agents.
agent_version: The version of the agent to use.
agent_description: The description to use when creating new agents.
conversation_id: Default conversation ID to use for conversations. Can be overridden by
conversation_id property when making a request.
project_endpoint: The Azure AI Project endpoint URL.
@@ -150,6 +152,7 @@ class AzureAIClient(OpenAIBaseResponsesClient):
# Initialize instance variables
self.agent_name = agent_name
self.agent_version = agent_version
self.agent_description = agent_description
self.use_latest_version = use_latest_version
self.project_client = project_client
self.credential = async_credential
@@ -280,7 +283,9 @@ class AzureAIClient(OpenAIBaseResponsesClient):
args["instructions"] = "".join(combined_instructions)
created_agent = await self.project_client.agents.create_version(
agent_name=self.agent_name, definition=PromptAgentDefinition(**args)
agent_name=self.agent_name,
definition=PromptAgentDefinition(**args),
description=self.agent_description,
)
self.agent_version = created_agent.version
@@ -352,16 +357,19 @@ class AzureAIClient(OpenAIBaseResponsesClient):
"""Initialize OpenAI client."""
self.client = self.project_client.get_openai_client() # type: ignore
def _update_agent_name(self, agent_name: str | None) -> None:
def _update_agent_name_and_description(self, agent_name: str | None, description: str | None = None) -> None:
"""Update the agent name in the chat client.
Args:
agent_name: The new name for the agent.
description: The new description for the agent.
"""
# This is a no-op in the base class, but can be overridden by subclasses
# to update the agent name in the client.
if agent_name and not self.agent_name:
self.agent_name = agent_name
if description and not self.agent_description:
self.agent_description = description
def get_mcp_tool(self, tool: HostedMCPTool) -> Any:
"""Get MCP tool from HostedMCPTool."""
@@ -86,6 +86,7 @@ def create_test_azure_ai_chat_client(
client.credential = None
client.agent_id = agent_id
client.agent_name = agent_name
client.agent_description = None
client.model_id = azure_ai_settings.model_deployment_name
client.thread_id = thread_id
client.should_cleanup_agent = should_cleanup_agent
@@ -441,34 +442,43 @@ async def test_azure_ai_chat_client_close_client_when_should_close_false(mock_ag
mock_agents_client.close.assert_not_called()
def test_azure_ai_chat_client_update_agent_name_when_current_is_none(mock_agents_client: MagicMock) -> None:
"""Test _update_agent_name updates name when current agent_name is None."""
def test_azure_ai_chat_client_update_agent_name_and_description_when_current_is_none(
mock_agents_client: MagicMock,
) -> None:
"""Test _update_agent_name_and_description updates name when current agent_name is None."""
chat_client = create_test_azure_ai_chat_client(mock_agents_client)
chat_client.agent_name = None # type: ignore
chat_client._update_agent_name("NewAgentName") # type: ignore
chat_client._update_agent_name_and_description("NewAgentName", "description") # type: ignore
assert chat_client.agent_name == "NewAgentName"
assert chat_client.agent_description == "description"
def test_azure_ai_chat_client_update_agent_name_when_current_exists(mock_agents_client: MagicMock) -> None:
"""Test _update_agent_name does not update when current agent_name exists."""
def test_azure_ai_chat_client_update_agent_name_and_description_when_current_exists(
mock_agents_client: MagicMock,
) -> None:
"""Test _update_agent_name_and_description does not update when current agent_name exists."""
chat_client = create_test_azure_ai_chat_client(mock_agents_client)
chat_client.agent_name = "ExistingName" # type: ignore
chat_client.agent_description = "ExistingDescription" # type: ignore
chat_client._update_agent_name("NewAgentName") # type: ignore
chat_client._update_agent_name_and_description("NewAgentName", "description") # type: ignore
assert chat_client.agent_name == "ExistingName"
assert chat_client.agent_description == "ExistingDescription"
def test_azure_ai_chat_client_update_agent_name_with_none_input(mock_agents_client: MagicMock) -> None:
"""Test _update_agent_name with None input."""
def test_azure_ai_chat_client_update_agent_name_and_description_with_none_input(mock_agents_client: MagicMock) -> None:
"""Test _update_agent_name_and_description with None input."""
chat_client = create_test_azure_ai_chat_client(mock_agents_client)
chat_client.agent_name = None # type: ignore
chat_client.agent_description = None # type: ignore
chat_client._update_agent_name(None) # type: ignore
chat_client._update_agent_name_and_description(None, None) # type: ignore
assert chat_client.agent_name is None
assert chat_client.agent_description is None
async def test_azure_ai_chat_client_create_run_options_with_messages(mock_agents_client: MagicMock) -> None:
@@ -84,6 +84,7 @@ def create_test_azure_ai_client(
client.credential = None
client.agent_name = agent_name
client.agent_version = agent_version
client.agent_description = None
client.use_latest_version = use_latest_version
client.model_id = azure_ai_settings.model_deployment_name
client.conversation_id = conversation_id
@@ -397,14 +398,14 @@ async def test_azure_ai_client_initialize_client(mock_project_client: MagicMock)
mock_project_client.get_openai_client.assert_called_once()
def test_azure_ai_client_update_agent_name(mock_project_client: MagicMock) -> None:
"""Test _update_agent_name method."""
def test_azure_ai_client_update_agent_name_and_description(mock_project_client: MagicMock) -> None:
"""Test _update_agent_name_and_description method."""
client = create_test_azure_ai_client(mock_project_client)
# Test updating agent name when current is None
with patch.object(client, "_update_agent_name") as mock_update:
with patch.object(client, "_update_agent_name_and_description") as mock_update:
mock_update.return_value = None
client._update_agent_name("new-agent") # type: ignore
client._update_agent_name_and_description("new-agent") # type: ignore
mock_update.assert_called_once_with("new-agent")
# Test behavior when agent name is updated
@@ -412,9 +413,9 @@ def test_azure_ai_client_update_agent_name(mock_project_client: MagicMock) -> No
client.agent_name = "test-agent" # Manually set for the test
# Test with None input
with patch.object(client, "_update_agent_name") as mock_update:
with patch.object(client, "_update_agent_name_and_description") as mock_update:
mock_update.return_value = None
client._update_agent_name(None) # type: ignore
client._update_agent_name_and_description(None) # type: ignore
mock_update.assert_called_once_with(None)
@@ -719,7 +719,7 @@ class ChatAgent(BaseAgent):
additional_properties=additional_chat_options or {}, # type: ignore
)
self._async_exit_stack = AsyncExitStack()
self._update_agent_name()
self._update_agent_name_and_description()
async def __aenter__(self) -> "Self":
"""Enter the async context manager.
@@ -755,15 +755,17 @@ class ChatAgent(BaseAgent):
"""
await self._async_exit_stack.aclose()
def _update_agent_name(self) -> None:
def _update_agent_name_and_description(self) -> None:
"""Update the agent name in the chat client.
Checks if the chat client supports agent name updates. The implementation
should check if there is already an agent name defined, and if not
set it to this value.
"""
if hasattr(self.chat_client, "_update_agent_name") and callable(self.chat_client._update_agent_name): # type: ignore[reportAttributeAccessIssue, attr-defined]
self.chat_client._update_agent_name(self.name) # type: ignore[reportAttributeAccessIssue, attr-defined]
if hasattr(self.chat_client, "_update_agent_name_and_description") and callable(
self.chat_client._update_agent_name_and_description
): # type: ignore[reportAttributeAccessIssue, attr-defined]
self.chat_client._update_agent_name_and_description(self.name, self.description) # type: ignore[reportAttributeAccessIssue, attr-defined]
async def run(
self,
@@ -27,6 +27,7 @@ class AzureOpenAIAssistantsClient(OpenAIAssistantsClient):
deployment_name: str | None = None,
assistant_id: str | None = None,
assistant_name: str | None = None,
assistant_description: str | None = None,
thread_id: str | None = None,
api_key: str | None = None,
endpoint: str | None = None,
@@ -49,6 +50,7 @@ class AzureOpenAIAssistantsClient(OpenAIAssistantsClient):
assistant_id: The ID of an Azure OpenAI assistant to use.
If not provided, a new assistant will be created (and deleted after the request).
assistant_name: The name to use when creating new assistants.
assistant_description: The description to use when creating new assistants.
thread_id: Default thread ID to use for conversations. Can be overridden by
conversation_id property when making a request.
If not provided, a new thread will be created (and deleted after the request).
@@ -155,6 +157,7 @@ class AzureOpenAIAssistantsClient(OpenAIAssistantsClient):
model_id=azure_openai_settings.chat_deployment_name,
assistant_id=assistant_id,
assistant_name=assistant_name,
assistant_description=assistant_description,
thread_id=thread_id,
async_client=async_client, # type: ignore[reportArgumentType]
default_headers=default_headers,
@@ -268,9 +268,9 @@ def _get_otlp_exporters(endpoints: list[str]) -> list["LogRecordExporter | SpanE
exporters: list["LogRecordExporter | SpanExporter | MetricExporter"] = []
for endpoint in endpoints:
exporters.append(OTLPLogExporter(endpoint=endpoint))
exporters.append(OTLPSpanExporter(endpoint=endpoint))
exporters.append(OTLPMetricExporter(endpoint=endpoint))
exporters.append(OTLPLogExporter(endpoint=endpoint)) # type: ignore[arg-type]
exporters.append(OTLPSpanExporter(endpoint=endpoint)) # type: ignore[arg-type]
exporters.append(OTLPMetricExporter(endpoint=endpoint)) # type: ignore[arg-type]
return exporters
@@ -64,6 +64,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
model_id: str | None = None,
assistant_id: str | None = None,
assistant_name: str | None = None,
assistant_description: str | None = None,
thread_id: str | None = None,
api_key: str | Callable[[], str | Awaitable[str]] | None = None,
org_id: str | None = None,
@@ -82,6 +83,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
assistant_id: The ID of an OpenAI assistant to use.
If not provided, a new assistant will be created (and deleted after the request).
assistant_name: The name to use when creating new assistants.
assistant_description: The description to use when creating new assistants.
thread_id: Default thread ID to use for conversations. Can be overridden by
conversation_id property when making a request.
If not provided, a new thread will be created (and deleted after the request).
@@ -147,6 +149,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
)
self.assistant_id: str | None = assistant_id
self.assistant_name: str | None = assistant_name
self.assistant_description: str | None = assistant_description
self.thread_id: str | None = thread_id
self._should_delete_assistant: bool = False
@@ -220,7 +223,11 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
raise ServiceInitializationError("Parameter 'model_id' is required for assistant creation.")
client = await self.ensure_client()
created_assistant = await client.beta.assistants.create(name=self.assistant_name, model=self.model_id)
created_assistant = await client.beta.assistants.create(
model=self.model_id,
description=self.assistant_description,
name=self.assistant_name,
)
self.assistant_id = created_assistant.id
self._should_delete_assistant = True
@@ -516,13 +523,16 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
return run_id, tool_outputs
def _update_agent_name(self, agent_name: str | None) -> None:
def _update_agent_name_and_description(self, agent_name: str | None, description: str | None = None) -> None:
"""Update the agent name in the chat client.
Args:
agent_name: The new name for the agent.
description: The new description for the agent.
"""
# This is a no-op in the base class, but can be overridden by subclasses
# to update the agent name in the client.
if agent_name and not self.assistant_name:
object.__setattr__(self, "assistant_name", agent_name)
self.assistant_name = agent_name
if description and not self.assistant_description:
self.assistant_description = description
@@ -872,36 +872,36 @@ def test_openai_assistants_client_convert_function_results_to_tool_output_mismat
assert tool_outputs[0].get("tool_call_id") == "call-456"
def test_openai_assistants_client_update_agent_name(mock_async_openai: MagicMock) -> None:
"""Test _update_agent_name method updates assistant_name when not already set."""
def test_openai_assistants_client_update_agent_name_and_description(mock_async_openai: MagicMock) -> None:
"""Test _update_agent_name_and_description method updates assistant_name when not already set."""
# Test updating agent name when assistant_name is None
chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name=None)
# Call the private method to update agent name
chat_client._update_agent_name("New Assistant Name") # type: ignore
chat_client._update_agent_name_and_description("New Assistant Name") # type: ignore
assert chat_client.assistant_name == "New Assistant Name"
def test_openai_assistants_client_update_agent_name_existing(mock_async_openai: MagicMock) -> None:
"""Test _update_agent_name method doesn't override existing assistant_name."""
def test_openai_assistants_client_update_agent_name_and_description_existing(mock_async_openai: MagicMock) -> None:
"""Test _update_agent_name_and_description method doesn't override existing assistant_name."""
# Test that existing assistant_name is not overridden
chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name="Existing Assistant")
# Call the private method to update agent name
chat_client._update_agent_name("New Assistant Name") # type: ignore
chat_client._update_agent_name_and_description("New Assistant Name") # type: ignore
# Should keep the existing name
assert chat_client.assistant_name == "Existing Assistant"
def test_openai_assistants_client_update_agent_name_none(mock_async_openai: MagicMock) -> None:
"""Test _update_agent_name method with None agent_name parameter."""
def test_openai_assistants_client_update_agent_name_and_description_none(mock_async_openai: MagicMock) -> None:
"""Test _update_agent_name_and_description method with None agent_name parameter."""
# Test that None agent_name doesn't change anything
chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name=None)
# Call the private method with None
chat_client._update_agent_name(None) # type: ignore
chat_client._update_agent_name_and_description(None) # type: ignore
# Should remain None
assert chat_client.assistant_name is None
@@ -47,7 +47,17 @@ Shows how to create an agent that can search and retrieve information from Micro
**Key concepts**: Azure AI Foundry integration, MCP server usage, async patterns, resource management
### 3. **Azure OpenAI Responses Agent** ([`azure_openai_responses_agent.py`](./azure_openai_responses_agent.py))
### 3. **Inline YAML Agent** ([`inline_yaml.py`](./inline_yaml.py))
Shows how to create an agent using an inline YAML string rather than a file.
- Uses Azure AI Foundry v2 Client with instructions.
**Requirements**: `pip install agent-framework-azure-ai --pre`
**Key concepts**: Inline YAML definition.
### 4. **Azure OpenAI Responses Agent** ([`azure_openai_responses_agent.py`](./azure_openai_responses_agent.py))
Illustrates a basic agent using Azure OpenAI with structured responses.
@@ -58,7 +68,7 @@ Illustrates a basic agent using Azure OpenAI with structured responses.
**Key concepts**: Azure OpenAI integration, credential management, structured outputs
### 4. **OpenAI Responses Agent** ([`openai_responses_agent.py`](./openai_responses_agent.py))
### 5. **OpenAI Responses Agent** ([`openai_responses_agent.py`](./openai_responses_agent.py))
Demonstrates the simplest possible agent using OpenAI directly.
@@ -243,6 +253,7 @@ Each sample can be run independently. Make sure you have the required environmen
# Run a specific sample
python get_weather_agent.py
python microsoft_learn_agent.py
python inline_yaml.py
python azure_openai_responses_agent.py
python openai_responses_agent.py
```
@@ -26,7 +26,7 @@ async def main():
# create the AgentFactory with a chat client and bindings
agent_factory = AgentFactory(
AzureOpenAIResponsesClient(credential=AzureCliCredential()),
chat_client=AzureOpenAIResponsesClient(credential=AzureCliCredential()),
bindings={"get_weather": get_weather},
)
# create the agent from the yaml
@@ -0,0 +1,44 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
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.
It uses a Azure AI Client so it needs the credential to be passed into the AgentFactory.
Prerequisites:
- `pip install agent-framework-azure-ai agent-framework-declarative --pre`
- Set the following environment variables in a .env file or your environment:
- AZURE_AI_PROJECT_ENDPOINT
- AZURE_OPENAI_MODEL
"""
async def main():
"""Create an agent from a declarative YAML specification and run it."""
yaml_definition = """kind: Prompt
name: DiagnosticAgent
displayName: Diagnostic Assistant
instructions: Specialized diagnostic and issue detection agent for systems with critical error protocol and automatic handoff capabilities
description: A agent that performs diagnostics on systems and can escalate issues when critical errors are detected.
model:
id: =Env.AZURE_OPENAI_MODEL
connection:
kind: remote
endpoint: =Env.AZURE_AI_PROJECT_ENDPOINT
"""
# create the agent from the yaml
async with (
AzureCliCredential() as credential,
AgentFactory(client_kwargs={"async_credential": credential}).create_agent_from_yaml(yaml_definition) as agent,
):
response = await agent.run("What can you do for me?")
print("Agent response:", response.text)
if __name__ == "__main__":
asyncio.run(main())