Skip to content
Merged
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
31 changes: 30 additions & 1 deletion python/packages/azure-ai/agent_framework_azure_ai/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
)
from azure.core.credentials_async import AsyncTokenCredential
from azure.core.exceptions import ResourceNotFoundError
from pydantic import ValidationError
from openai.types.responses.parsed_response import (
ParsedResponse,
)
from openai.types.responses.response import Response as OpenAIResponse
from pydantic import BaseModel, ValidationError

from ._shared import AzureAISettings

Expand Down Expand Up @@ -279,6 +283,19 @@ async def prepare_options(

run_options["extra_body"] = {"agent": agent_reference}

conversation_id = chat_options.conversation_id or self.conversation_id

# Handle different conversation ID formats
if conversation_id:
if conversation_id.startswith("resp_"):
# For response IDs, set previous_response_id and remove conversation property
run_options.pop("conversation", None)
run_options["previous_response_id"] = conversation_id
elif conversation_id.startswith("conv_"):
# For conversation IDs, set conversation and remove previous_response_id property
run_options.pop("previous_response_id", None)
run_options["conversation"] = conversation_id

# Remove properties that are not supported on request level
# but were configured on agent level
exclude = ["model", "tools", "response_format"]
Expand Down Expand Up @@ -325,3 +342,15 @@ def get_mcp_tool(self, tool: HostedMCPTool) -> MutableMapping[str, Any]:
mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}}

return mcp

def get_conversation_id(self, response: OpenAIResponse | ParsedResponse[BaseModel], store: bool) -> str | None:
"""Get the conversation ID from the response if store is True."""
if store:
# If conversation ID exists, it means that we operate with conversation
# so we use conversation ID as input and output.
if response.conversation and response.conversation.id:
return response.conversation.id
# If conversation ID doesn't exist, we operate with responses
# so we use response ID as input and output.
return response.id
return None
188 changes: 188 additions & 0 deletions python/packages/azure-ai/tests/test_azure_ai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from azure.ai.projects.models import (
ResponseTextFormatConfigurationJsonSchema,
)
from openai.types.responses.parsed_response import ParsedResponse
from openai.types.responses.response import Response as OpenAIResponse
from pydantic import BaseModel, ConfigDict, ValidationError

