Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 1 addition & 2 deletions .github/workflows/python-check-coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@
"packages.purview.agent_framework_purview",
"packages.anthropic.agent_framework_anthropic",
"packages.azure-ai-search.agent_framework_azure_ai_search",
"packages.core.agent_framework.azure",
"packages.core.agent_framework.openai",
"packages.openai.agent_framework_openai",
# Individual files (if you want to enforce specific files instead of whole packages)
"packages/core/agent_framework/observability.py",
# Add more targets here as coverage improves
Expand Down
72 changes: 72 additions & 0 deletions docs/decisions/0021-provider-leading-clients.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
status: accepted
contact: eavanvalkenburg
date: 2026-03-20
deciders: eavanvalkenburg, sphenry, chetantoshnival
consulted: taochenosu, moonbox3, dmytrostruk, giles17, alliscode
---

# Provider-Leading Client Design & OpenAI Package Extraction

## Context and Problem Statement

The `agent-framework-core` package currently bundles OpenAI and Azure OpenAI client implementations along with their dependencies (`openai`, `azure-identity`, `azure-ai-projects`, `packaging`). This makes core heavier than necessary for users who don't use OpenAI, and it conflates the core abstractions with a specific provider implementation. Additionally, the current class naming (`OpenAIResponsesClient`, `OpenAIChatClient`) is based on the underlying OpenAI API names rather than what users actually want to do, making discoverability harder for newcomers.

## Decision Drivers

- **Lightweight core**: Core should only contain abstractions, middleware infrastructure, and telemetry — no provider-specific code or dependencies.
- **Discoverability-first**: Import namespaces should guide users to the right client. `from agent_framework.openai import ...` should surface all OpenAI-related clients; `from agent_framework.azure import ...` should surface Foundry, Azure AI, and other Azure-specific classes.
- **Provider-leading naming**: The primary client name should reflect the provider, not the underlying API. The Responses API is now the recommended default for OpenAI, so its client should be called `OpenAIChatClient` (not `OpenAIResponsesClient`).
- **Clean separation of concerns**: Azure-specific deprecated wrappers belong in the azure-ai package, not in the OpenAI package.

## Considered Options

- **Keep OpenAI in core**: Simpler but keeps core heavy; doesn't help discoverability.
- **Extract OpenAI with Azure wrappers in the OpenAI package**: Keeps Azure OpenAI wrappers alongside OpenAI code, but pollutes the OpenAI package with Azure concerns.
- **Extract OpenAI, place Azure wrappers in azure-ai**: Clean separation; the OpenAI package has zero Azure dependencies; deprecated Azure wrappers live in a single file in azure-ai for easy future deletion.

## Decision Outcome

Chosen option: "Extract OpenAI, place Azure wrappers in azure-ai", because it achieves the lightest core, cleanest OpenAI package, and the most maintainable deprecation path.

Key changes:

