Python: Included existing agent definition in requests to Azure AI (#1285)

* Fixed instructions handling for existing Azure AI agents

* Updated tool handling for existing agents

* Small update

* Added more comments
This commit is contained in:
Dmytro Struk
2025-10-08 13:33:57 -07:00
committed by GitHub
Unverified
parent c341ee7ed2
commit 7e891fab39
3 changed files with 80 additions and 29 deletions
@@ -42,6 +42,7 @@ from agent_framework._pydantic import AFBaseSettings
from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException
from agent_framework.observability import use_observability
from azure.ai.agents.models import (
Agent,
AgentsNamedToolChoice,
AgentsNamedToolChoiceType,
AgentsToolChoiceOptionMode,
@@ -55,6 +56,7 @@ from azure.ai.agents.models import (
CodeInterpreterToolDefinition,
FileSearchTool,
FunctionName,
FunctionToolDefinition,
ListSortOrder,
McpTool,
MessageDeltaChunk,
@@ -251,6 +253,7 @@ class AzureAIAgentClient(BaseChatClient):
self.thread_id = thread_id
self._should_delete_agent = False # Track whether we should delete the agent
self._should_close_client = should_close_client # Track whether we should close client connection
self._agent_definition: Agent | None = None # Cached definition for existing agent
async def setup_azure_ai_observability(self, enable_sensitive_data: bool | None = None) -> None:
"""Use this method to setup tracing in your Azure AI Project.
@@ -375,6 +378,7 @@ class AzureAIAgentClient(BaseChatClient):
args["response_format"] = run_options["response_format"]
created_agent = await self.project_client.agents.create_agent(**args)
self.agent_id = str(created_agent.id)
self._agent_definition = created_agent
self._should_delete_agent = True
return self.agent_id
@@ -669,6 +673,26 @@ class AzureAIAgentClient(BaseChatClient):
self.agent_id = None
self._should_delete_agent = False
async def _load_agent_definition_if_needed(self) -> Agent | None:
"""Load and cache agent details if not already loaded."""
if self._agent_definition is None and self.agent_id is not None:
self._agent_definition = await self.project_client.agents.get_agent(self.agent_id)
return self._agent_definition
def _prepare_tool_choice(self, chat_options: ChatOptions) -> None:
"""Prepare the tools and tool choice for the chat options.
Args:
chat_options: The chat options to prepare.
"""
chat_tool_mode = chat_options.tool_choice
if chat_tool_mode is None or chat_tool_mode == ToolMode.NONE or chat_tool_mode == "none":
chat_options.tools = None
chat_options.tool_choice = ToolMode.NONE.mode
return
chat_options.tool_choice = chat_tool_mode.mode if isinstance(chat_tool_mode, ToolMode) else chat_tool_mode
async def _create_run_options(
self,
messages: MutableSequence[ChatMessage],
@@ -677,6 +701,8 @@ class AzureAIAgentClient(BaseChatClient):
) -> tuple[dict[str, Any], list[FunctionResultContent | FunctionApprovalResponseContent] | None]:
run_options: dict[str, Any] = {**kwargs}
agent_definition = await self._load_agent_definition_if_needed()
if chat_options is not None:
run_options["max_completion_tokens"] = chat_options.max_tokens
if chat_options.model_id is not None:
@@ -687,11 +713,21 @@ class AzureAIAgentClient(BaseChatClient):
run_options["temperature"] = chat_options.temperature
run_options["parallel_tool_calls"] = chat_options.allow_multiple_tool_calls
tool_definitions: list[ToolDefinition | dict[str, Any]] = []
# Add tools from existing agent
if agent_definition is not None:
# Don't include function tools, since they will be passed through chat_options.tools
agent_tools = [tool for tool in agent_definition.tools if not isinstance(tool, FunctionToolDefinition)]
if agent_tools:
tool_definitions.extend(agent_tools)
if agent_definition.tool_resources:
run_options["tool_resources"] = agent_definition.tool_resources
if chat_options.tool_choice is not None:
if chat_options.tool_choice != "none" and chat_options.tools:
tool_definitions = await self._prep_tools(chat_options.tools, run_options)
if tool_definitions:
run_options["tools"] = tool_definitions
# Add run tools
tool_definitions.extend(await self._prep_tools(chat_options.tools, run_options))
# Handle MCP tool resources for approval mode
mcp_tools = [tool for tool in chat_options.tools if isinstance(tool, HostedMCPTool)]
@@ -740,6 +776,9 @@ class AzureAIAgentClient(BaseChatClient):
function=FunctionName(name=chat_options.tool_choice.required_function_name),
)
if tool_definitions:
run_options["tools"] = tool_definitions
if chat_options.response_format is not None:
run_options["response_format"] = ResponseFormatJsonSchemaType(
json_schema=ResponseFormatJsonSchema(
@@ -748,7 +787,7 @@ class AzureAIAgentClient(BaseChatClient):
)
)
instructions: list[str] = [chat_options.instructions] if chat_options and chat_options.instructions else []
instructions: list[str] = []
required_action_results: list[FunctionResultContent | FunctionApprovalResponseContent] | None = None
additional_messages: list[ThreadMessageOptions] | None = None
@@ -790,6 +829,10 @@ class AzureAIAgentClient(BaseChatClient):
if additional_messages is not None:
run_options["additional_messages"] = additional_messages
# Add instruction from existing agent at the beginning
if agent_definition is not None and agent_definition.instructions:
instructions.insert(0, agent_definition.instructions)
if len(instructions) > 0:
run_options["instructions"] = "".join(instructions)
@@ -81,11 +81,12 @@ def create_test_azure_ai_chat_client(
client.project_client = mock_ai_project_client
client.credential = None
client.agent_id = agent_id
client.agent_name = None
client.agent_name = agent_name
client.model_id = azure_ai_settings.model_deployment_name
client.thread_id = thread_id
client._should_delete_agent = should_delete_agent
client._should_close_client = False
client._should_delete_agent = should_delete_agent # type: ignore
client._should_close_client = False # type: ignore
client._agent_definition = None # type: ignore
client.additional_properties = {}
client.middleware = None
@@ -297,6 +298,9 @@ async def test_azure_ai_chat_client_tool_results_without_thread_error_via_public
"""Test that tool results without thread ID raise error through public API."""
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
# Mock get_agent
mock_ai_project_client.agents.get_agent = AsyncMock(return_value=None)
# Create messages with tool results but no thread/conversation ID
messages = [
ChatMessage(role=Role.USER, text="Hello"),
@@ -315,6 +319,9 @@ async def test_azure_ai_chat_client_thread_management_through_public_api(mock_ai
"""Test thread creation and management through public API."""
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
# Mock get_agent to avoid the async error
mock_ai_project_client.agents.get_agent = AsyncMock(return_value=None)
mock_thread = MagicMock()
mock_thread.id = "new-thread-456"
mock_ai_project_client.agents.threads.create = AsyncMock(return_value=mock_thread)
@@ -451,6 +458,9 @@ async def test_azure_ai_chat_client_create_run_options_with_image_content(mock_a
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
# Mock get_agent
mock_ai_project_client.agents.get_agent = AsyncMock(return_value=None)
image_content = UriContent(uri="https://example.com/image.jpg", media_type="image/jpeg")
messages = [ChatMessage(role=Role.USER, contents=[image_content])]
@@ -2,14 +2,11 @@
import asyncio
import os
from random import randint
from typing import Annotated
from agent_framework import ChatAgent
from agent_framework.azure import AzureAIAgentClient
from azure.ai.projects.aio import AIProjectClient
from azure.identity.aio import AzureCliCredential
from pydantic import Field
"""
Azure AI Agent with Existing Agent Example
@@ -19,14 +16,6 @@ agent IDs, showing agent reuse patterns for production scenarios.
"""
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def main() -> None:
print("=== Azure AI Chat Client with Existing Agent ===")
@@ -35,24 +24,33 @@ async def main() -> None:
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as client,
):
# Create an agent that will persist
created_agent = await client.agents.create_agent(
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], name="WeatherAgent"
azure_ai_agent = await client.agents.create_agent(
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
# Create remote agent with default instructions
# These instructions will persist on created agent for every run.
instructions="End each response with [END].",
)
chat_client = AzureAIAgentClient(project_client=client, agent_id=azure_ai_agent.id)
try:
async with ChatAgent(
# passing in the client is optional here, so if you take the agent_id from the portal
# you can use it directly without the two lines above.
chat_client=AzureAIAgentClient(project_client=client, agent_id=created_agent.id),
instructions="You are a helpful weather agent.",
tools=get_weather,
chat_client=chat_client,
# Instructions here are applicable only to this ChatAgent instance
# These instructions will be combined with instructions on existing remote agent.
# The final instructions during the execution will look like:
# "'End each response with [END]. Respond with 'Hello World' only'"
instructions="Respond with 'Hello World' only",
) as agent:
result = await agent.run("What's the weather like in Tokyo?")
print(f"Result: {result}\n")
query = "How are you?"
print(f"User: {query}")
result = await agent.run(query)
# Based on local and remote instructions, the result will be
# 'Hello World [END]'.
print(f"Agent: {result}\n")
finally:
# Clean up the agent manually
await client.agents.delete_agent(created_agent.id)
await client.agents.delete_agent(azure_ai_agent.id)
if __name__ == "__main__":