mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Rebase durable task feature branch with main (#2806)
This commit is contained in:
@@ -43,7 +43,7 @@ from agent_framework import (
|
||||
use_function_invocation,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException
|
||||
from agent_framework.observability import use_observability
|
||||
from agent_framework.observability import use_instrumentation
|
||||
from azure.ai.agents.aio import AgentsClient
|
||||
from azure.ai.agents.models import (
|
||||
Agent,
|
||||
@@ -63,6 +63,8 @@ from azure.ai.agents.models import (
|
||||
McpTool,
|
||||
MessageDeltaChunk,
|
||||
MessageDeltaTextContent,
|
||||
MessageDeltaTextFileCitationAnnotation,
|
||||
MessageDeltaTextFilePathAnnotation,
|
||||
MessageDeltaTextUrlCitationAnnotation,
|
||||
MessageImageUrlParam,
|
||||
MessageInputContentBlock,
|
||||
@@ -105,7 +107,7 @@ TAzureAIAgentClient = TypeVar("TAzureAIAgentClient", bound="AzureAIAgentClient")
|
||||
|
||||
|
||||
@use_function_invocation
|
||||
@use_observability
|
||||
@use_instrumentation
|
||||
@use_chat_middleware
|
||||
class AzureAIAgentClient(BaseChatClient):
|
||||
"""Azure AI Agent Chat client."""
|
||||
@@ -471,6 +473,45 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
|
||||
return url_citations
|
||||
|
||||
def _extract_file_path_contents(self, message_delta_chunk: MessageDeltaChunk) -> list[HostedFileContent]:
|
||||
"""Extract file references from MessageDeltaChunk annotations.
|
||||
|
||||
Code interpreter generates files that are referenced via file path or file citation
|
||||
annotations in the message content. This method extracts those file IDs and returns
|
||||
them as HostedFileContent objects.
|
||||
|
||||
Handles two annotation types:
|
||||
- MessageDeltaTextFilePathAnnotation: Contains file_path.file_id
|
||||
- MessageDeltaTextFileCitationAnnotation: Contains file_citation.file_id
|
||||
|
||||
Args:
|
||||
message_delta_chunk: The message delta chunk to process
|
||||
|
||||
Returns:
|
||||
List of HostedFileContent objects for any files referenced in annotations
|
||||
"""
|
||||
file_contents: list[HostedFileContent] = []
|
||||
|
||||
for content in message_delta_chunk.delta.content:
|
||||
if isinstance(content, MessageDeltaTextContent) and content.text and content.text.annotations:
|
||||
for annotation in content.text.annotations:
|
||||
if isinstance(annotation, MessageDeltaTextFilePathAnnotation):
|
||||
# Extract file_id from the file_path annotation
|
||||
file_path = getattr(annotation, "file_path", None)
|
||||
if file_path is not None:
|
||||
file_id = getattr(file_path, "file_id", None)
|
||||
if file_id:
|
||||
file_contents.append(HostedFileContent(file_id=file_id))
|
||||
elif isinstance(annotation, MessageDeltaTextFileCitationAnnotation):
|
||||
# Extract file_id from the file_citation annotation
|
||||
file_citation = getattr(annotation, "file_citation", None)
|
||||
if file_citation is not None:
|
||||
file_id = getattr(file_citation, "file_id", None)
|
||||
if file_id:
|
||||
file_contents.append(HostedFileContent(file_id=file_id))
|
||||
|
||||
return file_contents
|
||||
|
||||
def _get_real_url_from_citation_reference(
|
||||
self, citation_url: str, azure_search_tool_calls: list[dict[str, Any]]
|
||||
) -> str:
|
||||
@@ -530,6 +571,9 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
# Extract URL citations from the delta chunk
|
||||
url_citations = self._extract_url_citations(event_data, azure_search_tool_calls)
|
||||
|
||||
# Extract file path contents from code interpreter outputs
|
||||
file_contents = self._extract_file_path_contents(event_data)
|
||||
|
||||
# Create contents with citations if any exist
|
||||
citation_content: list[Contents] = []
|
||||
if event_data.text or url_citations:
|
||||
@@ -538,6 +582,9 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
text_content_obj.annotations = url_citations
|
||||
citation_content.append(text_content_obj)
|
||||
|
||||
# Add file contents from file path annotations
|
||||
citation_content.extend(file_contents)
|
||||
|
||||
yield ChatResponseUpdate(
|
||||
role=role,
|
||||
contents=citation_content if citation_content else None,
|
||||
|
||||
@@ -15,7 +15,7 @@ from agent_framework import (
|
||||
use_function_invocation,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError
|
||||
from agent_framework.observability import use_observability
|
||||
from agent_framework.observability import use_instrumentation
|
||||
from agent_framework.openai._responses_client import OpenAIBaseResponsesClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import (
|
||||
@@ -49,7 +49,7 @@ TAzureAIClient = TypeVar("TAzureAIClient", bound="AzureAIClient")
|
||||
|
||||
|
||||
@use_function_invocation
|
||||
@use_observability
|
||||
@use_instrumentation
|
||||
@use_chat_middleware
|
||||
class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
"""Azure AI Agent client."""
|
||||
@@ -164,27 +164,94 @@ class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
# Track whether we should close client connection
|
||||
self._should_close_client = should_close_client
|
||||
|
||||
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.
|
||||
async def configure_azure_monitor(
|
||||
self,
|
||||
enable_sensitive_data: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Setup observability with Azure Monitor (Azure AI Foundry integration).
|
||||
|
||||
This will take the connection string from the project project_client.
|
||||
It will override any connection string that is set in the environment variables.
|
||||
It will disable any OTLP endpoint that might have been set.
|
||||
This method configures Azure Monitor for telemetry collection using the
|
||||
connection string from the Azure AI project client.
|
||||
|
||||
Args:
|
||||
enable_sensitive_data: Enable sensitive data logging (prompts, responses).
|
||||
Should only be enabled in development/test environments. Default is False.
|
||||
**kwargs: Additional arguments passed to configure_azure_monitor().
|
||||
Common options include:
|
||||
- enable_live_metrics (bool): Enable Azure Monitor Live Metrics
|
||||
- credential (TokenCredential): Azure credential for Entra ID auth
|
||||
- resource (Resource): Custom OpenTelemetry resource
|
||||
See https://learn.microsoft.com/python/api/azure-monitor-opentelemetry/azure.monitor.opentelemetry.configure_azure_monitor
|
||||
for full list of options.
|
||||
|
||||
Raises:
|
||||
ImportError: If azure-monitor-opentelemetry-exporter is not installed.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.identity.aio import DefaultAzureCredential
|
||||
|
||||
async with (
|
||||
DefaultAzureCredential() as credential,
|
||||
AIProjectClient(
|
||||
endpoint="https://your-project.api.azureml.ms", credential=credential
|
||||
) as project_client,
|
||||
AzureAIClient(project_client=project_client) as client,
|
||||
):
|
||||
# Setup observability with defaults
|
||||
await client.configure_azure_monitor()
|
||||
|
||||
# With live metrics enabled
|
||||
await client.configure_azure_monitor(enable_live_metrics=True)
|
||||
|
||||
# With sensitive data logging (dev/test only)
|
||||
await client.configure_azure_monitor(enable_sensitive_data=True)
|
||||
|
||||
Note:
|
||||
This method retrieves the Application Insights connection string from the
|
||||
Azure AI project client automatically. You must have Application Insights
|
||||
configured in your Azure AI project for this to work.
|
||||
"""
|
||||
# Get connection string from project client
|
||||
try:
|
||||
conn_string = await self.project_client.telemetry.get_application_insights_connection_string()
|
||||
except ResourceNotFoundError:
|
||||
logger.warning(
|
||||
"No Application Insights connection string found for the Azure AI Project, "
|
||||
"please call setup_observability() manually."
|
||||
"No Application Insights connection string found for the Azure AI Project. "
|
||||
"Please ensure Application Insights is configured in your Azure AI project, "
|
||||
"or call configure_otel_providers() manually with custom exporters."
|
||||
)
|
||||
return
|
||||
from agent_framework.observability import setup_observability
|
||||
|
||||
setup_observability(
|
||||
applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data
|
||||
# Import Azure Monitor with proper error handling
|
||||
try:
|
||||
from azure.monitor.opentelemetry import configure_azure_monitor
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"azure-monitor-opentelemetry is required for Azure Monitor integration. "
|
||||
"Install it with: pip install azure-monitor-opentelemetry"
|
||||
) from exc
|
||||
|
||||
from agent_framework.observability import create_metric_views, create_resource, enable_instrumentation
|
||||
|
||||
# Create resource if not provided in kwargs
|
||||
if "resource" not in kwargs:
|
||||
kwargs["resource"] = create_resource()
|
||||
|
||||
# Configure Azure Monitor with connection string and kwargs
|
||||
configure_azure_monitor(
|
||||
connection_string=conn_string,
|
||||
views=create_metric_views(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Complete setup with core observability
|
||||
enable_instrumentation(enable_sensitive_data=enable_sensitive_data)
|
||||
|
||||
async def __aenter__(self) -> "Self":
|
||||
"""Async context manager entry."""
|
||||
return self
|
||||
@@ -268,6 +335,10 @@ class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
|
||||
if "tools" in run_options:
|
||||
args["tools"] = run_options["tools"]
|
||||
if "temperature" in run_options:
|
||||
args["temperature"] = run_options["temperature"]
|
||||
if "top_p" in run_options:
|
||||
args["top_p"] = run_options["top_p"]
|
||||
|
||||
if "response_format" in run_options:
|
||||
response_format = run_options["response_format"]
|
||||
@@ -346,7 +417,7 @@ class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
|
||||
# Remove properties that are not supported on request level
|
||||
# but were configured on agent level
|
||||
exclude = ["model", "tools", "response_format"]
|
||||
exclude = ["model", "tools", "response_format", "temperature", "top_p"]
|
||||
|
||||
for property in exclude:
|
||||
run_options.pop(property, None)
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b251209"
|
||||
version = "1.0.0b251216"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -24,6 +24,7 @@ from agent_framework import (
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileContent,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedVectorStoreContent,
|
||||
@@ -42,6 +43,8 @@ from azure.ai.agents.models import (
|
||||
FileInfo,
|
||||
MessageDeltaChunk,
|
||||
MessageDeltaTextContent,
|
||||
MessageDeltaTextFileCitationAnnotation,
|
||||
MessageDeltaTextFilePathAnnotation,
|
||||
MessageDeltaTextUrlCitationAnnotation,
|
||||
RequiredFunctionToolCall,
|
||||
RequiredMcpToolCall,
|
||||
@@ -1362,6 +1365,108 @@ def test_azure_ai_chat_client_extract_url_citations_with_citations(mock_agents_c
|
||||
assert citation.annotated_regions[0].end_index == 20
|
||||
|
||||
|
||||
def test_azure_ai_chat_client_extract_file_path_contents_with_file_path_annotation(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _extract_file_path_contents with MessageDeltaChunk containing file path annotation."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
# Create mock file_path annotation
|
||||
mock_file_path = MagicMock()
|
||||
mock_file_path.file_id = "assistant-test-file-123"
|
||||
|
||||
mock_annotation = MagicMock(spec=MessageDeltaTextFilePathAnnotation)
|
||||
mock_annotation.file_path = mock_file_path
|
||||
|
||||
# Create mock text content with annotations
|
||||
mock_text = MagicMock()
|
||||
mock_text.annotations = [mock_annotation]
|
||||
|
||||
mock_text_content = MagicMock(spec=MessageDeltaTextContent)
|
||||
mock_text_content.text = mock_text
|
||||
|
||||
# Create mock delta
|
||||
mock_delta = MagicMock()
|
||||
mock_delta.content = [mock_text_content]
|
||||
|
||||
# Create mock MessageDeltaChunk
|
||||
mock_chunk = MagicMock(spec=MessageDeltaChunk)
|
||||
mock_chunk.delta = mock_delta
|
||||
|
||||
# Call the method
|
||||
file_contents = chat_client._extract_file_path_contents(mock_chunk)
|
||||
|
||||
# Verify results
|
||||
assert len(file_contents) == 1
|
||||
assert isinstance(file_contents[0], HostedFileContent)
|
||||
assert file_contents[0].file_id == "assistant-test-file-123"
|
||||
|
||||
|
||||
def test_azure_ai_chat_client_extract_file_path_contents_with_file_citation_annotation(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _extract_file_path_contents with MessageDeltaChunk containing file citation annotation."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
# Create mock file_citation annotation
|
||||
mock_file_citation = MagicMock()
|
||||
mock_file_citation.file_id = "cfile_test-citation-456"
|
||||
|
||||
mock_annotation = MagicMock(spec=MessageDeltaTextFileCitationAnnotation)
|
||||
mock_annotation.file_citation = mock_file_citation
|
||||
|
||||
# Create mock text content with annotations
|
||||
mock_text = MagicMock()
|
||||
mock_text.annotations = [mock_annotation]
|
||||
|
||||
mock_text_content = MagicMock(spec=MessageDeltaTextContent)
|
||||
mock_text_content.text = mock_text
|
||||
|
||||
# Create mock delta
|
||||
mock_delta = MagicMock()
|
||||
mock_delta.content = [mock_text_content]
|
||||
|
||||
# Create mock MessageDeltaChunk
|
||||
mock_chunk = MagicMock(spec=MessageDeltaChunk)
|
||||
mock_chunk.delta = mock_delta
|
||||
|
||||
# Call the method
|
||||
file_contents = chat_client._extract_file_path_contents(mock_chunk)
|
||||
|
||||
# Verify results
|
||||
assert len(file_contents) == 1
|
||||
assert isinstance(file_contents[0], HostedFileContent)
|
||||
assert file_contents[0].file_id == "cfile_test-citation-456"
|
||||
|
||||
|
||||
def test_azure_ai_chat_client_extract_file_path_contents_empty_annotations(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _extract_file_path_contents with no annotations returns empty list."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
# Create mock text content with no annotations
|
||||
mock_text = MagicMock()
|
||||
mock_text.annotations = []
|
||||
|
||||
mock_text_content = MagicMock(spec=MessageDeltaTextContent)
|
||||
mock_text_content.text = mock_text
|
||||
|
||||
# Create mock delta
|
||||
mock_delta = MagicMock()
|
||||
mock_delta.content = [mock_text_content]
|
||||
|
||||
# Create mock MessageDeltaChunk
|
||||
mock_chunk = MagicMock(spec=MessageDeltaChunk)
|
||||
mock_chunk.delta = mock_delta
|
||||
|
||||
# Call the method
|
||||
file_contents = chat_client._extract_file_path_contents(mock_chunk)
|
||||
|
||||
# Verify results
|
||||
assert len(file_contents) == 0
|
||||
|
||||
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
|
||||
@@ -477,6 +477,30 @@ async def test_azure_ai_client_agent_creation_with_instructions(
|
||||
assert call_args[1]["definition"].instructions == "Message instructions. Option instructions. "
|
||||
|
||||
|
||||
async def test_azure_ai_client_agent_creation_with_additional_args(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test agent creation with additional arguments."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
|
||||
|
||||
# Mock agent creation response
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.name = "test-agent"
|
||||
mock_agent.version = "1.0"
|
||||
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
|
||||
|
||||
run_options = {"model": "test-model", "temperature": 0.9, "top_p": 0.8}
|
||||
messages_instructions = "Message instructions. "
|
||||
|
||||
await client._get_agent_reference_or_create(run_options, messages_instructions) # type: ignore
|
||||
|
||||
# Verify agent was created with provided arguments
|
||||
call_args = mock_project_client.agents.create_version.call_args
|
||||
definition = call_args[1]["definition"]
|
||||
assert definition.temperature == 0.9
|
||||
assert definition.top_p == 0.8
|
||||
|
||||
|
||||
async def test_azure_ai_client_agent_creation_with_tools(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user