from agent_framework_azure_ai import AzureAIClient, AzureAISettings
Expand Down Expand Up @@ -537,6 +539,192 @@ async def test_azure_ai_client_prepare_options_excludes_response_format(
assert run_options["extra_body"]["agent"]["name"] == "test-agent"


async def test_azure_ai_client_prepare_options_with_resp_conversation_id(
mock_project_client: MagicMock,
) -> None:
"""Test prepare_options with conversation ID starting with 'resp_'."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")

messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
chat_options = ChatOptions(conversation_id="resp_12345")

with (
patch.object(
client.__class__.__bases__[0],
"prepare_options",
return_value={"model": "test-model", "previous_response_id": "old_value", "conversation": "old_conv"},
),
patch.object(
client,
"_get_agent_reference_or_create",
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
),
):
run_options = await client.prepare_options(messages, chat_options)

# Should set previous_response_id and remove conversation property
assert run_options["previous_response_id"] == "resp_12345"
assert "conversation" not in run_options


async def test_azure_ai_client_prepare_options_with_conv_conversation_id(
mock_project_client: MagicMock,
) -> None:
"""Test prepare_options with conversation ID starting with 'conv_'."""
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")

messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
chat_options = ChatOptions(conversation_id="conv_67890")

with (
patch.object(
client.__class__.__bases__[0],
"prepare_options",
return_value={"model": "test-model", "previous_response_id": "old_value", "conversation": "old_conv"},
),
patch.object(
client,
"_get_agent_reference_or_create",
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
),
):
run_options = await client.prepare_options(messages, chat_options)

# Should set conversation and remove previous_response_id property
assert run_options["conversation"] == "conv_67890"
assert "previous_response_id" not in run_options


async def test_azure_ai_client_prepare_options_with_client_conversation_id(
mock_project_client: MagicMock,
) -> None:
"""Test prepare_options using client's default conversation ID when chat options don't have one."""
client = create_test_azure_ai_client(
mock_project_client, agent_name="test-agent", agent_version="1.0", conversation_id="resp_client_default"
)

messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
chat_options = ChatOptions() # No conversation_id specified

with (
patch.object(
client.__class__.__bases__[0],
"prepare_options",
return_value={"model": "test-model", "previous_response_id": "old_value", "conversation": "old_conv"},
),
patch.object(
client,
"_get_agent_reference_or_create",
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
),
):
run_options = await client.prepare_options(messages, chat_options)

# Should use client's default conversation_id and set previous_response_id
assert run_options["previous_response_id"] == "resp_client_default"
assert "conversation" not in run_options


def test_get_conversation_id_with_store_true_and_conversation_id() -> None:
"""Test get_conversation_id returns conversation ID when store is True and conversation exists."""
client = create_test_azure_ai_client(MagicMock())

# Mock OpenAI response with conversation
mock_response = MagicMock(spec=OpenAIResponse)
mock_response.id = "resp_12345"
mock_conversation = MagicMock()
mock_conversation.id = "conv_67890"
mock_response.conversation = mock_conversation

result = client.get_conversation_id(mock_response, store=True)

assert result == "conv_67890"


def test_get_conversation_id_with_store_true_and_no_conversation() -> None:
"""Test get_conversation_id returns response ID when store is True and no conversation exists."""
client = create_test_azure_ai_client(MagicMock())

# Mock OpenAI response without conversation
mock_response = MagicMock(spec=OpenAIResponse)
mock_response.id = "resp_12345"
mock_response.conversation = None

result = client.get_conversation_id(mock_response, store=True)

assert result == "resp_12345"


def test_get_conversation_id_with_store_true_and_empty_conversation_id() -> None:
"""Test get_conversation_id returns response ID when store is True and conversation ID is empty."""
client = create_test_azure_ai_client(MagicMock())

# Mock OpenAI response with conversation but empty ID
mock_response = MagicMock(spec=OpenAIResponse)
mock_response.id = "resp_12345"
mock_conversation = MagicMock()
mock_conversation.id = ""
mock_response.conversation = mock_conversation

result = client.get_conversation_id(mock_response, store=True)

assert result == "resp_12345"


def test_get_conversation_id_with_store_false() -> None:
"""Test get_conversation_id returns None when store is False."""
client = create_test_azure_ai_client(MagicMock())

# Mock OpenAI response with conversation
mock_response = MagicMock(spec=OpenAIResponse)
mock_response.id = "resp_12345"
mock_conversation = MagicMock()
mock_conversation.id = "conv_67890"
mock_response.conversation = mock_conversation

result = client.get_conversation_id(mock_response, store=False)

assert result is None


def test_get_conversation_id_with_parsed_response_and_store_true() -> None:
"""Test get_conversation_id works with ParsedResponse when store is True."""
client = create_test_azure_ai_client(MagicMock())

# Create a simple BaseModel for testing
class TestModel(BaseModel):
content: str = "test"

# Mock ParsedResponse with conversation
mock_response = MagicMock(spec=ParsedResponse[BaseModel])
mock_response.id = "resp_parsed_12345"
mock_conversation = MagicMock()
mock_conversation.id = "conv_parsed_67890"
mock_response.conversation = mock_conversation

result = client.get_conversation_id(mock_response, store=True)

assert result == "conv_parsed_67890"


def test_get_conversation_id_with_parsed_response_no_conversation() -> None:
"""Test get_conversation_id returns response ID with ParsedResponse when no conversation exists."""
client = create_test_azure_ai_client(MagicMock())

# Create a simple BaseModel for testing
class TestModel(BaseModel):
content: str = "test"

# Mock ParsedResponse without conversation
mock_response = MagicMock(spec=ParsedResponse[BaseModel])
mock_response.id = "resp_parsed_12345"
mock_response.conversation = None

result = client.get_conversation_id(mock_response, store=True)

assert result == "resp_parsed_12345"


@pytest.fixture
def mock_project_client() -> MagicMock:
"""Fixture that provides a mock AIProjectClient."""
Expand Down
1 change: 1 addition & 0 deletions python/samples/getting_started/agents/azure_ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This folder contains examples demonstrating different ways to create and use age
| [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation using the `use_latest_version=True` parameter. |
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the `HostedCodeInterpreterTool` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. |
| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent name and version to the Azure AI client. Demonstrates agent reuse patterns for production scenarios. |
| [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Demonstrates how to use an existing conversation created on the service side with Azure AI agents. Shows two approaches: specifying conversation ID at the client level and using AgentThread with an existing conversation ID. |
| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIClient` settings, including project endpoint, model deployment, and credentials rather than relying on environment variable defaults. |
| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use the `HostedFileSearchTool` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. |
| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async def streaming_example() -> None:
tools=get_weather,
) as agent,
):
query = "What's the weather like in Portland?"
query = "What's the weather like in Tokyo?"
print(f"User: {query}")
print("Agent: ", end="", flush=True)
async for chunk in agent.run_stream(query):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from random import randint
from typing import Annotated

from agent_framework.azure import AzureAIClient
from azure.ai.projects.aio import AIProjectClient
from azure.identity.aio import AzureCliCredential
from pydantic import Field

"""
Azure AI Agent Existing Conversation Example

This sample demonstrates usage of AzureAIClient with existing conversation created on service side.
"""


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 example_with_client() -> None:
"""Example shows how to specify existing conversation ID when initializing Azure AI Client."""
print("=== Azure AI Agent With Existing Conversation and Client ===")
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
):
# Create a conversation using OpenAI client
openai_client = await project_client.get_openai_client()
conversation = await openai_client.conversations.create()
conversation_id = conversation.id
print(f"Conversation ID: {conversation_id}")

async with AzureAIClient(
project_client=project_client,
# Specify conversation ID on client level
conversation_id=conversation_id,
).create_agent(
name="BasicAgent",
instructions="You are a helpful agent.",
tools=get_weather,
) as agent:
query = "What's the weather like in Seattle?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}\n")

query = "What was my last question?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}\n")


async def example_with_thread() -> None:
"""This example shows how to specify existing conversation ID with AgentThread."""
print("=== Azure AI Agent With Existing Conversation and Thread ===")
async with (
AzureCliCredential() as credential,
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
AzureAIClient(project_client=project_client).create_agent(
name="BasicAgent",
instructions="You are a helpful agent.",
tools=get_weather,
) as agent,
):
# Create a conversation using OpenAI client
openai_client = await project_client.get_openai_client()
conversation = await openai_client.conversations.create()
conversation_id = conversation.id
print(f"Conversation ID: {conversation_id}")

# Create a thread with the existing ID
thread = agent.get_new_thread(service_thread_id=conversation_id)

query = "What's the weather like in Seattle?"
print(f"User: {query}")
result = await agent.run(query, thread=thread)
print(f"Agent: {result.text}\n")

query = "What was my last question?"
print(f"User: {query}")
result = await agent.run(query, thread=thread)
print(f"Agent: {result.text}\n")


async def main() -> None:
await example_with_client()
await example_with_thread()


if __name__ == "__main__":
asyncio.run(main())
Loading