[BREAKING] Python: fix OpenAI Azure routing and provider samples (#4925)

* Python: fix OpenAI Azure routing and provider samples

Prefer OpenAI when OPENAI_API_KEY is present unless Azure is explicitly requested. Clarify constructor docs, keep deprecated Azure wrappers compatible with stricter settings validation, and refresh the provider samples and tests to use the current client patterns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix bandit

* Python: align OpenAI embedding Azure routing

Extend the shared OpenAI-vs-Azure routing and credential behavior to the embedding client, add Azure embedding regression coverage, and refresh the embedding samples to use the generic client path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix embedding client pyright check

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: thin OpenAI embedding wrapper

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: document embedding overload routing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix callable OpenAI key routing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: fix Azure credential routing tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: address OpenAI review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: narrow Azure routing markers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: refine OpenAI model fallback order

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: narrow Azure deployment docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: remove embedding routing wording

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: run embedding Azure integration tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* changed variable name

* Python: expand OpenAI package README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* clarified readme

* Python: fix Azure OpenAI integration setup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: correct Azure integration env mapping

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* updated code to fix int tests

* test updates

* test fix

* fix test setup

* updates to tests and setup

* remove openai assistants int tests

* improvements in int tests

* fix env var

* fix env vars

* fix azure responses test

* trigger actions

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-03-27 14:33:39 +01:00
committed by GitHub
Unverified
parent 3611be82cf
commit cc0cfaaac8
103 changed files with 5451 additions and 4216 deletions
@@ -2,10 +2,9 @@
import importlib.metadata
from ._foundry_agent import FoundryAgent, RawFoundryAgent
from ._foundry_agent_client import RawFoundryAgentChatClient
from ._foundry_chat_client import FoundryChatClient, FoundryChatOptions, RawFoundryChatClient
from ._foundry_memory_provider import FoundryMemoryProvider
from ._agent import FoundryAgent, RawFoundryAgent, RawFoundryAgentChatClient
from ._chat_client import FoundryChatClient, FoundryChatOptions, RawFoundryChatClient
from ._memory_provider import FoundryMemoryProvider
try:
__version__ = importlib.metadata.version(__name__)
@@ -1,30 +1,37 @@
# Copyright (c) Microsoft. All rights reserved.
"""Microsoft Foundry Agent client for connecting to pre-configured agents in Foundry.
"""Microsoft Foundry Agent for connecting to pre-configured agents in Foundry.
This module provides ``RawFoundryAgentClient`` and ``FoundryAgentClient`` for
communicating with PromptAgents and HostedAgents via the Responses API.
This module provides ``RawFoundryAgent`` and ``FoundryAgent`` Agent subclasses
that connect to existing PromptAgents or HostedAgents in Foundry. Use
``FoundryAgent`` for the recommended experience with full middleware and telemetry.
"""
from __future__ import annotations
import logging
import sys
from collections.abc import Callable, Mapping, MutableMapping, Sequence
from collections.abc import Awaitable, Callable, Mapping, MutableMapping, Sequence
from typing import TYPE_CHECKING, Any, ClassVar, Generic, cast
from agent_framework._middleware import ChatMiddlewareLayer
from agent_framework._settings import load_settings
from agent_framework._telemetry import AGENT_FRAMEWORK_USER_AGENT
from agent_framework._tools import FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool
from agent_framework._types import Message
from agent_framework.observability import ChatTelemetryLayer
from agent_framework import (
AGENT_FRAMEWORK_USER_AGENT,
AgentMiddlewareLayer,
BaseContextProvider,
ChatAndFunctionMiddlewareTypes,
ChatMiddlewareLayer,
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
Message,
RawAgent,
load_settings,
)
from agent_framework.observability import AgentTelemetryLayer, ChatTelemetryLayer
from agent_framework_openai._chat_client import OpenAIChatOptions, RawOpenAIChatClient
from azure.ai.projects.aio import AIProjectClient
from ._entra_id_authentication import AzureCredentialTypes
logger: logging.Logger = logging.getLogger(__name__)
from azure.core.credentials import TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
@@ -33,22 +40,25 @@ else:
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore # pragma: no cover
from typing_extensions import override # type: ignore[import] # pragma: no cover
if sys.version_info >= (3, 11):
from typing import TypedDict # type: ignore # pragma: no cover
else:
from typing_extensions import TypedDict # type: ignore # pragma: no cover
if TYPE_CHECKING:
from agent_framework import Agent, BaseContextProvider
from agent_framework._middleware import (
ChatMiddleware,
ChatMiddlewareCallable,
FunctionMiddleware,
FunctionMiddlewareCallable,
from agent_framework import (
Agent,
BaseContextProvider,
ChatAndFunctionMiddlewareTypes,
MiddlewareTypes,
ToolTypes,
)
from agent_framework._tools import ToolTypes
logger: logging.Logger = logging.getLogger("agent_framework.foundry")
AzureTokenProvider = Callable[[], str | Awaitable[str]]
AzureCredentialTypes = TokenCredential | AsyncTokenCredential
class FoundryAgentSettings(TypedDict, total=False):
@@ -203,8 +213,6 @@ class RawFoundryAgentChatClient( # type: ignore[misc]
**kwargs: Any,
) -> Agent[FoundryAgentOptionsT]:
"""Create a FoundryAgent that reuses this client's Foundry configuration."""
from ._foundry_agent import FoundryAgent
function_tools = cast(
FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None,
tools,
@@ -359,9 +367,7 @@ class _FoundryAgentChatClient( # type: ignore[misc]
allow_preview: bool | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
middleware: (
Sequence[ChatMiddleware | ChatMiddlewareCallable | FunctionMiddleware | FunctionMiddlewareCallable] | None
) = None,
middleware: (Sequence[ChatAndFunctionMiddlewareTypes] | None) = None,
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
**kwargs: Any,
) -> None:
@@ -393,3 +399,236 @@ class _FoundryAgentChatClient( # type: ignore[misc]
function_invocation_configuration=function_invocation_configuration,
**kwargs,
)
class RawFoundryAgent( # type: ignore[misc]
RawAgent[FoundryAgentOptionsT],
):
"""Raw Microsoft Foundry Agent without agent-level middleware or telemetry.
Connects to an existing PromptAgent or HostedAgent in Foundry.
For full middleware and telemetry support, use :class:`FoundryAgent`.
Examples:
.. code-block:: python
from agent_framework.foundry import RawFoundryAgent
from azure.identity import AzureCliCredential
agent = RawFoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-prompt-agent",
agent_version="1.0",
credential=AzureCliCredential(),
)
result = await agent.run("Hello!")
"""
def __init__(
self,
*,
project_endpoint: str | None = None,
agent_name: str | None = None,
agent_version: str | None = None,
credential: AzureCredentialTypes | None = None,
project_client: AIProjectClient | None = None,
allow_preview: bool | None = None,
tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
client_type: type[RawFoundryAgentChatClient] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize a Foundry Agent.
Keyword Args:
project_endpoint: The Foundry project endpoint URL.
Can also be set via environment variable FOUNDRY_PROJECT_ENDPOINT.
agent_name: The name of the Foundry agent to connect to.
Can also be set via environment variable FOUNDRY_AGENT_NAME.
agent_version: The version of the agent (required for PromptAgents, optional for HostedAgents).
Can also be set via environment variable FOUNDRY_AGENT_VERSION.
credential: Azure credential for authentication.
project_client: An existing AIProjectClient to use.
allow_preview: Enables preview opt-in on internally-created AIProjectClient.
tools: Function tools to provide to the agent. Only ``FunctionTool`` objects are accepted.
context_providers: Optional context providers for injecting dynamic context.
client_type: Custom client class to use (must be a subclass of ``RawFoundryAgentChatClient``).
Defaults to ``_FoundryAgentChatClient`` (full client middleware).
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
kwargs: Additional keyword arguments passed to the Agent base class.
"""
# Create the client
actual_client_type = client_type or _FoundryAgentChatClient
if not issubclass(actual_client_type, RawFoundryAgentChatClient):
raise TypeError(
f"client_type must be a subclass of RawFoundryAgentChatClient, got {actual_client_type.__name__}"
)
client = actual_client_type(
project_endpoint=project_endpoint,
agent_name=agent_name,
agent_version=agent_version,
credential=credential,
project_client=project_client,
allow_preview=allow_preview,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
super().__init__(
client=client, # type: ignore[arg-type]
tools=tools, # type: ignore[arg-type]
context_providers=context_providers,
**kwargs,
)
async def configure_azure_monitor(
self,
enable_sensitive_data: bool = False,
**kwargs: Any,
) -> None:
"""Setup observability with Azure Monitor (Microsoft Foundry integration).
This method configures Azure Monitor for telemetry collection using the
connection string from the Foundry project client (accessed via the internal 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().
Raises:
ImportError: If azure-monitor-opentelemetry-exporter is not installed.
"""
from azure.core.exceptions import ResourceNotFoundError
client = self.client
if not isinstance(client, RawFoundryAgentChatClient):
raise TypeError("configure_azure_monitor requires a RawFoundryAgentChatClient-based client.")
try:
conn_string = await client.project_client.telemetry.get_application_insights_connection_string()
except ResourceNotFoundError:
logger.warning(
"No Application Insights connection string found for the Foundry project. "
"Please ensure Application Insights is configured in your project, "
"or call configure_otel_providers() manually with custom exporters."
)
return
try:
from azure.monitor.opentelemetry import configure_azure_monitor # type: ignore[import]
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
if "resource" not in kwargs:
kwargs["resource"] = create_resource()
configure_azure_monitor(
connection_string=conn_string,
views=create_metric_views(),
**kwargs,
)
enable_instrumentation(enable_sensitive_data=enable_sensitive_data)
class FoundryAgent( # type: ignore[misc]
AgentMiddlewareLayer,
AgentTelemetryLayer,
RawFoundryAgent[FoundryAgentOptionsT],
):
"""Microsoft Foundry Agent with full middleware and telemetry support.
Connects to an existing PromptAgent or HostedAgent in Foundry.
This is the recommended class for production use.
Examples:
.. code-block:: python
from agent_framework.foundry import FoundryAgent
from azure.identity import AzureCliCredential
# Connect to a PromptAgent
agent = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-prompt-agent",
agent_version="1.0",
credential=AzureCliCredential(),
tools=[my_function_tool],
)
result = await agent.run("Hello!")
# Connect to a HostedAgent (no version needed)
agent = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-hosted-agent",
credential=AzureCliCredential(),
)
# Custom client (e.g., raw client without client middleware)
agent = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-agent",
credential=AzureCliCredential(),
client_type=RawFoundryAgentChatClient,
)
"""
def __init__(
self,
*,
project_endpoint: str | None = None,
agent_name: str | None = None,
agent_version: str | None = None,
credential: AzureCredentialTypes | None = None,
project_client: AIProjectClient | None = None,
allow_preview: bool | None = None,
tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
client_type: type[RawFoundryAgentChatClient] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize a Foundry Agent with full middleware and telemetry.
Keyword Args:
project_endpoint: The Foundry project endpoint URL.
agent_name: The name of the Foundry agent to connect to.
agent_version: The version of the agent (for PromptAgents).
credential: Azure credential for authentication.
project_client: An existing AIProjectClient to use.
allow_preview: Enables preview opt-in on internally-created AIProjectClient.
tools: Function tools to provide to the agent. Only ``FunctionTool`` objects are accepted.
context_providers: Optional context providers.
middleware: Optional agent-level middleware.
client_type: Custom client class (must subclass ``RawFoundryAgentChatClient``).
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
kwargs: Additional keyword arguments.
"""
super().__init__(
project_endpoint=project_endpoint,
agent_name=agent_name,
agent_version=agent_version,
credential=credential,
project_client=project_client,
allow_preview=allow_preview,
tools=tools,
context_providers=context_providers,
middleware=middleware,
client_type=client_type,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
**kwargs,
)
@@ -4,14 +4,17 @@ from __future__ import annotations
import logging
import sys
from collections.abc import Sequence
from collections.abc import Awaitable, Callable, Sequence
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal
from agent_framework._middleware import ChatMiddlewareLayer
from agent_framework._settings import load_settings
from agent_framework._telemetry import AGENT_FRAMEWORK_USER_AGENT
from agent_framework._tools import FunctionInvocationConfiguration, FunctionInvocationLayer
from agent_framework._types import Content
from agent_framework import (
AGENT_FRAMEWORK_USER_AGENT,
ChatMiddlewareLayer,
Content,
FunctionInvocationConfiguration,
FunctionInvocationLayer,
load_settings,
)
from agent_framework.observability import ChatTelemetryLayer
from agent_framework_openai._chat_client import OpenAIChatOptions, RawOpenAIChatClient
from azure.ai.projects.aio import AIProjectClient
@@ -25,9 +28,8 @@ from azure.ai.projects.models import (
)
from azure.ai.projects.models import FileSearchTool as ProjectsFileSearchTool
from azure.ai.projects.models import MCPTool as FoundryMCPTool
from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider
from ._shared import resolve_file_ids
from azure.core.credentials import TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
@@ -43,15 +45,13 @@ else:
from typing_extensions import TypedDict # type: ignore # pragma: no cover
if TYPE_CHECKING:
from agent_framework._middleware import (
ChatMiddleware,
ChatMiddlewareCallable,
FunctionMiddleware,
FunctionMiddlewareCallable,
)
from agent_framework import ChatAndFunctionMiddlewareTypes
logger: logging.Logger = logging.getLogger("agent_framework.foundry")
AzureTokenProvider = Callable[[], str | Awaitable[str]]
AzureCredentialTypes = TokenCredential | AsyncTokenCredential
class FoundrySettings(TypedDict, total=False):
"""Settings for Microsoft FoundryChatClient resolved from args and environment.
@@ -67,6 +67,33 @@ class FoundrySettings(TypedDict, total=False):
project_endpoint: str | None
def resolve_file_ids(file_ids: Sequence[str | Content] | None) -> list[str] | None:
"""Resolve file IDs from strings or hosted-file Content objects."""
if not file_ids:
return None
resolved: list[str] = []
for item in file_ids:
if isinstance(item, str):
if not item:
raise ValueError("file_ids must not contain empty strings.")
resolved.append(item)
elif isinstance(item, Content):
if item.type != "hosted_file":
raise ValueError(
f"Unsupported Content type {item.type!r} for code interpreter file_ids. "
"Only Content.from_hosted_file() is supported."
)
if item.file_id is None:
raise ValueError(
"Content.from_hosted_file() item is missing a file_id. "
"Ensure the Content object has a valid file_id before using it in file_ids."
)
resolved.append(item.file_id)
return resolved if resolved else None
FoundryChatOptionsT = TypeVar(
"FoundryChatOptionsT",
bound=TypedDict, # type: ignore[valid-type]
@@ -492,9 +519,7 @@ class FoundryChatClient( # type: ignore[misc]
env_file_path: str | None = None,
env_file_encoding: str | None = None,
instruction_role: str | None = None,
middleware: (
Sequence[ChatMiddleware | ChatMiddlewareCallable | FunctionMiddleware | FunctionMiddlewareCallable] | None
) = None,
middleware: (Sequence[ChatAndFunctionMiddlewareTypes] | None) = None,
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
**kwargs: Any,
) -> None:
@@ -1,67 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import logging
from collections.abc import Awaitable, Callable
from typing import Union
from agent_framework.exceptions import ChatClientInvalidAuthException
from azure.core.credentials import TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
logger: logging.Logger = logging.getLogger(__name__)
AzureTokenProvider = Callable[[], Union[str, Awaitable[str]]]
"""A callable that returns a bearer token string, either synchronously or asynchronously."""
AzureCredentialTypes = Union[TokenCredential, AsyncTokenCredential]
"""Union of Azure credential types.
Accepts:
- ``TokenCredential`` — synchronous Azure credential (e.g. ``DefaultAzureCredential()``)
- ``AsyncTokenCredential`` — asynchronous Azure credential (e.g. ``azure.identity.aio.DefaultAzureCredential()``)
"""
def resolve_credential_to_token_provider(
credential: AzureCredentialTypes | AzureTokenProvider,
token_endpoint: str | None,
) -> AzureTokenProvider:
"""Convert an Azure credential or token provider into an ``ad_token_provider`` callable.
If the credential is already a callable token provider, it is returned as-is
(``token_endpoint`` is not required in this case).
If it is a ``TokenCredential`` or ``AsyncTokenCredential``, it is wrapped using
``azure.identity.get_bearer_token_provider`` (sync or async variant) which
handles token caching and automatic refresh.
Args:
credential: An Azure credential or token provider callable.
token_endpoint: The token scope/endpoint
(e.g. ``"https://cognitiveservices.azure.com/.default"``).
Required when ``credential`` is a ``TokenCredential`` or ``AsyncTokenCredential``.
Returns:
A callable that returns a bearer token string (sync or async).
Raises:
ServiceInvalidAuthError: If the token endpoint is empty when needed for credential wrapping.
"""
# Already a token provider callable (not a credential object) — use directly
if callable(credential) and not isinstance(credential, (TokenCredential, AsyncTokenCredential)):
return credential
if not token_endpoint:
raise ChatClientInvalidAuthException(
"A token endpoint must be provided either in settings, as an environment variable, or as an argument."
)
if isinstance(credential, AsyncTokenCredential):
from azure.identity.aio import get_bearer_token_provider as get_async_bearer_token_provider
return get_async_bearer_token_provider(credential, token_endpoint)
from azure.identity import get_bearer_token_provider
return get_bearer_token_provider(credential, token_endpoint) # type: ignore[arg-type]
@@ -1,287 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
"""Microsoft Foundry Agent for connecting to pre-configured agents in Foundry.
This module provides ``RawFoundryAgent`` and ``FoundryAgent`` — Agent subclasses
that connect to existing PromptAgents or HostedAgents in Foundry. Use
``FoundryAgent`` for the recommended experience with full middleware and telemetry.
"""
from __future__ import annotations
import logging
import sys
from collections.abc import Callable, Sequence
from typing import TYPE_CHECKING, Any
from agent_framework import (
AgentMiddlewareLayer,
BaseContextProvider,
RawAgent,
)
from agent_framework.observability import AgentTelemetryLayer
from azure.ai.projects.aio import AIProjectClient
from ._entra_id_authentication import AzureCredentialTypes
from ._foundry_agent_client import (
RawFoundryAgentChatClient,
_FoundryAgentChatClient, # pyright: ignore[reportPrivateUsage]
)
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
else:
from typing_extensions import TypeVar # type: ignore # pragma: no cover
if sys.version_info >= (3, 11):
from typing import TypedDict # type: ignore # pragma: no cover
else:
from typing_extensions import TypedDict # type: ignore # pragma: no cover
if TYPE_CHECKING:
from agent_framework._middleware import MiddlewareTypes
from agent_framework._tools import FunctionTool
from agent_framework_openai._chat_client import OpenAIChatOptions
logger: logging.Logger = logging.getLogger("agent_framework.foundry")
FoundryAgentOptionsT = TypeVar(
"FoundryAgentOptionsT",
bound=TypedDict, # type: ignore[valid-type]
default="OpenAIChatOptions",
covariant=True,
)
class RawFoundryAgent( # type: ignore[misc]
RawAgent[FoundryAgentOptionsT],
):
"""Raw Microsoft Foundry Agent without agent-level middleware or telemetry.
Connects to an existing PromptAgent or HostedAgent in Foundry.
For full middleware and telemetry support, use :class:`FoundryAgent`.
Examples:
.. code-block:: python
from agent_framework.foundry import RawFoundryAgent
from azure.identity import AzureCliCredential
agent = RawFoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-prompt-agent",
agent_version="1.0",
credential=AzureCliCredential(),
)
result = await agent.run("Hello!")
"""
def __init__(
self,
*,
project_endpoint: str | None = None,
agent_name: str | None = None,
agent_version: str | None = None,
credential: AzureCredentialTypes | None = None,
project_client: AIProjectClient | None = None,
allow_preview: bool | None = None,
tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
client_type: type[RawFoundryAgentChatClient] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize a Foundry Agent.
Keyword Args:
project_endpoint: The Foundry project endpoint URL.
Can also be set via environment variable FOUNDRY_PROJECT_ENDPOINT.
agent_name: The name of the Foundry agent to connect to.
Can also be set via environment variable FOUNDRY_AGENT_NAME.
agent_version: The version of the agent (required for PromptAgents, optional for HostedAgents).
Can also be set via environment variable FOUNDRY_AGENT_VERSION.
credential: Azure credential for authentication.
project_client: An existing AIProjectClient to use.
allow_preview: Enables preview opt-in on internally-created AIProjectClient.
tools: Function tools to provide to the agent. Only ``FunctionTool`` objects are accepted.
context_providers: Optional context providers for injecting dynamic context.
client_type: Custom client class to use (must be a subclass of ``RawFoundryAgentChatClient``).
Defaults to ``_FoundryAgentChatClient`` (full client middleware).
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
kwargs: Additional keyword arguments passed to the Agent base class.
"""
# Create the client
actual_client_type = client_type or _FoundryAgentChatClient
if not issubclass(actual_client_type, RawFoundryAgentChatClient):
raise TypeError(
f"client_type must be a subclass of RawFoundryAgentChatClient, got {actual_client_type.__name__}"
)
client = actual_client_type(
project_endpoint=project_endpoint,
agent_name=agent_name,
agent_version=agent_version,
credential=credential,
project_client=project_client,
allow_preview=allow_preview,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
super().__init__(
client=client, # type: ignore[arg-type]
tools=tools, # type: ignore[arg-type]
context_providers=context_providers,
**kwargs,
)
async def configure_azure_monitor(
self,
enable_sensitive_data: bool = False,
**kwargs: Any,
) -> None:
"""Setup observability with Azure Monitor (Microsoft Foundry integration).
This method configures Azure Monitor for telemetry collection using the
connection string from the Foundry project client (accessed via the internal 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().
Raises:
ImportError: If azure-monitor-opentelemetry-exporter is not installed.
"""
from azure.core.exceptions import ResourceNotFoundError
from ._foundry_agent_client import RawFoundryAgentChatClient
client = self.client
if not isinstance(client, RawFoundryAgentChatClient):
raise TypeError("configure_azure_monitor requires a RawFoundryAgentChatClient-based client.")
try:
conn_string = await client.project_client.telemetry.get_application_insights_connection_string()
except ResourceNotFoundError:
logger.warning(
"No Application Insights connection string found for the Foundry project. "
"Please ensure Application Insights is configured in your project, "
"or call configure_otel_providers() manually with custom exporters."
)
return
try:
from azure.monitor.opentelemetry import configure_azure_monitor # type: ignore[import]
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
if "resource" not in kwargs:
kwargs["resource"] = create_resource()
configure_azure_monitor(
connection_string=conn_string,
views=create_metric_views(),
**kwargs,
)
enable_instrumentation(enable_sensitive_data=enable_sensitive_data)
class FoundryAgent( # type: ignore[misc]
AgentMiddlewareLayer,
AgentTelemetryLayer,
RawFoundryAgent[FoundryAgentOptionsT],
):
"""Microsoft Foundry Agent with full middleware and telemetry support.
Connects to an existing PromptAgent or HostedAgent in Foundry.
This is the recommended class for production use.
Examples:
.. code-block:: python
from agent_framework.foundry import FoundryAgent
from azure.identity import AzureCliCredential
# Connect to a PromptAgent
agent = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-prompt-agent",
agent_version="1.0",
credential=AzureCliCredential(),
tools=[my_function_tool],
)
result = await agent.run("Hello!")
# Connect to a HostedAgent (no version needed)
agent = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-hosted-agent",
credential=AzureCliCredential(),
)
# Custom client (e.g., raw client without client middleware)
agent = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-agent",
credential=AzureCliCredential(),
client_type=RawFoundryAgentChatClient,
)
"""
def __init__(
self,
*,
project_endpoint: str | None = None,
agent_name: str | None = None,
agent_version: str | None = None,
credential: AzureCredentialTypes | None = None,
project_client: AIProjectClient | None = None,
allow_preview: bool | None = None,
tools: FunctionTool | Callable[..., Any] | Sequence[FunctionTool | Callable[..., Any]] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
client_type: type[RawFoundryAgentChatClient] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize a Foundry Agent with full middleware and telemetry.
Keyword Args:
project_endpoint: The Foundry project endpoint URL.
agent_name: The name of the Foundry agent to connect to.
agent_version: The version of the agent (for PromptAgents).
credential: Azure credential for authentication.
project_client: An existing AIProjectClient to use.
allow_preview: Enables preview opt-in on internally-created AIProjectClient.
tools: Function tools to provide to the agent. Only ``FunctionTool`` objects are accepted.
context_providers: Optional context providers.
middleware: Optional agent-level middleware.
client_type: Custom client class (must subclass ``RawFoundryAgentChatClient``).
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
kwargs: Additional keyword arguments.
"""
super().__init__(
project_endpoint=project_endpoint,
agent_name=agent_name,
agent_version=agent_version,
credential=credential,
project_client=project_client,
allow_preview=allow_preview,
tools=tools,
context_providers=context_providers,
middleware=middleware,
client_type=client_type,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
**kwargs,
)
@@ -13,25 +13,38 @@ import sys
from contextlib import AbstractAsyncContextManager
from typing import TYPE_CHECKING, Any, ClassVar
from agent_framework import AGENT_FRAMEWORK_USER_AGENT, Message
from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext
from agent_framework._settings import load_settings
from agent_framework import (
AGENT_FRAMEWORK_USER_AGENT,
AgentSession,
BaseContextProvider,
Message,
SessionContext,
load_settings,
)
from azure.ai.projects.aio import AIProjectClient
from azure.core.credentials import TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
from openai.types.responses import ResponseInputItemParam
from ._entra_id_authentication import AzureCredentialTypes
from ._shared import FoundryProjectSettings
if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
from typing import Self, TypedDict # pragma: no cover
else:
from typing_extensions import Self # pragma: no cover
from typing_extensions import Self, TypedDict # pragma: no cover
if TYPE_CHECKING:
from agent_framework._agents import SupportsAgentRun
from agent_framework import SupportsAgentRun
logger = logging.getLogger(__name__)
AzureCredentialTypes = TokenCredential | AsyncTokenCredential
class FoundryProjectSettings(TypedDict, total=False):
"""Foundry project settings loaded from FOUNDRY_ environment variables."""
project_endpoint: str | None
class FoundryMemoryProvider(BaseContextProvider):
"""Foundry Memory context provider using the new BaseContextProvider hooks pattern.
@@ -1,49 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import logging
import sys
from collections.abc import Sequence
from agent_framework import Content
if sys.version_info >= (3, 11):
from typing import TypedDict # pragma: no cover
else:
from typing_extensions import TypedDict # type: ignore # pragma: no cover
logger = logging.getLogger("agent_framework.foundry")
class FoundryProjectSettings(TypedDict, total=False):
"""Foundry project settings loaded from FOUNDRY_ environment variables."""
project_endpoint: str | None
def resolve_file_ids(file_ids: Sequence[str | Content] | None) -> list[str] | None:
"""Resolve file IDs from strings or hosted-file Content objects."""
if not file_ids:
return None
resolved: list[str] = []
for item in file_ids:
if isinstance(item, str):
if not item:
raise ValueError("file_ids must not contain empty strings.")
resolved.append(item)
elif isinstance(item, Content):
if item.type != "hosted_file":
raise ValueError(
f"Unsupported Content type {item.type!r} for code interpreter file_ids. "
"Only Content.from_hosted_file() is supported."
)
if item.file_id is None:
raise ValueError(
"Content.from_hosted_file() item is missing a file_id. "
"Ensure the Content object has a valid file_id before using it in file_ids."
)
resolved.append(item.file_id)
return resolved if resolved else None