Python: Fix tool normalization and provider sample consolidation (#3953)

* Fix tool normalization and provider samples

- restore callable/single-tool normalization paths and unset tool-choice behavior\n- consolidate and expand chat/provider samples (OpenAI/Azure/Anthropic/Ollama/Bedrock)\n- migrate Bedrock lazy import surface to agent_framework.amazon and move provider samples

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

* small fix in sample

* Finalize provider, samples, and core cleanup

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

* Fix CopilotTool passthrough in agent

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

* fix link

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-02-16 16:30:38 +00:00
committed by GitHub
co-authored by Copilot
parent ed113f941c
commit aab621f5eb
99 changed files with 1190 additions and 969 deletions
@@ -0,0 +1,19 @@
# Provider Samples Overview
This directory groups provider-specific samples for Agent Framework.
| Folder | What you will find |
| --- | --- |
| [`anthropic/`](anthropic/) | Anthropic Claude samples using both `AnthropicClient` and `ClaudeAgent`, including tools, MCP, sessions, and Foundry Anthropic integration. |
| [`amazon/`](amazon/) | AWS Bedrock samples using `BedrockChatClient`, including tool-enabled agent usage. |
| [`azure_ai/`](azure_ai/) | Azure AI Foundry V2 (`azure-ai-projects`) samples with `AzureAIClient`, from basic setup to advanced patterns like search, memory, A2A, MCP, and provider methods. |
| [`azure_ai_agent/`](azure_ai_agent/) | Azure AI Foundry V1 (`azure-ai-agents`) samples with `AzureAIAgentsProvider`, including provider methods and common hosted tool integrations. |
| [`azure_openai/`](azure_openai/) | Azure OpenAI samples for Assistants, Chat, and Responses clients, with examples for sessions, tools, MCP, file search, and code interpreter. |
| [`copilotstudio/`](copilotstudio/) | Microsoft Copilot Studio agent samples, including required environment/app registration setup and explicit authentication patterns. |
| [`custom/`](custom/) | Framework extensibility samples for building custom `BaseAgent` and `BaseChatClient` implementations, including layer-composition guidance. |
| [`foundry_local/`](foundry_local/) | Foundry Local samples using `FoundryLocalClient` for local model inference with streaming, non-streaming, and tool-calling patterns. |
| [`github_copilot/`](github_copilot/) | `GitHubCopilotAgent` samples showing basic usage, session handling, permission-scoped shell/file/url access, and MCP integration. |
| [`ollama/`](ollama/) | Local Ollama samples using `OllamaChatClient` (recommended) plus OpenAI-compatible Ollama setup, including reasoning and multimodal examples. |
| [`openai/`](openai/) | OpenAI provider samples for Assistants, Chat, and Responses clients, including tools, structured output, sessions, MCP, web search, and multimodal tasks. |
Each folder has its own README with setup requirements and file-by-file details.
@@ -0,0 +1,17 @@
# Bedrock Examples
This folder contains examples demonstrating how to use AWS Bedrock models with the Agent Framework. The sample
uses `BEDROCK_CHAT_MODEL_ID`, `BEDROCK_REGION`, and AWS credentials (`AWS_ACCESS_KEY_ID`,
`AWS_SECRET_ACCESS_KEY`, optional `AWS_SESSION_TOKEN`).
## Examples
| File | Description |
|------|-------------|
| [`bedrock_chat_client.py`](bedrock_chat_client.py) | Uses `BedrockChatClient` with a simple tool-enabled `Agent` to demonstrate direct Bedrock chat integration. |
## Environment Variables
- `BEDROCK_CHAT_MODEL_ID`: Bedrock model ID (for example, `anthropic.claude-3-5-sonnet-20240620-v1:0`)
- `BEDROCK_REGION`: AWS region (defaults to `us-east-1` if unset)
- AWS credentials via standard variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, optional `AWS_SESSION_TOKEN`)
@@ -0,0 +1,61 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.amazon import BedrockChatClient
from pydantic import Field
"""
Bedrock Chat Client Example
This sample demonstrates using `BedrockChatClient` with an agent and a simple tool.
Environment variables used:
- `BEDROCK_CHAT_MODEL_ID`
- `BEDROCK_REGION` (defaults to `us-east-1` if unset)
- AWS credentials via standard variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`,
optional `AWS_SESSION_TOKEN`)
"""
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_weather(
city: Annotated[str, Field(description="The city to get the weather for.")],
) -> dict[str, str]:
"""Return a mock forecast for the requested city."""
normalized_city = city.strip() or "New York"
return {"city": normalized_city, "forecast": "72F and sunny"}
async def main() -> None:
"""Run a Bedrock-backed agent with one tool call."""
# 1. Create an agent with Bedrock chat client and one tool.
agent = Agent(
client=BedrockChatClient(),
instructions="You are a concise travel assistant.",
name="BedrockWeatherAgent",
tool_choice="auto",
tools=[get_weather],
)
# 2. Run a query that uses the weather tool.
query = "Use the weather tool to check the forecast for New York."
print(f"User: {query}")
response = await agent.run(query)
print(f"Assistant: {response.text}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
User: Use the weather tool to check the forecast for New York.
Assistant: The forecast for New York is 72F and sunny.
"""
@@ -19,7 +19,7 @@ import asyncio
from typing import Annotated
from agent_framework import tool
from agent_framework_claude import ClaudeAgent
from agent_framework.anthropic import ClaudeAgent
@tool
@@ -19,7 +19,7 @@ servers you trust. Use permission handlers to control what actions are allowed.
import asyncio
from typing import Any
from agent_framework_claude import ClaudeAgent
from agent_framework.anthropic import ClaudeAgent
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
@@ -22,7 +22,7 @@ More permissions mean more potential for unintended actions.
import asyncio
from typing import Any
from agent_framework_claude import ClaudeAgent
from agent_framework.anthropic import ClaudeAgent
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
@@ -13,7 +13,7 @@ from random import randint
from typing import Annotated
from agent_framework import tool
from agent_framework_claude import ClaudeAgent
from agent_framework.anthropic import ClaudeAgent
from pydantic import Field
@@ -14,7 +14,7 @@ Shell commands have full access to your system within the permissions of the run
import asyncio
from typing import Any
from agent_framework_claude import ClaudeAgent
from agent_framework.anthropic import ClaudeAgent
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
@@ -17,7 +17,7 @@ Available built-in tools:
import asyncio
from agent_framework_claude import ClaudeAgent
from agent_framework.anthropic import ClaudeAgent
async def main() -> None:
@@ -16,7 +16,7 @@ URL fetching allows the agent to access any URL accessible from your network.
import asyncio
from agent_framework_claude import ClaudeAgent
from agent_framework.anthropic import ClaudeAgent
async def main() -> None:
@@ -0,0 +1,22 @@
# Foundry Local Examples
This folder contains examples demonstrating how to run local models with `FoundryLocalClient` via `agent_framework.microsoft`.
## Prerequisites
1. Install Foundry Local and required local runtime components.
2. Install the connector package:
```bash
pip install agent-framework-foundry-local --pre
```
## Examples
| File | Description |
|------|-------------|
| [`foundry_local_agent.py`](foundry_local_agent.py) | Basic Foundry Local agent usage with streaming and non-streaming responses, plus function tool calling. |
## Environment Variables
- `FOUNDRY_LOCAL_MODEL_ID`: Optional model alias/ID to use by default when `model_id` is not passed to `FoundryLocalClient`.
@@ -0,0 +1,80 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa
from __future__ import annotations
import asyncio
from random import randint
from typing import TYPE_CHECKING, Annotated
from agent_framework.microsoft import FoundryLocalClient
if TYPE_CHECKING:
from agent_framework import Agent
"""
This sample demonstrates basic usage of the FoundryLocalClient.
Shows both streaming and non-streaming responses with function tools.
Running this sample the first time will be slow, as the model needs to be
downloaded and initialized.
Also, not every model supports function calling, so be sure to check the
model capabilities in the Foundry catalog, or pick one from the list printed
when running this sample.
"""
def get_weather(
location: Annotated[str, "The location to get the weather for."],
) -> str:
"""Get the weather for a given location."""
conditions = ["sunny", "cloudy", "rainy", "stormy"]
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
async def non_streaming_example(agent: Agent) -> None:
"""Example of non-streaming response (get the complete result at once)."""
print("=== Non-streaming Response Example ===")
query = "What's the weather like in Seattle?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
async def streaming_example(agent: Agent) -> None:
"""Example of streaming response (get results as they are generated)."""
print("=== Streaming Response Example ===")
query = "What's the weather like in Amsterdam?"
print(f"User: {query}")
print("Agent: ", end="", flush=True)
async for chunk in agent.run(query, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
print("\n")
async def main() -> None:
print("=== Basic Foundry Local Client Agent Example ===")
client = FoundryLocalClient(model_id="phi-4-mini")
print(f"Client Model ID: {client.model_id}\n")
print("Other available models (tool calling supported only):")
for model in client.manager.list_catalog_models():
if model.supports_tool_calling:
print(
f"- {model.alias} for {model.task} - id={model.id} - {(model.file_size_mb / 1000):.2f} GB - {model.license}"
)
agent = client.as_agent(
name="LocalAgent",
instructions="You are a helpful agent.",
tools=get_weather,
)
await non_streaming_example(agent)
await streaming_example(agent)
if __name__ == "__main__":
asyncio.run(main())
@@ -6,7 +6,6 @@ import tempfile
import urllib.request as urllib_request
from pathlib import Path
import aiofiles # pyright: ignore[reportMissingModuleSource]
from agent_framework import Content
from agent_framework.openai import OpenAIResponsesClient
@@ -20,8 +19,11 @@ and automated visual asset generation.
"""
async def save_image(output: Content) -> None:
"""Save the generated image to a temporary directory."""
def save_image(output: Content) -> None:
"""Save the generated image to a temporary directory.
This sample is simplified, usually a async aware storing method would be better.
"""
filename = "generated_image.webp"
file_path = Path(tempfile.gettempdir()) / filename
@@ -37,15 +39,15 @@ async def save_image(output: Content) -> None:
data_bytes = None
else:
try:
data_bytes = await asyncio.to_thread(lambda: urllib_request.urlopen(uri).read())
data_bytes = urllib_request.urlopen(uri).read()
except Exception:
data_bytes = None
if data_bytes is None:
raise RuntimeError("Image output present but could not retrieve bytes.")
async with aiofiles.open(file_path, "wb") as f:
await f.write(data_bytes)
with open(file_path, "wb") as f:
f.write(data_bytes)
print(f"Image downloaded and saved to: {file_path}")
@@ -76,15 +78,15 @@ async def main() -> None:
image_saved = False
for message in result.messages:
for content in message.contents:
if content.type == "image_generation_tool_result_tool_result" and content.outputs:
if content.type == "image_generation_tool_result" and content.outputs:
output = content.outputs
if isinstance(output, Content) and output.uri:
await save_image(output)
save_image(output)
image_saved = True
elif isinstance(output, list):
for out in output:
if isinstance(out, Content) and out.uri:
await save_image(out)
save_image(out)
image_saved = True
break
if image_saved: