mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] Python: Provider-leading client design & OpenAI package extraction (#4818)
* Python: Provider-leading client design & OpenAI package extraction Major refactoring of the Python Agent Framework client architecture: - Extract OpenAI clients into new `agent-framework-openai` package - Core package no longer depends on openai, azure-identity, azure-ai-projects - Rename clients for discoverability: OpenAIResponsesClient → OpenAIChatClient, OpenAIChatClient → OpenAIChatCompletionClient - Unify `model_id`/`deployment_name`/`model_deployment_name` → `model` param - New FoundryChatClient for Azure AI Foundry Responses API - New FoundryAgent/FoundryAgentClient for connecting to pre-configured Foundry agents - Remove OpenAIBase/OpenAIConfigMixin from non-deprecated client MRO - Deprecate AzureOpenAI* clients, AzureAIClient, OpenAIAssistantsClient - Reorganize samples: azure_openai+azure_ai+azure_ai_agent → azure/ - ADR-0020: Provider-Leading Client Design Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: missing Agent imports in samples, .model_id → .model in foundry_local sample Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: CI failures — mypy errors, coverage targets, sample imports - azure-ai mypy: add type ignores for TypedDict total=, model arg, forward ref - Coverage: replace core.azure/openai targets with openai package target - project_provider: add type annotation for opts dict Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: populate openai .pyi stub, fix broken README links, coverage targets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fixes * updated observabilitty * reset azure init.pyi * fix errors * updated adr number * fix foundry local * fixed not renamed docstrings and comments, and added deprecated markers to old classes * fix tests and pyprojects * fix test vars * updated function tests * update durable * updated test setup for functions * Fix Foundry auth in workflow samples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Stabilize Python integration workflows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update hosting samples for Foundry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Trigger full CI rerun Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Trigger CI rerun again Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * trigger rerun * trigger rerun * fix for litellm * undo durabletask changes * Move Foundry APIs into foundry namespace Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Foundry pyproject formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Split provider samples by Foundry surface Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore hosting sample requirements Also fix the Foundry Local sample link after the provider sample move. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updated tests * udpated foundry integration tests * removed dist from azurefunctions tests * Use separate Foundry clients for concurrent agents Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix client setup in azfunc and durable * disabled two tests * updated setup for some function and durable tests * improved azure openai setup with new clients * ignore deprecated * fixes * skip 11 * remove openai assistants int tests --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
4b533608b6
commit
5e056b672e
@@ -2,23 +2,35 @@
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._agent_provider import AzureAIAgentsProvider
|
||||
from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions
|
||||
from ._client import AzureAIClient, AzureAIProjectAgentOptions, RawAzureAIClient
|
||||
from ._agent_provider import AzureAIAgentsProvider # pyright: ignore[reportDeprecated]
|
||||
from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions # pyright: ignore[reportDeprecated]
|
||||
from ._client import AzureAIClient, AzureAIProjectAgentOptions, RawAzureAIClient # pyright: ignore[reportDeprecated]
|
||||
from ._deprecated_azure_openai import (
|
||||
AzureOpenAIAssistantsClient, # pyright: ignore[reportDeprecated]
|
||||
AzureOpenAIAssistantsOptions,
|
||||
AzureOpenAIChatClient, # pyright: ignore[reportDeprecated]
|
||||
AzureOpenAIChatOptions,
|
||||
AzureOpenAIConfigMixin,
|
||||
AzureOpenAIEmbeddingClient, # pyright: ignore[reportDeprecated]
|
||||
AzureOpenAIResponsesClient, # pyright: ignore[reportDeprecated]
|
||||
AzureOpenAIResponsesOptions,
|
||||
AzureOpenAISettings,
|
||||
AzureUserSecurityContext,
|
||||
)
|
||||
from ._embedding_client import (
|
||||
AzureAIInferenceEmbeddingClient,
|
||||
AzureAIInferenceEmbeddingOptions,
|
||||
AzureAIInferenceEmbeddingSettings,
|
||||
RawAzureAIInferenceEmbeddingClient,
|
||||
)
|
||||
from ._foundry_memory_provider import FoundryMemoryProvider
|
||||
from ._project_provider import AzureAIProjectAgentProvider
|
||||
from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider
|
||||
from ._project_provider import AzureAIProjectAgentProvider # pyright: ignore[reportDeprecated]
|
||||
from ._shared import AzureAISettings
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
__version__ = "0.0.0"
|
||||
|
||||
__all__ = [
|
||||
"AzureAIAgentClient",
|
||||
@@ -31,7 +43,18 @@ __all__ = [
|
||||
"AzureAIProjectAgentOptions",
|
||||
"AzureAIProjectAgentProvider",
|
||||
"AzureAISettings",
|
||||
"FoundryMemoryProvider",
|
||||
"AzureCredentialTypes",
|
||||
"AzureOpenAIAssistantsClient",
|
||||
"AzureOpenAIAssistantsOptions",
|
||||
"AzureOpenAIChatClient",
|
||||
"AzureOpenAIChatOptions",
|
||||
"AzureOpenAIConfigMixin",
|
||||
"AzureOpenAIEmbeddingClient",
|
||||
"AzureOpenAIResponsesClient",
|
||||
"AzureOpenAIResponsesOptions",
|
||||
"AzureOpenAISettings",
|
||||
"AzureTokenProvider",
|
||||
"AzureUserSecurityContext",
|
||||
"RawAzureAIClient",
|
||||
"RawAzureAIInferenceEmbeddingClient",
|
||||
"__version__",
|
||||
|
||||
@@ -18,19 +18,23 @@ from agent_framework import (
|
||||
from agent_framework._mcp import MCPTool
|
||||
from agent_framework._settings import load_settings
|
||||
from agent_framework._tools import ToolTypes
|
||||
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
|
||||
from azure.ai.agents.aio import AgentsClient
|
||||
from azure.ai.agents.models import Agent as AzureAgent
|
||||
from azure.ai.agents.models import ResponseFormatJsonSchema, ResponseFormatJsonSchemaType
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions
|
||||
from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions # pyright: ignore[reportDeprecated]
|
||||
from ._entra_id_authentication import AzureCredentialTypes
|
||||
from ._shared import AzureAISettings, to_azure_ai_agent_tools
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import Self, TypeVar # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import Self, TypeVar # type: ignore # pragma: no cover
|
||||
if sys.version_info >= (3, 13):
|
||||
from warnings import deprecated # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import deprecated # type: ignore # pragma: no cover
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import TypedDict # type: ignore # pragma: no cover
|
||||
else:
|
||||
@@ -47,6 +51,11 @@ OptionsCoT = TypeVar(
|
||||
)
|
||||
|
||||
|
||||
@deprecated(
|
||||
"AzureAIAgentClient and the AzureAIAgentsProvider are deprecated. "
|
||||
"They target the V1 Agents Service API and have no direct replacement; "
|
||||
"for new Foundry projects, use FoundryAgent."
|
||||
)
|
||||
class AzureAIAgentsProvider(Generic[OptionsCoT]):
|
||||
"""Provider for Azure AI Agent Service V1 (Persistent Agents API).
|
||||
|
||||
@@ -426,7 +435,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
|
||||
context_providers: Context providers to include during agent invocation.
|
||||
"""
|
||||
# Create the underlying client
|
||||
client = AzureAIAgentClient(
|
||||
client = AzureAIAgentClient( # pyright: ignore[reportDeprecated]
|
||||
agents_client=self._agents_client,
|
||||
agent_id=agent.id,
|
||||
agent_name=agent.name,
|
||||
|
||||
@@ -36,7 +36,6 @@ from agent_framework import (
|
||||
)
|
||||
from agent_framework._settings import load_settings
|
||||
from agent_framework._tools import ToolTypes
|
||||
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
|
||||
from agent_framework.exceptions import (
|
||||
ChatClientException,
|
||||
ChatClientInvalidRequestException,
|
||||
@@ -92,12 +91,14 @@ from azure.ai.agents.models import (
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ._entra_id_authentication import AzureCredentialTypes
|
||||
from ._shared import AzureAISettings, resolve_file_ids, to_azure_ai_agent_tools
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar # type: ignore # pragma: no cover
|
||||
from warnings import deprecated # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypeVar # type: ignore # pragma: no cover
|
||||
from typing_extensions import TypeVar, deprecated # type: ignore # pragma: no cover
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore # pragma: no cover
|
||||
else:
|
||||
@@ -210,6 +211,11 @@ AzureAIAgentOptionsT = TypeVar(
|
||||
# endregion
|
||||
|
||||
|
||||
@deprecated(
|
||||
"AzureAIAgentClient is deprecated. "
|
||||
"It targets the V1 Agents Service API and has no direct replacement; "
|
||||
"for new Foundry projects, use FoundryAgent."
|
||||
)
|
||||
class AzureAIAgentClient(
|
||||
FunctionInvocationLayer[AzureAIAgentOptionsT],
|
||||
ChatMiddlewareLayer[AzureAIAgentOptionsT],
|
||||
@@ -221,7 +227,8 @@ class AzureAIAgentClient(
|
||||
|
||||
.. deprecated::
|
||||
AzureAIAgentClient is deprecated and will be removed in a future release.
|
||||
Use :class:`AzureAIClient` instead for the V2 (Projects/Responses) API.
|
||||
It targets the V1 Agents Service API and has no direct replacement.
|
||||
For new Foundry projects, use :class:`FoundryAgent`.
|
||||
"""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai" # type: ignore[reportIncompatibleVariableOverride, misc]
|
||||
@@ -239,7 +246,8 @@ class AzureAIAgentClient(
|
||||
|
||||
.. deprecated::
|
||||
This method is deprecated and will be removed in a future release.
|
||||
Use :meth:`AzureAIClient.get_code_interpreter_tool` instead.
|
||||
For new Foundry projects, configure hosted tools on the Foundry agent definition
|
||||
in the service instead.
|
||||
|
||||
Keyword Args:
|
||||
file_ids: List of uploaded file IDs or Content objects to make available to
|
||||
@@ -272,7 +280,7 @@ class AzureAIAgentClient(
|
||||
"""
|
||||
warnings.warn(
|
||||
"AzureAIAgentClient.get_code_interpreter_tool() is deprecated and will be removed in a future release; "
|
||||
"use AzureAIClient.get_code_interpreter_tool() instead.",
|
||||
"for new Foundry projects, configure hosted tools on the Foundry agent definition in the service instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
@@ -288,7 +296,8 @@ class AzureAIAgentClient(
|
||||
|
||||
.. deprecated::
|
||||
This method is deprecated and will be removed in a future release.
|
||||
Use :meth:`AzureAIClient.get_file_search_tool` instead.
|
||||
For new Foundry projects, configure hosted tools on the Foundry agent definition
|
||||
in the service instead.
|
||||
|
||||
Keyword Args:
|
||||
vector_store_ids: List of vector store IDs to search within.
|
||||
@@ -308,7 +317,7 @@ class AzureAIAgentClient(
|
||||
"""
|
||||
warnings.warn(
|
||||
"AzureAIAgentClient.get_file_search_tool() is deprecated and will be removed in a future release; "
|
||||
"use AzureAIClient.get_file_search_tool() instead.",
|
||||
"for new Foundry projects, configure hosted tools on the Foundry agent definition in the service instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
@@ -325,7 +334,8 @@ class AzureAIAgentClient(
|
||||
|
||||
.. deprecated::
|
||||
This method is deprecated and will be removed in a future release.
|
||||
Use :meth:`AzureAIClient.get_web_search_tool` instead.
|
||||
For new Foundry projects, configure hosted tools on the Foundry agent definition
|
||||
in the service instead.
|
||||
|
||||
For Azure AI Agents, web search uses Bing Grounding or Bing Custom Search.
|
||||
If no arguments are provided, attempts to read from environment variables.
|
||||
@@ -369,7 +379,7 @@ class AzureAIAgentClient(
|
||||
"""
|
||||
warnings.warn(
|
||||
"AzureAIAgentClient.get_web_search_tool() is deprecated and will be removed in a future release; "
|
||||
"use AzureAIClient.get_web_search_tool() instead.",
|
||||
"for new Foundry projects, configure hosted tools on the Foundry agent definition in the service instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
@@ -410,7 +420,8 @@ class AzureAIAgentClient(
|
||||
|
||||
.. deprecated::
|
||||
This method is deprecated and will be removed in a future release.
|
||||
Use :meth:`AzureAIClient.get_mcp_tool` instead.
|
||||
For new Foundry projects, configure hosted tools on the Foundry agent definition
|
||||
in the service instead.
|
||||
|
||||
This configures an MCP (Model Context Protocol) server that will be called
|
||||
by Azure AI's service. The tools from this MCP server are executed remotely
|
||||
@@ -446,7 +457,7 @@ class AzureAIAgentClient(
|
||||
"""
|
||||
warnings.warn(
|
||||
"AzureAIAgentClient.get_mcp_tool() is deprecated and will be removed in a future release; "
|
||||
"use AzureAIClient.get_mcp_tool() instead.",
|
||||
"for new Foundry projects, configure hosted tools on the Foundry agent definition in the service instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
@@ -561,12 +572,6 @@ class AzureAIAgentClient(
|
||||
client: AzureAIAgentClient[MyOptions] = AzureAIAgentClient(credential=credential)
|
||||
response = await client.get_response("Hello", options={"my_custom_option": "value"})
|
||||
"""
|
||||
warnings.warn(
|
||||
"AzureAIAgentClient is deprecated and will be removed in a future release; "
|
||||
"use AzureAIClient instead for the V2 (Projects/Responses) API.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
azure_ai_settings = load_settings(
|
||||
AzureAISettings,
|
||||
env_prefix="AZURE_AI_",
|
||||
|
||||
@@ -30,10 +30,9 @@ from agent_framework import (
|
||||
)
|
||||
from agent_framework._settings import load_settings
|
||||
from agent_framework._tools import ToolTypes
|
||||
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
|
||||
from agent_framework.observability import ChatTelemetryLayer
|
||||
from agent_framework.openai import OpenAIResponsesOptions
|
||||
from agent_framework.openai._responses_client import RawOpenAIResponsesClient
|
||||
from agent_framework_openai._chat_client import RawOpenAIChatClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import (
|
||||
ApproximateLocation,
|
||||
@@ -50,12 +49,14 @@ from azure.ai.projects.models import (
|
||||
from azure.ai.projects.models import FileSearchTool as ProjectsFileSearchTool
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
|
||||
from ._entra_id_authentication import AzureCredentialTypes
|
||||
from ._shared import AzureAISettings, create_text_format_config, resolve_file_ids
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar # type: ignore # pragma: no cover
|
||||
from warnings import deprecated # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypeVar # type: ignore # pragma: no cover
|
||||
from typing_extensions import TypeVar, deprecated # type: ignore # pragma: no cover
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore # pragma: no cover
|
||||
else:
|
||||
@@ -68,7 +69,7 @@ else:
|
||||
logger = logging.getLogger("agent_framework.azure")
|
||||
|
||||
|
||||
class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False):
|
||||
class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False): # type: ignore[misc, call-arg]
|
||||
"""Azure AI Project Agent options."""
|
||||
|
||||
rai_config: RaiConfig
|
||||
@@ -88,8 +89,13 @@ AzureAIClientOptionsT = TypeVar(
|
||||
_DOC_INDEX_PATTERN = re.compile(r"doc_(\d+)")
|
||||
|
||||
|
||||
class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[AzureAIClientOptionsT]):
|
||||
"""Raw Azure AI client without middleware, telemetry, or function invocation layers.
|
||||
@deprecated(
|
||||
"RawAzureAIClient is deprecated. "
|
||||
"Use RawFoundryAgentChatClient for low-level Foundry agent client customization, "
|
||||
"or FoundryAgent for the recommended production API."
|
||||
)
|
||||
class RawAzureAIClient(RawOpenAIChatClient[AzureAIClientOptionsT], Generic[AzureAIClientOptionsT]):
|
||||
"""Deprecated raw Azure AI client without middleware, telemetry, or function invocation layers.
|
||||
|
||||
Warning:
|
||||
**This class should not normally be used directly.** It does not include middleware,
|
||||
@@ -101,7 +107,8 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
|
||||
2. **ChatMiddlewareLayer** - Applies chat middleware per model call and stays outside telemetry
|
||||
3. **ChatTelemetryLayer** - Must stay inside chat middleware for correct per-call telemetry
|
||||
|
||||
Use ``AzureAIClient`` instead for a fully-featured client with all layers applied.
|
||||
Use ``RawFoundryAgentChatClient`` for low-level Foundry agent customization, or
|
||||
``FoundryAgent`` for the recommended production API.
|
||||
"""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai" # type: ignore[reportIncompatibleVariableOverride, misc]
|
||||
@@ -215,8 +222,10 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
|
||||
project_client = AIProjectClient(**project_client_kwargs)
|
||||
should_close_client = True
|
||||
|
||||
# Initialize parent
|
||||
super().__init__(
|
||||
# Initialize parent with OpenAI client from project
|
||||
super().__init__( # type: ignore
|
||||
async_client=project_client.get_openai_client(),
|
||||
model=azure_ai_settings.get("model"), # type: ignore[arg-type]
|
||||
additional_properties=additional_properties,
|
||||
)
|
||||
|
||||
@@ -680,10 +689,6 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
|
||||
|
||||
return result, instructions
|
||||
|
||||
async def _initialize_client(self) -> None:
|
||||
"""Initialize OpenAI client."""
|
||||
self.client = self.project_client.get_openai_client() # type: ignore
|
||||
|
||||
def _update_agent_name_and_description(self, agent_name: str | None, description: str | None = None) -> None:
|
||||
"""Update the agent name in the chat client.
|
||||
|
||||
@@ -842,7 +847,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
|
||||
if not stream:
|
||||
|
||||
async def _enrich_response() -> ChatResponse:
|
||||
response = await super(RawAzureAIClient, self)._inner_get_response(
|
||||
response = await super(RawAzureAIClient, self)._inner_get_response( # pyright: ignore[reportDeprecated]
|
||||
messages=messages, options=options, stream=False, **kwargs
|
||||
)
|
||||
get_urls = self._extract_azure_search_urls(response.raw_representation.output) # type: ignore[union-attr]
|
||||
@@ -1182,8 +1187,8 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
|
||||
It does NOT create an agent on the Azure AI service - the actual agent
|
||||
will be created on the server during the first invocation (run).
|
||||
|
||||
For creating and managing persistent agents on the server, use
|
||||
:class:`~agent_framework_azure_ai.AzureAIProjectAgentProvider` instead.
|
||||
For working with pre-configured persistent agents on the server, use
|
||||
:class:`~agent_framework_azure_ai.FoundryAgent` instead.
|
||||
|
||||
Keyword Args:
|
||||
id: The unique identifier for the agent. Will be created automatically if not provided.
|
||||
@@ -1213,21 +1218,23 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
|
||||
)
|
||||
|
||||
|
||||
@deprecated("AzureAIClient is deprecated. Use FoundryAgent instead.")
|
||||
class AzureAIClient(
|
||||
FunctionInvocationLayer[AzureAIClientOptionsT],
|
||||
ChatMiddlewareLayer[AzureAIClientOptionsT],
|
||||
ChatTelemetryLayer[AzureAIClientOptionsT],
|
||||
RawAzureAIClient[AzureAIClientOptionsT],
|
||||
RawAzureAIClient[AzureAIClientOptionsT], # pyright: ignore[reportDeprecated]
|
||||
Generic[AzureAIClientOptionsT],
|
||||
):
|
||||
"""Azure AI client with middleware, telemetry, and function invocation support.
|
||||
"""Deprecated Azure AI client with middleware, telemetry, and function invocation support.
|
||||
|
||||
This is the recommended client for most use cases. It includes:
|
||||
This class is deprecated. Use ``FoundryAgent`` instead for connecting to
|
||||
pre-configured agents in Foundry. It includes:
|
||||
- Chat middleware support for request/response interception
|
||||
- OpenTelemetry-based telemetry for observability
|
||||
- Automatic function/tool invocation handling
|
||||
|
||||
For a minimal implementation without these features, use :class:`RawAzureAIClient`.
|
||||
For a minimal implementation without these features, use :class:`RawFoundryAgentChatClient`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -0,0 +1,897 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Deprecated Azure OpenAI client classes.
|
||||
|
||||
All classes in this module are deprecated and will be removed in a future release.
|
||||
Migrate to the ``agent_framework_openai`` package equivalents with an ``AsyncAzureOpenAI`` client,
|
||||
or use ``FoundryChatClient`` for Azure AI Foundry projects.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from collections.abc import Mapping, Sequence
|
||||
from copy import copy
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, cast
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from agent_framework._middleware import ChatMiddlewareLayer
|
||||
from agent_framework._settings import SecretString, load_settings
|
||||
from agent_framework._telemetry import AGENT_FRAMEWORK_USER_AGENT, APP_INFO, prepend_agent_framework_to_user_agent
|
||||
from agent_framework._tools import FunctionInvocationConfiguration, FunctionInvocationLayer
|
||||
from agent_framework._types import Annotation, Content
|
||||
from agent_framework.observability import ChatTelemetryLayer, EmbeddingTelemetryLayer
|
||||
from agent_framework_openai._assistants_client import OpenAIAssistantsClient, OpenAIAssistantsOptions
|
||||
from agent_framework_openai._chat_client import OpenAIChatOptions, RawOpenAIChatClient
|
||||
from agent_framework_openai._chat_completion_client import OpenAIChatCompletionOptions, RawOpenAIChatCompletionClient
|
||||
from agent_framework_openai._embedding_client import OpenAIEmbeddingOptions, RawOpenAIEmbeddingClient
|
||||
from agent_framework_openai._shared import OpenAIBase
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from openai import AsyncOpenAI
|
||||
from openai.lib.azure import AsyncAzureOpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider, resolve_credential_to_token_provider
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar # type: ignore # pragma: no cover
|
||||
from warnings import deprecated # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypeVar, deprecated # type: ignore # pragma: no cover
|
||||
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
|
||||
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 openai.types.chat.chat_completion import Choice
|
||||
from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# region Constants and Settings
|
||||
|
||||
DEFAULT_AZURE_API_VERSION: Final[str] = "2024-10-21"
|
||||
DEFAULT_AZURE_TOKEN_ENDPOINT: Final[str] = "https://cognitiveservices.azure.com/.default" # noqa: S105
|
||||
|
||||
|
||||
class AzureOpenAISettings(TypedDict, total=False):
|
||||
"""AzureOpenAI model settings.
|
||||
|
||||
Settings are resolved in this order: explicit keyword arguments, values from an
|
||||
explicitly provided .env file, then environment variables with the prefix
|
||||
'AZURE_OPENAI_'. If settings are missing after resolution, validation will fail.
|
||||
|
||||
Keyword Args:
|
||||
endpoint: The endpoint of the Azure deployment.
|
||||
chat_deployment_name: The name of the Azure Chat deployment.
|
||||
responses_deployment_name: The name of the Azure Responses deployment.
|
||||
embedding_deployment_name: The name of the Azure Embedding deployment.
|
||||
api_key: The API key for the Azure deployment.
|
||||
api_version: The API version to use.
|
||||
base_url: The url of the Azure deployment.
|
||||
token_endpoint: The token endpoint to use to retrieve the authentication token.
|
||||
"""
|
||||
|
||||
chat_deployment_name: str | None
|
||||
responses_deployment_name: str | None
|
||||
embedding_deployment_name: str | None
|
||||
endpoint: str | None
|
||||
base_url: str | None
|
||||
api_key: SecretString | None
|
||||
api_version: str | None
|
||||
token_endpoint: str | None
|
||||
|
||||
|
||||
def _apply_azure_defaults(
|
||||
settings: AzureOpenAISettings,
|
||||
default_api_version: str = DEFAULT_AZURE_API_VERSION,
|
||||
default_token_endpoint: str = DEFAULT_AZURE_TOKEN_ENDPOINT,
|
||||
) -> None:
|
||||
"""Apply default values for api_version and token_endpoint after loading settings.
|
||||
|
||||
Args:
|
||||
settings: The loaded Azure OpenAI settings dict.
|
||||
default_api_version: The default API version to use if not set.
|
||||
default_token_endpoint: The default token endpoint to use if not set.
|
||||
"""
|
||||
if not settings.get("api_version"):
|
||||
settings["api_version"] = default_api_version
|
||||
if not settings.get("token_endpoint"):
|
||||
settings["token_endpoint"] = default_token_endpoint
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region AzureOpenAIConfigMixin
|
||||
|
||||
|
||||
class AzureOpenAIConfigMixin(OpenAIBase):
|
||||
"""Internal class for configuring a connection to an Azure OpenAI service."""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.openai"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
deployment_name: str,
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
api_version: str = DEFAULT_AZURE_API_VERSION,
|
||||
api_key: str | None = None,
|
||||
token_endpoint: str | None = None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
client: AsyncOpenAI | None = None,
|
||||
instruction_role: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Configure a connection to an Azure OpenAI service.
|
||||
|
||||
Args:
|
||||
deployment_name: Name of the deployment.
|
||||
endpoint: The specific endpoint URL for the deployment.
|
||||
base_url: The base URL for Azure services.
|
||||
api_version: Azure API version.
|
||||
api_key: API key for Azure services.
|
||||
token_endpoint: Azure AD token scope.
|
||||
credential: Azure credential or token provider for authentication.
|
||||
default_headers: Default headers for HTTP requests.
|
||||
client: An existing client to use.
|
||||
instruction_role: The role to use for 'instruction' messages.
|
||||
kwargs: Additional keyword arguments.
|
||||
"""
|
||||
merged_headers = dict(copy(default_headers)) if default_headers else {}
|
||||
if APP_INFO:
|
||||
merged_headers.update(APP_INFO)
|
||||
merged_headers = prepend_agent_framework_to_user_agent(merged_headers)
|
||||
if not client:
|
||||
ad_token_provider = None
|
||||
if not api_key and credential:
|
||||
ad_token_provider = resolve_credential_to_token_provider(credential, token_endpoint)
|
||||
|
||||
if not api_key and not ad_token_provider:
|
||||
raise ValueError("Please provide either api_key, credential, or a client.")
|
||||
|
||||
if not endpoint and not base_url:
|
||||
raise ValueError("Please provide an endpoint or a base_url")
|
||||
|
||||
args: dict[str, Any] = {
|
||||
"default_headers": merged_headers,
|
||||
}
|
||||
if api_version:
|
||||
args["api_version"] = api_version
|
||||
if ad_token_provider:
|
||||
args["azure_ad_token_provider"] = ad_token_provider
|
||||
if api_key:
|
||||
args["api_key"] = api_key
|
||||
if base_url:
|
||||
args["base_url"] = str(base_url)
|
||||
if endpoint and not base_url:
|
||||
args["azure_endpoint"] = str(endpoint)
|
||||
if deployment_name:
|
||||
args["azure_deployment"] = deployment_name
|
||||
if "websocket_base_url" in kwargs:
|
||||
args["websocket_base_url"] = kwargs.pop("websocket_base_url")
|
||||
|
||||
client = AsyncAzureOpenAI(**args)
|
||||
|
||||
self.endpoint = str(endpoint)
|
||||
self.base_url = str(base_url)
|
||||
self.api_version = api_version
|
||||
self.deployment_name = deployment_name
|
||||
self.instruction_role = instruction_role
|
||||
if default_headers:
|
||||
from agent_framework._telemetry import USER_AGENT_KEY
|
||||
|
||||
def_headers = {k: v for k, v in default_headers.items() if k != USER_AGENT_KEY}
|
||||
else:
|
||||
def_headers = None
|
||||
self.default_headers = def_headers
|
||||
|
||||
super().__init__(model_id=deployment_name, client=client, **kwargs)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region AzureOpenAIResponsesClient
|
||||
|
||||
|
||||
AzureOpenAIResponsesOptionsT = TypeVar(
|
||||
"AzureOpenAIResponsesOptionsT",
|
||||
bound=TypedDict, # type: ignore[valid-type]
|
||||
default="OpenAIChatOptions",
|
||||
covariant=True,
|
||||
)
|
||||
|
||||
AzureOpenAIResponsesOptions = OpenAIChatOptions
|
||||
|
||||
|
||||
@deprecated(
|
||||
"AzureOpenAIResponsesClient is deprecated. "
|
||||
"Use OpenAIChatClient with an AsyncAzureOpenAI client, or FoundryChatClient for Foundry projects."
|
||||
)
|
||||
class AzureOpenAIResponsesClient( # type: ignore[misc]
|
||||
FunctionInvocationLayer[AzureOpenAIResponsesOptionsT],
|
||||
ChatMiddlewareLayer[AzureOpenAIResponsesOptionsT],
|
||||
ChatTelemetryLayer[AzureOpenAIResponsesOptionsT],
|
||||
RawOpenAIChatClient[AzureOpenAIResponsesOptionsT],
|
||||
Generic[AzureOpenAIResponsesOptionsT],
|
||||
):
|
||||
"""Deprecated Azure Responses client. Use OpenAIChatClient with an AsyncAzureOpenAI client instead."""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.openai"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
deployment_name: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
api_version: str | None = None,
|
||||
token_endpoint: str | None = None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncOpenAI | None = None,
|
||||
project_client: Any | None = None,
|
||||
project_endpoint: str | None = None,
|
||||
allow_preview: bool | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
instruction_role: str | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize an Azure OpenAI Responses client.
|
||||
|
||||
Keyword Args:
|
||||
api_key: The API key.
|
||||
deployment_name: The deployment name.
|
||||
endpoint: The deployment endpoint.
|
||||
base_url: The deployment base URL.
|
||||
api_version: The deployment API version.
|
||||
token_endpoint: The token endpoint to request an Azure token.
|
||||
credential: Azure credential or token provider for authentication.
|
||||
default_headers: Default headers for HTTP requests.
|
||||
async_client: An existing client to use.
|
||||
project_client: An existing AIProjectClient to use.
|
||||
project_endpoint: The Azure AI Foundry project endpoint URL.
|
||||
allow_preview: Enables preview opt-in on internally-created AIProjectClient.
|
||||
env_file_path: Path to .env file for settings.
|
||||
env_file_encoding: Encoding for .env file.
|
||||
instruction_role: The role to use for 'instruction' messages.
|
||||
middleware: Optional sequence of middleware.
|
||||
function_invocation_configuration: Optional function invocation configuration.
|
||||
kwargs: Additional keyword arguments.
|
||||
"""
|
||||
if (model_id := kwargs.pop("model_id", None)) and not deployment_name:
|
||||
deployment_name = str(model_id)
|
||||
|
||||
if async_client is None and (project_client is not None or project_endpoint is not None):
|
||||
async_client = self._create_client_from_project(
|
||||
project_client=project_client,
|
||||
project_endpoint=project_endpoint,
|
||||
credential=credential,
|
||||
allow_preview=allow_preview,
|
||||
)
|
||||
|
||||
azure_openai_settings = load_settings(
|
||||
AzureOpenAISettings,
|
||||
env_prefix="AZURE_OPENAI_",
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
endpoint=endpoint,
|
||||
responses_deployment_name=deployment_name,
|
||||
api_version=api_version,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
token_endpoint=token_endpoint,
|
||||
)
|
||||
_apply_azure_defaults(azure_openai_settings, default_api_version="preview")
|
||||
endpoint_value = azure_openai_settings.get("endpoint")
|
||||
if (
|
||||
not azure_openai_settings.get("base_url")
|
||||
and endpoint_value
|
||||
and (hostname := urlparse(str(endpoint_value)).hostname)
|
||||
and hostname.endswith(".openai.azure.com")
|
||||
):
|
||||
azure_openai_settings["base_url"] = urljoin(str(endpoint_value), "/openai/v1/")
|
||||
|
||||
responses_deployment_name = azure_openai_settings.get("responses_deployment_name")
|
||||
if not responses_deployment_name:
|
||||
raise ValueError(
|
||||
"Azure OpenAI deployment name is required. Set via 'deployment_name' parameter "
|
||||
"or 'AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME' environment variable."
|
||||
)
|
||||
|
||||
if not async_client:
|
||||
# Create the Azure OpenAI client directly
|
||||
merged_headers = dict(copy(default_headers)) if default_headers else {}
|
||||
if APP_INFO:
|
||||
merged_headers.update(APP_INFO)
|
||||
merged_headers = prepend_agent_framework_to_user_agent(merged_headers)
|
||||
|
||||
api_key_secret = azure_openai_settings.get("api_key")
|
||||
ad_token_provider = None
|
||||
if not api_key_secret and credential:
|
||||
ad_token_provider = resolve_credential_to_token_provider(
|
||||
credential, azure_openai_settings.get("token_endpoint")
|
||||
)
|
||||
|
||||
if not api_key_secret and not ad_token_provider:
|
||||
raise ValueError("Please provide either api_key, credential, or a client.")
|
||||
|
||||
client_endpoint = azure_openai_settings.get("endpoint")
|
||||
client_base_url = azure_openai_settings.get("base_url")
|
||||
if not client_endpoint and not client_base_url:
|
||||
raise ValueError("Please provide an endpoint or a base_url")
|
||||
|
||||
client_args: dict[str, Any] = {"default_headers": merged_headers}
|
||||
if resolved_api_version := azure_openai_settings.get("api_version"):
|
||||
client_args["api_version"] = resolved_api_version
|
||||
if ad_token_provider:
|
||||
client_args["azure_ad_token_provider"] = ad_token_provider
|
||||
if api_key_secret:
|
||||
client_args["api_key"] = api_key_secret.get_secret_value()
|
||||
if client_base_url:
|
||||
client_args["base_url"] = str(client_base_url)
|
||||
if client_endpoint and not client_base_url:
|
||||
client_args["azure_endpoint"] = str(client_endpoint)
|
||||
if responses_deployment_name:
|
||||
client_args["azure_deployment"] = responses_deployment_name
|
||||
if "websocket_base_url" in kwargs:
|
||||
client_args["websocket_base_url"] = kwargs.pop("websocket_base_url")
|
||||
|
||||
async_client = AsyncAzureOpenAI(**client_args)
|
||||
|
||||
# Store Azure-specific attributes for serialization
|
||||
self.endpoint = str(endpoint_value) if endpoint_value else None
|
||||
self.api_version = azure_openai_settings.get("api_version") or ""
|
||||
self.deployment_name = responses_deployment_name
|
||||
|
||||
super().__init__(
|
||||
async_client=async_client,
|
||||
model=responses_deployment_name,
|
||||
api_version=azure_openai_settings.get("api_version"),
|
||||
instruction_role=instruction_role,
|
||||
default_headers=default_headers,
|
||||
middleware=middleware, # type: ignore[arg-type]
|
||||
function_invocation_configuration=function_invocation_configuration,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _create_client_from_project(
|
||||
*,
|
||||
project_client: AIProjectClient | None,
|
||||
project_endpoint: str | None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None,
|
||||
allow_preview: bool | None = None,
|
||||
) -> AsyncOpenAI:
|
||||
"""Create an AsyncOpenAI client from an Azure AI Foundry project."""
|
||||
if project_client is not None:
|
||||
return project_client.get_openai_client()
|
||||
|
||||
if not project_endpoint:
|
||||
raise ValueError("Azure AI project endpoint is required when project_client is not provided.")
|
||||
if not credential:
|
||||
raise ValueError("Azure credential is required when using project_endpoint without a project_client.")
|
||||
project_client_kwargs: dict[str, Any] = {
|
||||
"endpoint": project_endpoint,
|
||||
"credential": credential, # type: ignore[arg-type]
|
||||
"user_agent": AGENT_FRAMEWORK_USER_AGENT,
|
||||
}
|
||||
if allow_preview is not None:
|
||||
project_client_kwargs["allow_preview"] = allow_preview
|
||||
project_client = AIProjectClient(**project_client_kwargs)
|
||||
return project_client.get_openai_client()
|
||||
|
||||
@override
|
||||
def _check_model_presence(self, options: dict[str, Any]) -> None:
|
||||
if not options.get("model"):
|
||||
if not self.model:
|
||||
raise ValueError("deployment_name must be a non-empty string")
|
||||
options["model"] = self.model
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region AzureOpenAIChatClient
|
||||
|
||||
|
||||
ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None)
|
||||
|
||||
|
||||
class AzureUserSecurityContext(TypedDict, total=False):
|
||||
"""User security context for Azure AI applications.
|
||||
|
||||
These fields help security operations teams investigate and mitigate security
|
||||
incidents by providing context about the application and end user.
|
||||
"""
|
||||
|
||||
application_name: str
|
||||
"""Name of the application making the request."""
|
||||
|
||||
end_user_id: str
|
||||
"""Unique identifier for the end user (recommend hashing username/email)."""
|
||||
|
||||
end_user_tenant_id: str
|
||||
"""Microsoft 365 tenant ID the end user belongs to. Required for multi-tenant apps."""
|
||||
|
||||
source_ip: str
|
||||
"""The original client's IP address."""
|
||||
|
||||
|
||||
class AzureOpenAIChatOptions(OpenAIChatCompletionOptions[ResponseModelT], Generic[ResponseModelT], total=False):
|
||||
"""Azure OpenAI-specific chat options dict.
|
||||
|
||||
Extends OpenAIChatCompletionOptions with Azure-specific options including
|
||||
the "On Your Data" feature and enhanced security context.
|
||||
"""
|
||||
|
||||
data_sources: list[dict[str, Any]]
|
||||
"""Azure "On Your Data" data sources for retrieval-augmented generation."""
|
||||
|
||||
user_security_context: AzureUserSecurityContext
|
||||
"""Enhanced security context for Azure Defender integration."""
|
||||
|
||||
n: int
|
||||
"""Number of chat completion choices to generate for each input message."""
|
||||
|
||||
|
||||
AzureOpenAIChatOptionsT = TypeVar(
|
||||
"AzureOpenAIChatOptionsT",
|
||||
bound=TypedDict, # type: ignore[valid-type]
|
||||
default="AzureOpenAIChatOptions",
|
||||
covariant=True,
|
||||
)
|
||||
|
||||
|
||||
@deprecated("AzureOpenAIChatClient is deprecated. Use OpenAIChatCompletionClient with an AsyncAzureOpenAI client.")
|
||||
class AzureOpenAIChatClient( # type: ignore[misc]
|
||||
FunctionInvocationLayer[AzureOpenAIChatOptionsT],
|
||||
ChatMiddlewareLayer[AzureOpenAIChatOptionsT],
|
||||
ChatTelemetryLayer[AzureOpenAIChatOptionsT],
|
||||
RawOpenAIChatCompletionClient[AzureOpenAIChatOptionsT],
|
||||
Generic[AzureOpenAIChatOptionsT],
|
||||
):
|
||||
"""Deprecated Azure OpenAI Chat client. Use OpenAIChatCompletionClient with AsyncAzureOpenAI instead."""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.openai"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
deployment_name: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
api_version: str | None = None,
|
||||
token_endpoint: str | None = None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncAzureOpenAI | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
instruction_role: str | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
) -> None:
|
||||
"""Initialize an Azure OpenAI Chat completion client.
|
||||
|
||||
Keyword Args:
|
||||
api_key: The API key.
|
||||
deployment_name: The deployment name.
|
||||
endpoint: The deployment endpoint.
|
||||
base_url: The deployment base URL.
|
||||
api_version: The deployment API version.
|
||||
token_endpoint: The token endpoint to request an Azure token.
|
||||
credential: Azure credential or token provider for authentication.
|
||||
default_headers: Default headers for HTTP requests.
|
||||
async_client: An existing client to use.
|
||||
additional_properties: Additional properties stored on the client instance.
|
||||
env_file_path: Path to .env file for settings.
|
||||
env_file_encoding: Encoding for .env file.
|
||||
instruction_role: The role to use for 'instruction' messages.
|
||||
middleware: Optional sequence of middleware.
|
||||
function_invocation_configuration: Optional function invocation configuration.
|
||||
"""
|
||||
azure_openai_settings = load_settings(
|
||||
AzureOpenAISettings,
|
||||
env_prefix="AZURE_OPENAI_",
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
endpoint=endpoint,
|
||||
chat_deployment_name=deployment_name,
|
||||
api_version=api_version,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
token_endpoint=token_endpoint,
|
||||
)
|
||||
_apply_azure_defaults(azure_openai_settings)
|
||||
|
||||
chat_deployment_name = azure_openai_settings.get("chat_deployment_name")
|
||||
if not chat_deployment_name:
|
||||
raise ValueError(
|
||||
"Azure OpenAI deployment name is required. Set via 'deployment_name' parameter "
|
||||
"or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable."
|
||||
)
|
||||
|
||||
if not async_client:
|
||||
# Create the Azure OpenAI client directly
|
||||
merged_headers = dict(copy(default_headers)) if default_headers else {}
|
||||
if APP_INFO:
|
||||
merged_headers.update(APP_INFO)
|
||||
merged_headers = prepend_agent_framework_to_user_agent(merged_headers)
|
||||
|
||||
api_key_secret = azure_openai_settings.get("api_key")
|
||||
ad_token_provider = None
|
||||
if not api_key_secret and credential:
|
||||
ad_token_provider = resolve_credential_to_token_provider(
|
||||
credential, azure_openai_settings.get("token_endpoint")
|
||||
)
|
||||
|
||||
if not api_key_secret and not ad_token_provider:
|
||||
raise ValueError("Please provide either api_key, credential, or a client.")
|
||||
|
||||
endpoint_value = azure_openai_settings.get("endpoint")
|
||||
base_url_value = azure_openai_settings.get("base_url")
|
||||
if not endpoint_value and not base_url_value:
|
||||
raise ValueError("Please provide an endpoint or a base_url")
|
||||
|
||||
client_args: dict[str, Any] = {"default_headers": merged_headers}
|
||||
if resolved_api_version := azure_openai_settings.get("api_version"):
|
||||
client_args["api_version"] = resolved_api_version
|
||||
if ad_token_provider:
|
||||
client_args["azure_ad_token_provider"] = ad_token_provider
|
||||
if api_key_secret:
|
||||
client_args["api_key"] = api_key_secret.get_secret_value()
|
||||
if base_url_value:
|
||||
client_args["base_url"] = str(base_url_value)
|
||||
if endpoint_value and not base_url_value:
|
||||
client_args["azure_endpoint"] = str(endpoint_value)
|
||||
if chat_deployment_name:
|
||||
client_args["azure_deployment"] = chat_deployment_name
|
||||
|
||||
async_client = AsyncAzureOpenAI(**client_args)
|
||||
|
||||
# Store Azure-specific attributes for serialization
|
||||
self.endpoint = str(azure_openai_settings.get("endpoint") or "")
|
||||
self.api_version = azure_openai_settings.get("api_version") or ""
|
||||
self.deployment_name = chat_deployment_name
|
||||
|
||||
super().__init__(
|
||||
async_client=async_client,
|
||||
model=chat_deployment_name,
|
||||
api_version=azure_openai_settings.get("api_version"),
|
||||
instruction_role=instruction_role,
|
||||
default_headers=default_headers,
|
||||
additional_properties=additional_properties,
|
||||
middleware=middleware, # type: ignore[arg-type]
|
||||
function_invocation_configuration=function_invocation_configuration,
|
||||
)
|
||||
|
||||
@override
|
||||
def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> Content | None:
|
||||
"""Parse the choice into a Content object with type='text'.
|
||||
|
||||
Overwritten from RawOpenAIChatCompletionClient to deal with Azure On Your Data function.
|
||||
"""
|
||||
message = getattr(choice, "message", None)
|
||||
if message is None:
|
||||
message = getattr(choice, "delta", None)
|
||||
if message is None: # type: ignore
|
||||
return None
|
||||
if hasattr(message, "refusal") and message.refusal:
|
||||
return Content.from_text(text=message.refusal, raw_representation=choice)
|
||||
if not message.content:
|
||||
return None
|
||||
text_content = Content.from_text(text=message.content, raw_representation=choice)
|
||||
if not message.model_extra or "context" not in message.model_extra:
|
||||
return text_content
|
||||
|
||||
context_raw: object = cast(object, message.context) # type: ignore[union-attr]
|
||||
if isinstance(context_raw, str):
|
||||
try:
|
||||
context_raw = json.loads(context_raw)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Context is not a valid JSON string, ignoring context.")
|
||||
return text_content
|
||||
if not isinstance(context_raw, dict):
|
||||
logger.warning("Context is not a valid dictionary, ignoring context.")
|
||||
return text_content
|
||||
context = cast(dict[str, Any], context_raw)
|
||||
if intent := context.get("intent"):
|
||||
text_content.additional_properties = {"intent": intent}
|
||||
citations = context.get("citations")
|
||||
if isinstance(citations, list) and citations:
|
||||
annotations: list[Annotation] = []
|
||||
for citation_raw in cast(list[object], citations):
|
||||
if not isinstance(citation_raw, dict):
|
||||
continue
|
||||
citation = cast(dict[str, Any], citation_raw)
|
||||
annotations.append(
|
||||
Annotation(
|
||||
type="citation",
|
||||
title=citation.get("title", ""),
|
||||
url=citation.get("url", ""),
|
||||
snippet=citation.get("content", ""),
|
||||
file_id=citation.get("filepath", ""),
|
||||
tool_name="Azure-on-your-Data",
|
||||
additional_properties={"chunk_id": citation.get("chunk_id", "")},
|
||||
raw_representation=citation,
|
||||
)
|
||||
)
|
||||
text_content.annotations = annotations
|
||||
return text_content
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region AzureOpenAIAssistantsClient
|
||||
|
||||
|
||||
AzureOpenAIAssistantsOptionsT = TypeVar(
|
||||
"AzureOpenAIAssistantsOptionsT",
|
||||
bound=TypedDict, # type: ignore[valid-type]
|
||||
default="OpenAIAssistantsOptions",
|
||||
covariant=True,
|
||||
)
|
||||
|
||||
AzureOpenAIAssistantsOptions = OpenAIAssistantsOptions
|
||||
|
||||
|
||||
@deprecated(
|
||||
"AzureOpenAIAssistantsClient is deprecated. "
|
||||
"Use OpenAIAssistantsClient (also deprecated) or migrate to OpenAIChatClient."
|
||||
)
|
||||
class AzureOpenAIAssistantsClient(
|
||||
OpenAIAssistantsClient[AzureOpenAIAssistantsOptionsT], Generic[AzureOpenAIAssistantsOptionsT]
|
||||
):
|
||||
"""Deprecated Azure OpenAI Assistants client. Use OpenAIAssistantsClient or migrate to OpenAIChatClient."""
|
||||
|
||||
DEFAULT_AZURE_API_VERSION: ClassVar[str] = "2024-05-01-preview"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
deployment_name: str | None = None,
|
||||
assistant_id: str | None = None,
|
||||
assistant_name: str | None = None,
|
||||
assistant_description: str | None = None,
|
||||
thread_id: str | None = None,
|
||||
api_key: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
api_version: str | None = None,
|
||||
token_endpoint: str | None = None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncAzureOpenAI | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize an Azure OpenAI Assistants client.
|
||||
|
||||
Keyword Args:
|
||||
deployment_name: The Azure OpenAI deployment name.
|
||||
assistant_id: The ID of an Azure OpenAI assistant to use.
|
||||
assistant_name: The name to use when creating new assistants.
|
||||
assistant_description: The description to use when creating new assistants.
|
||||
thread_id: Default thread ID to use for conversations.
|
||||
api_key: The API key to use.
|
||||
endpoint: The deployment endpoint.
|
||||
base_url: The deployment base URL.
|
||||
api_version: The deployment API version.
|
||||
token_endpoint: The token endpoint to request an Azure token.
|
||||
credential: Azure credential or token provider for authentication.
|
||||
default_headers: Default headers for HTTP requests.
|
||||
async_client: An existing client to use.
|
||||
env_file_path: Path to .env file for settings.
|
||||
env_file_encoding: Encoding for .env file.
|
||||
"""
|
||||
azure_openai_settings = load_settings(
|
||||
AzureOpenAISettings,
|
||||
env_prefix="AZURE_OPENAI_",
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
endpoint=endpoint,
|
||||
chat_deployment_name=deployment_name,
|
||||
api_version=api_version,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
token_endpoint=token_endpoint,
|
||||
)
|
||||
_apply_azure_defaults(azure_openai_settings, default_api_version=self.DEFAULT_AZURE_API_VERSION)
|
||||
|
||||
chat_deployment_name = azure_openai_settings.get("chat_deployment_name")
|
||||
if not chat_deployment_name:
|
||||
raise ValueError(
|
||||
"Azure OpenAI deployment name is required. Set via 'deployment_name' parameter "
|
||||
"or 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME' environment variable."
|
||||
)
|
||||
|
||||
api_key_secret = azure_openai_settings.get("api_key")
|
||||
token_scope = azure_openai_settings.get("token_endpoint")
|
||||
|
||||
ad_token_provider = None
|
||||
if not async_client and not api_key_secret and credential:
|
||||
ad_token_provider = resolve_credential_to_token_provider(credential, token_scope)
|
||||
|
||||
if not async_client and not api_key_secret and not ad_token_provider:
|
||||
raise ValueError("Please provide either api_key, credential, or a client.")
|
||||
|
||||
if not async_client:
|
||||
client_params: dict[str, Any] = {
|
||||
"default_headers": default_headers,
|
||||
}
|
||||
if resolved_api_version := azure_openai_settings.get("api_version"):
|
||||
client_params["api_version"] = resolved_api_version
|
||||
|
||||
if api_key_secret:
|
||||
client_params["api_key"] = api_key_secret.get_secret_value()
|
||||
elif ad_token_provider:
|
||||
client_params["azure_ad_token_provider"] = ad_token_provider
|
||||
|
||||
if resolved_base_url := azure_openai_settings.get("base_url"):
|
||||
client_params["base_url"] = str(resolved_base_url)
|
||||
elif resolved_endpoint := azure_openai_settings.get("endpoint"):
|
||||
client_params["azure_endpoint"] = str(resolved_endpoint)
|
||||
|
||||
async_client = AsyncAzureOpenAI(**client_params)
|
||||
|
||||
super().__init__(
|
||||
model_id=chat_deployment_name,
|
||||
assistant_id=assistant_id,
|
||||
assistant_name=assistant_name,
|
||||
assistant_description=assistant_description,
|
||||
thread_id=thread_id,
|
||||
async_client=async_client, # type: ignore[reportArgumentType]
|
||||
default_headers=default_headers,
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region AzureOpenAIEmbeddingClient
|
||||
|
||||
|
||||
AzureOpenAIEmbeddingOptionsT = TypeVar(
|
||||
"AzureOpenAIEmbeddingOptionsT",
|
||||
bound=TypedDict, # type: ignore[valid-type]
|
||||
default="OpenAIEmbeddingOptions",
|
||||
covariant=True,
|
||||
)
|
||||
|
||||
|
||||
@deprecated("AzureOpenAIEmbeddingClient is deprecated. Use OpenAIEmbeddingClient with an AsyncAzureOpenAI client.")
|
||||
class AzureOpenAIEmbeddingClient(
|
||||
EmbeddingTelemetryLayer[str, list[float], AzureOpenAIEmbeddingOptionsT],
|
||||
RawOpenAIEmbeddingClient[AzureOpenAIEmbeddingOptionsT],
|
||||
Generic[AzureOpenAIEmbeddingOptionsT],
|
||||
):
|
||||
"""Deprecated Azure OpenAI embedding client. Use OpenAIEmbeddingClient with AsyncAzureOpenAI instead."""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.openai"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
deployment_name: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
base_url: str | None = None,
|
||||
api_version: str | None = None,
|
||||
token_endpoint: str | None = None,
|
||||
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncAzureOpenAI | None = None,
|
||||
otel_provider_name: str | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize an Azure OpenAI embedding client.
|
||||
|
||||
Keyword Args:
|
||||
api_key: The API key.
|
||||
deployment_name: The deployment name.
|
||||
endpoint: The deployment endpoint.
|
||||
base_url: The deployment base URL.
|
||||
api_version: The deployment API version.
|
||||
token_endpoint: The token endpoint to request an Azure token.
|
||||
credential: Azure credential or token provider for authentication.
|
||||
default_headers: Default headers for HTTP requests.
|
||||
async_client: An existing client to use.
|
||||
otel_provider_name: Override the OpenTelemetry provider name.
|
||||
env_file_path: Path to .env file for settings.
|
||||
env_file_encoding: Encoding for .env file.
|
||||
"""
|
||||
azure_openai_settings = load_settings(
|
||||
AzureOpenAISettings,
|
||||
env_prefix="AZURE_OPENAI_",
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
endpoint=endpoint,
|
||||
embedding_deployment_name=deployment_name,
|
||||
api_version=api_version,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
token_endpoint=token_endpoint,
|
||||
)
|
||||
_apply_azure_defaults(azure_openai_settings)
|
||||
|
||||
embedding_deployment_name = azure_openai_settings.get("embedding_deployment_name")
|
||||
if not embedding_deployment_name:
|
||||
raise ValueError(
|
||||
"Azure OpenAI embedding deployment name is required. Set via 'deployment_name' parameter "
|
||||
"or 'AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME' environment variable."
|
||||
)
|
||||
|
||||
if not async_client:
|
||||
# Create the Azure OpenAI client directly
|
||||
merged_headers = dict(copy(default_headers)) if default_headers else {}
|
||||
if APP_INFO:
|
||||
merged_headers.update(APP_INFO)
|
||||
merged_headers = prepend_agent_framework_to_user_agent(merged_headers)
|
||||
|
||||
api_key_secret = azure_openai_settings.get("api_key")
|
||||
ad_token_provider = None
|
||||
if not api_key_secret and credential:
|
||||
ad_token_provider = resolve_credential_to_token_provider(
|
||||
credential, azure_openai_settings.get("token_endpoint")
|
||||
)
|
||||
|
||||
if not api_key_secret and not ad_token_provider:
|
||||
raise ValueError("Please provide either api_key, credential, or a client.")
|
||||
|
||||
endpoint_value = azure_openai_settings.get("endpoint")
|
||||
base_url_value = azure_openai_settings.get("base_url")
|
||||
if not endpoint_value and not base_url_value:
|
||||
raise ValueError("Please provide an endpoint or a base_url")
|
||||
|
||||
client_args: dict[str, Any] = {"default_headers": merged_headers}
|
||||
if resolved_api_version := azure_openai_settings.get("api_version"):
|
||||
client_args["api_version"] = resolved_api_version
|
||||
if ad_token_provider:
|
||||
client_args["azure_ad_token_provider"] = ad_token_provider
|
||||
if api_key_secret:
|
||||
client_args["api_key"] = api_key_secret.get_secret_value()
|
||||
if base_url_value:
|
||||
client_args["base_url"] = str(base_url_value)
|
||||
if endpoint_value and not base_url_value:
|
||||
client_args["azure_endpoint"] = str(endpoint_value)
|
||||
if embedding_deployment_name:
|
||||
client_args["azure_deployment"] = embedding_deployment_name
|
||||
|
||||
async_client = AsyncAzureOpenAI(**client_args)
|
||||
|
||||
# Store Azure-specific attributes for serialization
|
||||
self.endpoint = str(azure_openai_settings.get("endpoint") or "")
|
||||
self.api_version = azure_openai_settings.get("api_version") or ""
|
||||
self.deployment_name = embedding_deployment_name
|
||||
|
||||
super().__init__(
|
||||
async_client=async_client,
|
||||
model=embedding_deployment_name,
|
||||
default_headers=default_headers,
|
||||
)
|
||||
if otel_provider_name is not None:
|
||||
self.OTEL_PROVIDER_NAME = otel_provider_name # type: ignore[misc]
|
||||
|
||||
|
||||
# endregion
|
||||
@@ -0,0 +1,67 @@
|
||||
# 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,261 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Foundry Memory Context Provider using BaseContextProvider.
|
||||
|
||||
This module provides ``FoundryMemoryProvider``, built on
|
||||
:class:`BaseContextProvider`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
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.azure._entra_id_authentication import AzureCredentialTypes
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from openai.types.responses import ResponseInputItemParam
|
||||
|
||||
from ._shared import AzureAISettings
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import Self # pragma: no cover
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agent_framework._agents import SupportsAgentRun
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FoundryMemoryProvider(BaseContextProvider):
|
||||
"""Foundry Memory context provider using the new BaseContextProvider hooks pattern.
|
||||
|
||||
Integrates Azure AI Foundry Memory Store for persistent semantic memory,
|
||||
searching and storing memories via the Azure AI Projects SDK.
|
||||
|
||||
Args:
|
||||
source_id: Unique identifier for this provider instance.
|
||||
project_client: Azure AI Project client for memory operations.
|
||||
memory_store_name: The name of the memory store to use.
|
||||
scope: The namespace that logically groups and isolates memories (e.g., user ID).
|
||||
context_prompt: The prompt to prepend to retrieved memories.
|
||||
update_delay: Timeout period before processing memory update in seconds.
|
||||
Defaults to 300 (5 minutes). Set to 0 to immediately trigger updates.
|
||||
"""
|
||||
|
||||
DEFAULT_SOURCE_ID: ClassVar[str] = "foundry_memory"
|
||||
DEFAULT_CONTEXT_PROMPT = "## Memories\nConsider the following memories when answering user questions:"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source_id: str = DEFAULT_SOURCE_ID,
|
||||
*,
|
||||
project_client: AIProjectClient | None = None,
|
||||
project_endpoint: str | None = None,
|
||||
credential: AzureCredentialTypes | None = None,
|
||||
allow_preview: bool | None = None,
|
||||
memory_store_name: str,
|
||||
scope: str | None = None,
|
||||
context_prompt: str | None = None,
|
||||
update_delay: int = 300,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the Foundry Memory context provider.
|
||||
|
||||
Args:
|
||||
source_id: Unique identifier for this provider instance.
|
||||
project_client: Azure AI Project client for memory operations.
|
||||
project_endpoint: Azure AI project endpoint URL. Used when project_client is not provided.
|
||||
credential: Azure credential for authentication. Accepts a TokenCredential,
|
||||
AsyncTokenCredential, or a callable token provider.
|
||||
Required when project_client is not provided.
|
||||
allow_preview: Enables preview opt-in on internally-created ``AIProjectClient``.
|
||||
memory_store_name: The name of the memory store to use.
|
||||
scope: The namespace that logically groups and isolates memories (e.g., user ID).
|
||||
If None, `session_id` will be used.
|
||||
context_prompt: The prompt to prepend to retrieved memories.
|
||||
update_delay: Timeout period before processing memory update in seconds.
|
||||
env_file_path: Path to environment file for loading settings.
|
||||
env_file_encoding: Encoding of the environment file.
|
||||
"""
|
||||
super().__init__(source_id)
|
||||
azure_ai_settings = load_settings(
|
||||
AzureAISettings,
|
||||
env_prefix="AZURE_AI_",
|
||||
project_endpoint=project_endpoint,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
|
||||
if project_client is None:
|
||||
resolved_endpoint = azure_ai_settings.get("project_endpoint")
|
||||
if not resolved_endpoint:
|
||||
raise ValueError(
|
||||
"Azure AI project endpoint is required. Set via 'project_endpoint' parameter "
|
||||
"or 'AZURE_AI_PROJECT_ENDPOINT' environment variable."
|
||||
)
|
||||
if not credential:
|
||||
raise ValueError("Azure credential is required when project_client is not provided.")
|
||||
project_client_kwargs: dict[str, Any] = {
|
||||
"endpoint": resolved_endpoint,
|
||||
"credential": credential, # type: ignore[arg-type]
|
||||
"user_agent": AGENT_FRAMEWORK_USER_AGENT,
|
||||
}
|
||||
if allow_preview is not None:
|
||||
project_client_kwargs["allow_preview"] = allow_preview
|
||||
project_client = AIProjectClient(**project_client_kwargs)
|
||||
|
||||
if not memory_store_name:
|
||||
raise ValueError("memory_store_name is required")
|
||||
if not scope:
|
||||
raise ValueError("scope is required")
|
||||
|
||||
self.project_client = project_client
|
||||
self.memory_store_name = memory_store_name
|
||||
self.scope = scope
|
||||
self.context_prompt = context_prompt or self.DEFAULT_CONTEXT_PROMPT
|
||||
self.update_delay = update_delay
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
"""Async context manager entry."""
|
||||
if self.project_client and isinstance(self.project_client, AbstractAsyncContextManager):
|
||||
await self.project_client.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None:
|
||||
"""Async context manager exit."""
|
||||
if self.project_client and isinstance(self.project_client, AbstractAsyncContextManager):
|
||||
await self.project_client.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
# -- Hooks pattern ---------------------------------------------------------
|
||||
|
||||
async def before_run(
|
||||
self,
|
||||
*,
|
||||
agent: SupportsAgentRun,
|
||||
session: AgentSession,
|
||||
context: SessionContext,
|
||||
state: dict[str, Any],
|
||||
) -> None:
|
||||
"""Search Foundry Memory for relevant memories and add to the session context.
|
||||
|
||||
This method:
|
||||
1. Retrieves static memories (user profile) on first call per session
|
||||
2. Searches for contextual memories based on input messages
|
||||
3. Combines and injects memories into the context
|
||||
"""
|
||||
# On first run, retrieve static memories (user profile memories)
|
||||
if not state.get("initialized"):
|
||||
try:
|
||||
static_search_result = await self.project_client.beta.memory_stores.search_memories(
|
||||
name=self.memory_store_name,
|
||||
scope=self.scope or context.session_id, # type: ignore[arg-type]
|
||||
)
|
||||
static_memories = [{"content": memory.memory_item.content} for memory in static_search_result.memories]
|
||||
state["static_memories"] = static_memories
|
||||
except Exception as e:
|
||||
# Log but don't fail - memory retrieval is non-critical
|
||||
logger.warning(f"Failed to retrieve static memories: {e}")
|
||||
state["static_memories"] = []
|
||||
finally:
|
||||
# Mark as initialized regardless of success to avoid repeated attempts
|
||||
state["initialized"] = True
|
||||
|
||||
# Search for contextual memories based on input messages
|
||||
# Check if there are any non-empty input messages
|
||||
has_input = any(msg and msg.text and msg.text.strip() for msg in context.input_messages)
|
||||
if not has_input:
|
||||
return
|
||||
|
||||
# Convert input messages to memory search item format
|
||||
items: list[ResponseInputItemParam] = [
|
||||
{"type": "message", "role": "user", "content": msg.text}
|
||||
for msg in context.input_messages
|
||||
if msg and msg.text and msg.text.strip()
|
||||
]
|
||||
|
||||
try:
|
||||
search_result = await self.project_client.beta.memory_stores.search_memories(
|
||||
name=self.memory_store_name,
|
||||
scope=self.scope or context.session_id, # type: ignore[arg-type]
|
||||
items=items,
|
||||
previous_search_id=state.get("previous_search_id"),
|
||||
)
|
||||
|
||||
# Extract search_id for next incremental search
|
||||
if search_result.memories:
|
||||
state["previous_search_id"] = search_result.search_id
|
||||
|
||||
# Combine static and contextual memories
|
||||
contextual_memories = [{"content": memory.memory_item.content} for memory in search_result.memories]
|
||||
|
||||
all_memories = state.get("static_memories", []) + contextual_memories
|
||||
|
||||
# Inject memories into context
|
||||
if all_memories:
|
||||
line_separated_memories = "\n".join(
|
||||
str(memory.get("content", "")) for memory in all_memories if memory.get("content")
|
||||
)
|
||||
if line_separated_memories:
|
||||
context.extend_messages(
|
||||
self.source_id,
|
||||
[Message(role="user", text=f"{self.context_prompt}\n{line_separated_memories}")],
|
||||
)
|
||||
except Exception as e:
|
||||
# Log but don't fail - memory retrieval is non-critical
|
||||
logger.warning(f"Failed to search contextual memories: {e}")
|
||||
|
||||
async def after_run(
|
||||
self,
|
||||
*,
|
||||
agent: SupportsAgentRun,
|
||||
session: AgentSession,
|
||||
context: SessionContext,
|
||||
state: dict[str, Any],
|
||||
) -> None:
|
||||
"""Store request/response messages to Foundry Memory for future retrieval.
|
||||
|
||||
This method updates the memory store with conversation messages.
|
||||
The update is debounced by the configured update_delay.
|
||||
"""
|
||||
messages_to_store: list[Message] = list(context.input_messages)
|
||||
if context.response and context.response.messages:
|
||||
messages_to_store.extend(context.response.messages)
|
||||
|
||||
# Filter and convert messages to memory update item format
|
||||
items: list[ResponseInputItemParam] = []
|
||||
for message in messages_to_store:
|
||||
if message.role in {"user", "assistant", "system"} and message.text and message.text.strip():
|
||||
if message.role == "user":
|
||||
items.append({"role": "user", "type": "message", "content": message.text})
|
||||
elif message.role == "assistant":
|
||||
items.append({"role": "assistant", "type": "message", "content": message.text})
|
||||
|
||||
if not items:
|
||||
return
|
||||
|
||||
try:
|
||||
# Fire and forget - don't wait for the update to complete
|
||||
update_poller = await self.project_client.beta.memory_stores.begin_update_memories(
|
||||
name=self.memory_store_name,
|
||||
scope=self.scope or context.session_id, # type: ignore[arg-type]
|
||||
items=items,
|
||||
previous_update_id=state.get("previous_update_id"),
|
||||
update_delay=self.update_delay,
|
||||
)
|
||||
# Store the update_id for next incremental update
|
||||
state["previous_update_id"] = update_poller.update_id
|
||||
|
||||
except Exception as e:
|
||||
# Log but don't fail - memory storage is non-critical
|
||||
logger.warning(f"Failed to update memories: {e}")
|
||||
|
||||
|
||||
__all__ = ["FoundryMemoryProvider"]
|
||||
@@ -18,7 +18,6 @@ from agent_framework import (
|
||||
from agent_framework._mcp import MCPTool
|
||||
from agent_framework._settings import load_settings
|
||||
from agent_framework._tools import ToolTypes
|
||||
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import (
|
||||
AgentVersionDetails,
|
||||
@@ -29,13 +28,15 @@ from azure.ai.projects.models import (
|
||||
FunctionTool as AzureFunctionTool,
|
||||
)
|
||||
|
||||
from ._client import AzureAIClient, AzureAIProjectAgentOptions
|
||||
from ._client import AzureAIClient, AzureAIProjectAgentOptions # pyright: ignore[reportDeprecated]
|
||||
from ._entra_id_authentication import AzureCredentialTypes
|
||||
from ._shared import AzureAISettings, create_text_format_config, from_azure_ai_tools, to_azure_ai_tools
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar # type: ignore # pragma: no cover
|
||||
from warnings import deprecated # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import TypeVar # type: ignore # pragma: no cover
|
||||
from typing_extensions import TypeVar, deprecated # type: ignore # pragma: no cover
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self, TypedDict # type: ignore # pragma: no cover
|
||||
else:
|
||||
@@ -55,11 +56,12 @@ OptionsCoT = TypeVar(
|
||||
)
|
||||
|
||||
|
||||
@deprecated("AzureAIProjectAgentProvider is deprecated. Use FoundryAgent instead.")
|
||||
class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
"""Provider for Azure AI Agent Service (Responses API).
|
||||
"""Deprecated provider for Azure AI Agent Service (Responses API).
|
||||
|
||||
This provider allows you to create, retrieve, and manage Azure AI agents
|
||||
using the AIProjectClient from the Azure AI Projects SDK.
|
||||
This provider is deprecated. Use ``FoundryAgent`` instead to connect to
|
||||
pre-configured agents in Foundry.
|
||||
|
||||
Examples:
|
||||
Using with explicit AIProjectClient:
|
||||
@@ -200,7 +202,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
)
|
||||
|
||||
# Extract options from default_options if present
|
||||
opts = dict(default_options) if default_options else {}
|
||||
opts: dict[str, Any] = dict(default_options) if default_options else {}
|
||||
response_format = opts.get("response_format")
|
||||
rai_config = opts.get("rai_config")
|
||||
reasoning = opts.get("reasoning")
|
||||
@@ -384,7 +386,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
if not isinstance(details.definition, PromptAgentDefinition):
|
||||
raise ValueError("Agent definition must be PromptAgentDefinition to get a Agent.")
|
||||
|
||||
client = AzureAIClient(
|
||||
client = AzureAIClient( # pyright: ignore[reportDeprecated]
|
||||
project_client=self._project_client,
|
||||
agent_name=details.name,
|
||||
agent_version=details.version,
|
||||
|
||||
Reference in New Issue
Block a user