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:
Eduard van Valkenburg
2026-03-25 10:56:29 +01:00
committed by GitHub
Unverified
parent 4b533608b6
commit 5e056b672e
485 changed files with 9784 additions and 12084 deletions
@@ -12,9 +12,6 @@ This gallery helps Semantic Kernel (SK) developers move to the Microsoft Agent F
- [03_chat_completion_thread_and_stream.py](chat_completion/03_chat_completion_thread_and_stream.py) — Demonstrates session reuse and streaming prompts.
### Azure AI agent parity
- [01_basic_azure_ai_agent.py](azure_ai_agent/01_basic_azure_ai_agent.py) — Create and run an Azure AI agent end to end.
- [02_azure_ai_agent_with_code_interpreter.py](azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py) — Enable hosted code interpreter/tool execution.
- [03_azure_ai_agent_threads_and_followups.py](azure_ai_agent/03_azure_ai_agent_threads_and_followups.py) — Persist sessions and follow-ups across invocations.
### OpenAI Assistants API parity
- [01_basic_openai_assistant.py](openai_assistant/01_basic_openai_assistant.py) — Baseline assistant comparison.
@@ -1,65 +0,0 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "semantic-kernel",
# ]
# ///
# Run with any PEP 723 compatible runner, e.g.:
# uv run samples/semantic-kernel-migration/azure_ai_agent/01_basic_azure_ai_agent.py
# Copyright (c) Microsoft. All rights reserved.
"""Create an Azure AI agent using both Semantic Kernel and Agent Framework.
Prerequisites:
- Azure AI agent resource with a deployed model.
- Logged-in Azure CLI or other credential supported by AzureCliCredential.
"""
import asyncio
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def run_semantic_kernel() -> None:
from azure.identity.aio import AzureCliCredential
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings
async with AzureCliCredential() as credential, AzureAIAgent.create_client(credential=credential) as client:
settings = AzureAIAgentSettings() # Reads env vars for region/deployment.
# SK builds the remote agent definition then wraps it with AzureAIAgent.
definition = await client.agents.create_agent(
model=settings.model_deployment_name,
name="Support",
instructions="Answer customer questions in one paragraph.",
)
agent = AzureAIAgent(client=client, definition=definition)
response = await agent.get_response("How do I upgrade my plan?")
print("[SK]", response.message.content)
async def run_agent_framework() -> None:
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential
async with (
AzureCliCredential() as credential,
AzureAIAgentClient(credential=credential).as_agent(
name="Support",
instructions="Answer customer questions in one paragraph.",
) as agent,
):
# AF client returns an asynchronous context manager for remote agents.
reply = await agent.run("How do I upgrade my plan?")
print("[AF]", reply.text)
async def main() -> None:
await run_semantic_kernel()
await run_agent_framework()
if __name__ == "__main__":
asyncio.run(main())
@@ -1,76 +0,0 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "semantic-kernel",
# ]
# ///
# Run with any PEP 723 compatible runner, e.g.:
# uv run samples/semantic-kernel-migration/azure_ai_agent/02_azure_ai_agent_with_code_interpreter.py
# Copyright (c) Microsoft. All rights reserved.
"""Enable the hosted code interpreter for Azure AI agents in SK and AF.
The Azure AI service natively executes the code interpreter tool. Provide the
resource details via AzureAIAgentSettings (SK) or environment variables consumed
by AzureAIAgentClient (AF).
"""
import asyncio
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def run_semantic_kernel() -> None:
from azure.identity.aio import AzureCliCredential
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings
async with AzureCliCredential() as credential, AzureAIAgent.create_client(credential=credential) as client:
settings = AzureAIAgentSettings()
# Register the hosted code interpreter tool with the remote agent.
definition = await client.agents.create_agent(
model=settings.model_deployment_name,
name="Analyst",
instructions="Use the code interpreter for numeric work.",
tools=[{"type": "code_interpreter"}],
)
agent = AzureAIAgent(client=client, definition=definition)
response = await agent.get_response(
"Use Python to compute 42 ** 2 and explain the result.",
)
print("[SK]", response.message.content)
async def run_agent_framework() -> None:
from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider
from azure.identity.aio import AzureCliCredential
async with (
AzureCliCredential() as credential,
AzureAIAgentsProvider(credential=credential) as provider,
):
code_interpreter_tool = AzureAIAgentClient.get_code_interpreter_tool()
agent = await provider.create_agent(
name="Analyst",
instructions="Use the code interpreter for numeric work.",
tools=[code_interpreter_tool],
)
# Code interpreter tool mirrors the built-in Azure AI capability.
reply = await agent.run(
"Use Python to compute 42 ** 2 and explain the result.",
tool_choice="auto",
)
print("[AF]", reply.text)
async def main() -> None:
await run_semantic_kernel()
await run_agent_framework()
if __name__ == "__main__":
asyncio.run(main())
@@ -1,81 +0,0 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "semantic-kernel",
# ]
# ///
# Run with any PEP 723 compatible runner, e.g.:
# uv run samples/semantic-kernel-migration/azure_ai_agent/03_azure_ai_agent_threads_and_followups.py
# Copyright (c) Microsoft. All rights reserved.
"""Maintain Azure AI agent conversation state across turns in SK and AF."""
import asyncio
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def run_semantic_kernel() -> None:
from azure.identity.aio import AzureCliCredential
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentSettings, AzureAIAgentThread
async with AzureCliCredential() as credential, AzureAIAgent.create_client(credential=credential) as client:
settings = AzureAIAgentSettings()
definition = await client.agents.create_agent(
model=settings.model_deployment_name,
name="Planner",
instructions="Track follow-up questions within the same thread.",
)
agent = AzureAIAgent(client=client, definition=definition)
thread: AzureAIAgentThread | None = None
# SK returns the updated AzureAIAgentThread on each response.
first = await agent.get_response("Outline the onboarding checklist.", thread=thread)
thread = first.thread
print("[SK][turn1]", first.message.content)
second = await agent.get_response(
"Highlight the items that require legal review.",
thread=thread,
)
print("[SK][turn2]", second.message.content)
if thread is not None:
print("[SK][thread-id]", thread.id)
async def run_agent_framework() -> None:
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential
async with (
AzureCliCredential() as credential,
AzureAIAgentClient(credential=credential).as_agent(
name="Planner",
instructions="Track follow-up questions within the same thread.",
) as agent,
):
session = agent.create_session()
# AF sessions are explicit and can be serialized for external storage.
first = await agent.run("Outline the onboarding checklist.", session=session)
print("[AF][turn1]", first.text)
second = await agent.run(
"Highlight the items that require legal review.",
session=session,
)
print("[AF][turn2]", second.text)
serialized = session.to_dict()
print("[AF][session-json]", serialized)
async def main() -> None:
await run_semantic_kernel()
await run_agent_framework()
if __name__ == "__main__":
asyncio.run(main())
@@ -17,6 +17,7 @@ model of choice before running.
import asyncio
from agent_framework import Agent
from dotenv import load_dotenv
# Load environment variables from .env file
@@ -44,7 +45,8 @@ async def run_agent_framework() -> None:
from agent_framework.openai import OpenAIChatClient
# AF constructs a lightweight Agent backed by OpenAIChatClient.
chat_agent = OpenAIChatClient().as_agent(
chat_agent = Agent(
client=OpenAIChatClient(),
name="Support",
instructions="Answer in one sentence.",
)
@@ -48,7 +48,7 @@ async def run_semantic_kernel() -> None:
async def run_agent_framework() -> None:
from agent_framework import tool
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatClient
@tool(name="specials", description="List daily specials")
@@ -56,7 +56,8 @@ async def run_agent_framework() -> None:
return "Clam chowder, Cobb salad, Chai tea"
# AF tools are provided as callables on each agent instance.
chat_agent = OpenAIChatClient().as_agent(
chat_agent = Agent(
client=OpenAIChatClient(),
name="Host",
instructions="Answer menu questions accurately.",
tools=[specials],
@@ -16,6 +16,7 @@ for the second turn.
import asyncio
from agent_framework import Agent
from dotenv import load_dotenv
# Load environment variables from .env file
@@ -54,7 +55,8 @@ async def run_agent_framework() -> None:
from agent_framework.openai import OpenAIChatClient
# AF session objects are requested explicitly from the agent.
chat_agent = OpenAIChatClient().as_agent(
chat_agent = Agent(
client=OpenAIChatClient(),
name="Writer",
instructions="Keep answers short and friendly.",
)
@@ -9,21 +9,19 @@
# Copyright (c) Microsoft. All rights reserved.
"""Create an OpenAI Assistant using SK and Agent Framework."""
import asyncio
import os
from agent_framework import Agent
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
ASSISTANT_MODEL = os.environ.get("OPENAI_ASSISTANT_MODEL", "gpt-4o-mini")
async def run_semantic_kernel() -> None:
from semantic_kernel.agents import AssistantAgentThread, OpenAIAssistantAgent
client = OpenAIAssistantAgent.create_client()
# Provision the assistant on the OpenAI Assistants service.
definition = await client.beta.assistants.create(
@@ -32,7 +30,6 @@ async def run_semantic_kernel() -> None:
instructions="Answer questions in one concise paragraph.",
)
agent = OpenAIAssistantAgent(client=client, definition=definition)
thread: AssistantAgentThread | None = None
response = await agent.get_response("What is the capital of Denmark?", thread=thread)
thread = response.thread
@@ -43,13 +40,10 @@ async def run_semantic_kernel() -> None:
async def run_agent_framework() -> None:
from agent_framework.openai import OpenAIAssistantsClient
assistants_client = OpenAIAssistantsClient()
# AF wraps the assistant lifecycle with an async context manager.
async with assistants_client.as_agent(
name="Helper",
instructions="Answer questions in one concise paragraph.",
model=ASSISTANT_MODEL,
async with Agent(
client=assistants_client,
) as assistant_agent:
session = assistant_agent.create_session()
reply = await assistant_agent.run("What is the capital of Denmark?", session=session)
@@ -1,4 +1,5 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "semantic-kernel",
@@ -12,6 +13,7 @@
import asyncio
from agent_framework import Agent
from dotenv import load_dotenv
# Load environment variables from .env file
@@ -50,7 +52,7 @@ async def run_agent_framework() -> None:
code_interpreter_tool = OpenAIAssistantsClient.get_code_interpreter_tool()
# AF exposes the same tool configuration via create_agent.
async with assistants_client.as_agent(
async with Agent(client=assistants_client,
name="CodeRunner",
instructions="Use the code interpreter when calculations are required.",
model="gpt-4.1",
@@ -69,7 +69,7 @@ async def run_semantic_kernel() -> None:
async def run_agent_framework() -> None:
from agent_framework import tool
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIAssistantsClient
@tool(
@@ -81,7 +81,7 @@ async def run_agent_framework() -> None:
assistants_client = OpenAIAssistantsClient()
# AF converts the decorated function into an assistant-compatible tool.
async with assistants_client.as_agent(
async with Agent(client=assistants_client,
name="WeatherHelper",
instructions="Call get_forecast to fetch weather details.",
model=ASSISTANT_MODEL,
@@ -25,7 +25,7 @@ async def run_semantic_kernel() -> None:
client = OpenAIResponsesAgent.create_client()
# SK response agents wrap OpenAI's hosted Responses API.
agent = OpenAIResponsesAgent(
ai_model_id=OpenAISettings().responses_model_id,
ai_model=OpenAISettings().responses_model_id,
client=client,
instructions="Answer in one concise sentence.",
name="Expert",
@@ -31,7 +31,7 @@ async def run_semantic_kernel() -> None:
client = OpenAIResponsesAgent.create_client()
# Plugins advertise callable tools to the Responses agent.
agent = OpenAIResponsesAgent(
ai_model_id=OpenAISettings().responses_model_id,
ai_model=OpenAISettings().responses_model_id,
client=client,
instructions="Use the add tool when math is required.",
name="MathExpert",
@@ -32,7 +32,7 @@ async def run_semantic_kernel() -> None:
client = OpenAIResponsesAgent.create_client()
# response_format requests schema-constrained output from the model.
agent = OpenAIResponsesAgent(
ai_model_id=OpenAISettings().responses_model_id,
ai_model=OpenAISettings().responses_model_id,
client=client,
instructions="Return launch briefs as structured JSON.",
name="ProductMarketer",
@@ -15,7 +15,7 @@ import asyncio
from collections.abc import Sequence
from typing import cast
from agent_framework import Message
from agent_framework import Agent, Message
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
@@ -91,12 +91,12 @@ def _print_semantic_kernel_outputs(outputs: Sequence[ChatMessageContent]) -> Non
async def run_agent_framework_example(prompt: str) -> Sequence[list[Message]]:
client = AzureOpenAIChatClient(credential=AzureCliCredential())
physics = client.as_agent(
physics = Agent(client=client,
instructions=("You are an expert in physics. Answer questions from a physics perspective."),
name="physics",
)
chemistry = client.as_agent(
chemistry = Agent(client=client,
instructions=("You are an expert in chemistry. Answer questions from a chemistry perspective."),
name="chemistry",
)
@@ -17,7 +17,7 @@ from collections.abc import Sequence
from typing import Any, cast
from agent_framework import Agent, Message
from agent_framework.azure import AzureOpenAIChatClient, AzureOpenAIResponsesClient
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import GroupChatBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -130,8 +130,7 @@ class ChatCompletionGroupChatManager(GroupChatManager):
chat_history,
settings=PromptExecutionSettings(response_format=BooleanResult),
)
result = BooleanResult.model_validate_json(response.content)
return result
return BooleanResult.model_validate_json(response.content)
@override
async def select_next_agent(
@@ -235,19 +234,19 @@ async def run_agent_framework_example(task: str) -> str:
"Gather concise facts or considerations that help plan a community hackathon. "
"Keep your responses factual and scannable."
),
client=AzureOpenAIChatClient(credential=credential),
client=FoundryChatClient(credential=credential),
)
planner = Agent(
name="Planner",
description="Turns the collected notes into a concrete action plan.",
instructions=("Propose a structured action plan that accounts for logistics, roles, and timeline."),
client=AzureOpenAIResponsesClient(credential=credential),
client=FoundryChatClient(credential=credential),
)
workflow = GroupChatBuilder(
participants=[researcher, planner],
orchestrator_agent=AzureOpenAIChatClient(credential=credential).as_agent(),
orchestrator_agent=Agent(client=FoundryChatClient(credential=credential)),
).build()
final_response = ""
@@ -13,17 +13,18 @@
import asyncio
import sys
from collections.abc import AsyncIterable, Iterator, Sequence
from typing import cast
from agent_framework import (
Agent,
Message,
WorkflowEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from semantic_kernel.agents import Agent, ChatCompletionAgent, HandoffOrchestration, OrchestrationHandoffs
from semantic_kernel.agents import Agent as SKAgent
from semantic_kernel.agents import ChatCompletionAgent, HandoffOrchestration, OrchestrationHandoffs
from semantic_kernel.agents.runtime import InProcessRuntime
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.contents import (
@@ -74,7 +75,7 @@ class OrderReturnPlugin:
return f"Return for order {order_id} has been processed successfully (reason: {reason})."
def build_semantic_kernel_agents() -> tuple[list[Agent], OrchestrationHandoffs]:
def build_semantic_kernel_agents() -> tuple[list[SKAgent], OrchestrationHandoffs]:
credential = AzureCliCredential()
triage = ChatCompletionAgent(
@@ -189,8 +190,9 @@ async def run_semantic_kernel_example(initial_task: str, scripted_responses: Seq
######################################################################
def _create_af_agents(client: AzureOpenAIChatClient):
triage = client.as_agent(
def _create_af_agents(client: FoundryChatClient):
triage = Agent(
client=client,
name="triage_agent",
instructions=(
"You are a customer support triage agent. Route requests:\n"
@@ -199,19 +201,22 @@ def _create_af_agents(client: AzureOpenAIChatClient):
"- handoff_to_order_return_agent for returns"
),
)
refund = client.as_agent(
refund = Agent(
client=client,
name="refund_agent",
instructions=(
"Handle refunds. Ask for order id and reason. If shipping info is needed, hand off to order_status_agent."
),
)
status = client.as_agent(
status = Agent(
client=client,
name="order_status_agent",
instructions=(
"Provide order status, tracking, and timelines. If billing questions appear, hand off to refund_agent."
),
)
returns = client.as_agent(
returns = Agent(
client=client,
name="order_return_agent",
instructions=(
"Coordinate returns, confirm addresses, and summarize next steps. Hand off to triage_agent if unsure."
@@ -235,13 +240,12 @@ def _collect_handoff_requests(events: list[WorkflowEvent]) -> list[WorkflowEvent
def _extract_final_conversation(events: list[WorkflowEvent]) -> list[Message]:
for event in events:
if event.type == "output":
data = cast(list[Message], event.data)
return data
return event.data
return []
async def run_agent_framework_example(initial_task: str, scripted_responses: Sequence[str]) -> str:
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = FoundryChatClient(credential=AzureCliCredential())
triage, refund, status, returns = _create_af_agents(client)
workflow = (
@@ -137,7 +137,7 @@ async def run_agent_framework_example(prompt: str) -> str | None:
instructions=(
"You are a Researcher. You find information without additional computation or quantitative analysis."
),
client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
client=OpenAIChatClient(model="gpt-4o-search-preview"),
)
# Create code interpreter tool using static method
@@ -15,12 +15,13 @@ import asyncio
from collections.abc import Sequence
from typing import cast
from agent_framework import Message
from agent_framework import Agent, Message
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from semantic_kernel.agents import Agent, ChatCompletionAgent, SequentialOrchestration
from semantic_kernel.agents import Agent as SKAgent
from semantic_kernel.agents import ChatCompletionAgent, SequentialOrchestration
from semantic_kernel.agents.runtime import InProcessRuntime
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.contents import ChatMessageContent
@@ -36,7 +37,7 @@ PROMPT = "Write a tagline for a budget-friendly eBike."
######################################################################
def build_semantic_kernel_agents() -> list[Agent]:
def build_semantic_kernel_agents() -> list[SKAgent]:
credential = AzureCliCredential()
writer_agent = ChatCompletionAgent(
@@ -77,12 +78,12 @@ async def sk_agent_response_callback(
async def run_agent_framework_example(prompt: str) -> list[Message]:
client = AzureOpenAIChatClient(credential=AzureCliCredential())
writer = client.as_agent(
writer = Agent(client=client,
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
name="writer",
)
reviewer = client.as_agent(
reviewer = Agent(client=client,
instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."),
name="reviewer",
)