1. **New `agent-framework-openai` package** with dependencies on `agent-framework-core`, `openai`, and `packaging` only.
2. **Class renames**: `OpenAIResponsesClient` → `OpenAIChatClient` (Responses API), `OpenAIChatClient` → `OpenAIChatCompletionClient` (Chat Completions API). Old names remain as deprecated aliases.
3. **Deprecated classes**: `OpenAIAssistantsClient`, all `AzureOpenAI*Client` classes, `AzureAIClient`, `AzureAIAgentClient`, and `AzureAIProjectAgentProvider` are marked deprecated.
4. **New `FoundryChatClient`** in azure-ai for Azure AI Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
5. **All deprecated `AzureOpenAI*` classes** consolidated into a single file (`_deprecated_azure_openai.py`) in the azure-ai package for clean future deletion.
6. **Core's `agent_framework.openai` and `agent_framework.azure` namespaces** become lazy-loading gateways, preserving backward-compatible import paths while removing hard dependencies.
7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_RESPONSES_MODEL_ID` / `OPENAI_CHAT_MODEL_ID`).
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Azure AI Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.

### Foundry Agent Design: `FoundryAgentClient` vs `FoundryAgent`

The existing `AzureAIClient` combines two concerns: CRUD lifecycle management (creating/deleting agents on the service) and runtime communication (sending messages via the Responses API). The new design removes CRUD entirely — users connect to agents that already exist in Foundry.

**Two approaches were considered:**

**Option A — `FoundryAgentClient` only (public ChatClient):**
Users compose `Agent(client=FoundryAgentClient(...), tools=[...])`. This follows the universal `Agent(client=X)` pattern used by every other provider. However, a "client" that wraps a named remote agent (with `agent_name` as a constructor param) is semantically odd — clients typically wrap a model endpoint, not a specific agent.

**Option B — `FoundryAgent` (Agent subclass) + private `_FoundryAgentChatClient` and public `RawFoundryAgentChatClient`:**
Users write `FoundryAgent(agent_name="my-agent", ...)` for the common case. Internally, `FoundryAgent` creates a `_FoundryAgentChatClient` and passes it to the standard `Agent` base class. For advanced customization, users pass `client_type=RawFoundryAgentChatClient` (or a custom subclass) to control the client middleware layers. The `Agent(client=RawFoundryAgentChatClient(...))` composition pattern still works for users who prefer it.

**Chosen option: Option B**, because:
- The common case (`FoundryAgent(...)`) is a single object with no boilerplate.
- `client_type=` gives full control over client middleware without parameter duplication — the agent forwards connection params to the client internally.
- `RawFoundryAgent(RawAgent)` and `FoundryAgent(Agent)` mirror the established `RawAgent`/`Agent` pattern.
- Runtime validation (only `FunctionTool` allowed) lives in `RawFoundryAgentChatClient._prepare_options`, ensuring it applies regardless of how the client is used — through `FoundryAgent`, `Agent(client=...)`, or any custom composition.

**Public classes:**
- `RawFoundryAgentChatClient(RawOpenAIChatClient)` — Responses API client that injects agent reference and validates tools. Extension point for custom client middleware.
- `RawFoundryAgent(RawAgent)` — Agent without agent-level middleware/telemetry.
- `FoundryAgent(AgentTelemetryLayer, AgentMiddlewareLayer, RawFoundryAgent)` — Recommended production agent.

**Internal (private):**
- `_FoundryAgentChatClient` — Full client with function invocation, chat middleware, and telemetry layers. Created automatically by `FoundryAgent`; users customize via `client_type=RawFoundryAgentChatClient` or a custom subclass.

**Deprecated:**
- `AzureAIClient` — replaced by `FoundryAgent` (which uses `FoundryAgentClient` internally).
- `AzureAIAgentClient` — refers to V1 Agents Service API, no direct replacement.
- `AzureAIProjectAgentProvider` — replaced by `FoundryAgent`.
2 changes: 1 addition & 1 deletion python/packages/azure-ai-search/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The Azure AI Search integration provides context providers for RAG (Retrieval Au

### Basic Usage Example

See the [Azure AI Search context provider examples](../../samples/02-agents/providers/azure_ai/) which demonstrate:
See the [Azure AI Search context provider examples](../../samples/02-agents/context_providers/azure_ai_search/) which demonstrate:

- Semantic search with hybrid (vector + keyword) queries
- Agentic mode with Knowledge Bases for complex multi-hop reasoning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
from agent_framework import AGENT_FRAMEWORK_USER_AGENT, Annotation, Content, Message, SupportsGetEmbeddings
from agent_framework._sessions import AgentSession, BaseContextProvider, SessionContext
from agent_framework._settings import SecretString, load_settings
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
from azure.core.credentials import AzureKeyCredential
from azure.core.credentials import AzureKeyCredential, TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
from azure.core.exceptions import ResourceNotFoundError
from azure.search.documents.aio import SearchClient
Expand Down Expand Up @@ -111,6 +110,8 @@
except ImportError:
_agentic_retrieval_available = False

AzureCredentialTypes = TokenCredential | AsyncTokenCredential

logger = logging.getLogger("agent_framework.azure_ai_search")

_DEFAULT_AGENTIC_MESSAGE_HISTORY_COUNT = 10
Expand Down
35 changes: 34 additions & 1 deletion python/packages/azure-ai/agent_framework_azure_ai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,36 @@
from ._agent_provider import AzureAIAgentsProvider
from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions
from ._client import AzureAIClient, AzureAIProjectAgentOptions, RawAzureAIClient
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 ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider
from ._foundry_agent import FoundryAgent, RawFoundryAgent
from ._foundry_agent_client import RawFoundryAgentChatClient
from ._foundry_chat_client import FoundryChatClient, RawFoundryChatClient
from ._foundry_memory_provider import FoundryMemoryProvider
from ._project_provider import AzureAIProjectAgentProvider
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",
Expand All @@ -31,8 +47,25 @@
"AzureAIProjectAgentOptions",
"AzureAIProjectAgentProvider",
"AzureAISettings",
"AzureCredentialTypes",
"AzureOpenAIAssistantsClient",
"AzureOpenAIAssistantsOptions",
"AzureOpenAIChatClient",
"AzureOpenAIChatOptions",
"AzureOpenAIConfigMixin",
"AzureOpenAIEmbeddingClient",
"AzureOpenAIResponsesClient",
"AzureOpenAIResponsesOptions",
"AzureOpenAISettings",
"AzureTokenProvider",
"AzureUserSecurityContext",
"FoundryAgent",
"FoundryChatClient",
"FoundryMemoryProvider",
"RawAzureAIClient",
"RawAzureAIInferenceEmbeddingClient",
"RawFoundryAgent",
"RawFoundryAgentChatClient",
"RawFoundryChatClient",
"__version__",
]
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
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 ._entra_id_authentication import AzureCredentialTypes
from ._shared import AzureAISettings, to_azure_ai_agent_tools

if sys.version_info >= (3, 13):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
)
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,
Expand Down Expand Up @@ -92,6 +91,7 @@
)
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):
Expand Down
16 changes: 7 additions & 9 deletions python/packages/azure-ai/agent_framework_azure_ai/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@
)
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,
Expand All @@ -50,6 +49,7 @@
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):
Expand All @@ -68,7 +68,7 @@
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
Expand All @@ -88,7 +88,7 @@ class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False):
_DOC_INDEX_PATTERN = re.compile(r"doc_(\d+)")


class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[AzureAIClientOptionsT]):
class RawAzureAIClient(RawOpenAIChatClient[AzureAIClientOptionsT], Generic[AzureAIClientOptionsT]):
"""Raw Azure AI client without middleware, telemetry, or function invocation layers.

Warning:
Expand Down Expand Up @@ -215,8 +215,10 @@ class MyOptions(ChatOptions, total=False):
project_client = AIProjectClient(**project_client_kwargs)
should_close_client = True

# Initialize parent
# Initialize parent with OpenAI client from project
super().__init__(
async_client=project_client.get_openai_client(),
model=azure_ai_settings.get("model"), # type: ignore[arg-type]
additional_properties=additional_properties,
)

Expand Down Expand Up @@ -680,10 +682,6 @@ def _prepare_messages_for_azure_ai(self, messages: Sequence[Message]) -> tuple[l

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.

Expand Down
Loading
Loading