mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[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:
committed by
GitHub
Unverified
parent
3611be82cf
commit
cc0cfaaac8
@@ -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__)
|
||||
|
||||
+266
-27
@@ -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,
|
||||
)
|
||||
+43
-18
@@ -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,
|
||||
)
|
||||
+22
-9
@@ -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
|
||||
Reference in New Issue
Block a user