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 17:30:38 +01:00
committed by GitHub
Unverified
parent ed113f941c
commit aab621f5eb
99 changed files with 1190 additions and 969 deletions
+15
View File
@@ -10,6 +10,21 @@ We use [ruff](https://github.com/astral-sh/ruff) for both linting and formatting
- **Target Python version**: 3.10+
- **Google-style docstrings**: All public functions, classes, and modules should have docstrings following Google conventions
### Module Docstrings
Public modules must include a module-level docstring, including `__init__.py` files.
- Namespace-style `__init__.py` modules (for example under `agent_framework/<provider>/`) should use a structured
docstring that includes:
- A one-line summary of the namespace
- A short "This module lazily re-exports objects from:" section that lists only pip install package names
(for example `agent-framework-a2a`)
- A short "Supported classes:" (or "Supported classes and functions:") section
- The main `agent_framework/__init__.py` should include a concise background-oriented docstring rather than a long
per-symbol list.
- Core modules with broad surface area, including `agent_framework/exceptions.py` and
`agent_framework/observability.py`, should always have explicit module docstrings.
## Type Annotations
### Future Annotations
@@ -720,6 +720,8 @@ class AnthropicClient(
if options.get("tool_choice") is None:
return result or None
tool_mode = validate_tool_mode(options.get("tool_choice"))
if tool_mode is None:
return result or None
allow_multiple = options.get("allow_multiple_tool_calls")
match tool_mode.get("mode"):
case "auto":
@@ -16,6 +16,7 @@ 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.exceptions import ServiceInitializationError
from azure.ai.agents.aio import AgentsClient
from azure.ai.agents.models import Agent as AzureAgent
@@ -169,11 +170,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
model: str | None = None,
instructions: str | None = None,
description: str | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
@@ -242,7 +239,12 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
normalized_tools = normalize_tools(tools)
if normalized_tools:
# Only convert non-MCP tools to Azure AI format
non_mcp_tools = [t for t in normalized_tools if not isinstance(t, MCPTool)]
non_mcp_tools: list[FunctionTool | MutableMapping[str, Any]] = []
for normalized_tool in normalized_tools:
if isinstance(normalized_tool, MCPTool):
continue
if isinstance(normalized_tool, (FunctionTool, MutableMapping)):
non_mcp_tools.append(normalized_tool)
if non_mcp_tools:
# Pass run_options to capture tool_resources (e.g., for file search vector stores)
run_options: dict[str, Any] = {}
@@ -266,11 +268,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
self,
id: str,
*,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
@@ -322,11 +320,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
def as_agent(
self,
agent: AzureAgent,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
@@ -379,7 +373,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
def _to_chat_agent_from_agent(
self,
agent: AzureAgent,
provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None = None,
provided_tools: Sequence[ToolTypes] | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
@@ -422,8 +416,8 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
def _merge_tools(
self,
agent_tools: Sequence[Any] | None,
provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None,
) -> list[FunctionTool | dict[str, Any]]:
provided_tools: Sequence[ToolTypes] | None,
) -> list[ToolTypes]:
"""Merge hosted tools from agent with user-provided function tools.
Args:
@@ -433,7 +427,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
Returns:
Combined list of tools for the Agent.
"""
merged: list[FunctionTool | dict[str, Any]] = []
merged: list[ToolTypes] = []
# Convert hosted tools from agent definition
hosted_tools = from_azure_ai_agent_tools(agent_tools)
@@ -459,7 +453,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
def _validate_function_tools(
self,
agent_tools: Sequence[Any] | None,
provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None,
provided_tools: Sequence[ToolTypes] | None,
) -> None:
"""Validate that required function tools are provided.
@@ -34,6 +34,7 @@ from agent_framework import (
UsageDetails,
)
from agent_framework._settings import load_settings
from agent_framework._tools import ToolTypes
from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError, ServiceResponseException
from agent_framework.observability import ChatTelemetryLayer
from azure.ai.agents.aio import AgentsClient
@@ -1428,11 +1429,7 @@ class AzureAIAgentClient(
name: str | None = None,
description: str | None = None,
instructions: str | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: AzureAIAgentOptionsT | Mapping[str, Any] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
@@ -5,7 +5,7 @@ from __future__ import annotations
import json
import logging
import sys
from collections.abc import Callable, Mapping, MutableMapping, Sequence
from collections.abc import Callable, Mapping, Sequence
from contextlib import suppress
from typing import Any, ClassVar, Generic, Literal, TypedDict, TypeVar, cast
@@ -22,6 +22,7 @@ from agent_framework import (
MiddlewareTypes,
)
from agent_framework._settings import load_settings
from agent_framework._tools import ToolTypes
from agent_framework.exceptions import ServiceInitializationError
from agent_framework.observability import ChatTelemetryLayer
from agent_framework.openai import OpenAIResponsesOptions
@@ -880,11 +881,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
name: str | None = None,
description: str | None = None,
instructions: str | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: AzureAIClientOptionsT | Mapping[str, Any] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
@@ -17,6 +17,7 @@ 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.exceptions import ServiceInitializationError
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import (
@@ -161,11 +162,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
model: str | None = None,
instructions: str | None = None,
description: str | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
@@ -226,7 +223,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
for tool in normalized_tools:
if isinstance(tool, MCPTool):
mcp_tools.append(tool)
else:
elif isinstance(tool, (FunctionTool, MutableMapping)):
non_mcp_tools.append(tool)
# Connect MCP tools and discover their functions BEFORE creating the agent
@@ -263,11 +260,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
*,
name: str | None = None,
reference: AgentReference | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
@@ -323,11 +316,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
def as_agent(
self,
details: AgentVersionDetails,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
@@ -367,7 +356,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
def _to_chat_agent_from_details(
self,
details: AgentVersionDetails,
provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None = None,
provided_tools: Sequence[ToolTypes] | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
@@ -415,8 +404,8 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
def _merge_tools(
self,
definition_tools: Sequence[Any] | None,
provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None,
) -> list[FunctionTool | dict[str, Any]]:
provided_tools: Sequence[ToolTypes] | None,
) -> list[ToolTypes]:
"""Merge hosted tools from definition with user-provided function tools.
Args:
@@ -426,7 +415,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
Returns:
Combined list of tools for the Agent.
"""
merged: list[FunctionTool | dict[str, Any]] = []
merged: list[ToolTypes] = []
# Convert hosted tools from definition (MCP, code interpreter, file search, web search)
# Function tools from the definition are skipped - we use user-provided implementations instead
@@ -450,11 +439,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
def _validate_function_tools(
self,
agent_tools: Sequence[Any] | None,
provided_tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None,
provided_tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
) -> None:
"""Validate that required function tools are provided."""
# Normalize and validate function tools
+2 -2
View File
@@ -12,7 +12,7 @@ Integration with AWS Bedrock for LLM inference.
## Usage
```python
from agent_framework_bedrock import BedrockChatClient
from agent_framework.amazon import BedrockChatClient
client = BedrockChatClient(model_id="anthropic.claude-3-sonnet-20240229-v1:0")
response = await client.get_response("Hello")
@@ -21,5 +21,5 @@ response = await client.get_response("Hello")
## Import Path
```python
from agent_framework_bedrock import BedrockChatClient
from agent_framework.amazon import BedrockChatClient
```
+1 -1
View File
@@ -12,7 +12,7 @@ The Bedrock integration enables Microsoft Agent Framework applications to call A
### Basic Usage Example
See the [Bedrock sample script](samples/bedrock_sample.py) for a runnable end-to-end script that:
See the [Bedrock sample](../../samples/02-agents/providers/amazon/bedrock_chat_client.py) for a runnable end-to-end script that:
- Loads credentials from the `BEDROCK_*` environment variables
- Instantiates `BedrockChatClient`
@@ -260,7 +260,7 @@ class BedrockChatClient(
Examples:
.. code-block:: python
from agent_framework.bedrock import BedrockChatClient
from agent_framework.amazon import BedrockChatClient
# Basic usage with default credentials
client = BedrockChatClient(model_id="<model name>")
@@ -1,45 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
from agent_framework import Agent, tool
from agent_framework_bedrock import BedrockChatClient
@tool(approval_mode="never_require")
def get_weather(city: str) -> dict[str, str]:
"""Return a mock forecast for the requested city."""
normalized = city.strip() or "New York"
return {"city": normalized, "forecast": "72F and sunny"}
async def main() -> None:
"""Run the Bedrock sample agent, invoke the weather tool, and log the response."""
agent = Agent(
client=BedrockChatClient(),
instructions="You are a concise travel assistant.",
name="BedrockWeatherAgent",
tool_choice="auto",
tools=[get_weather],
)
response = await agent.run("Use the weather tool to check the forecast for new york.")
logging.info("\nAssistant reply:", response.text or "<no text returned>")
logging.info("\nConversation transcript:")
for message in response.messages:
for idx, content in enumerate(message.contents, start=1):
match content.type:
case "text":
logging.info(f" {idx}. text -> {content.text}")
case "function_call":
logging.info(f" {idx}. function_call ({content.name}) -> {content.arguments}")
case "function_result":
logging.info(f" {idx}. function_result ({content.call_id}) -> {content.result}")
case _:
logging.info(f" {idx}. {content.type}")
if __name__ == "__main__":
asyncio.run(main())
@@ -23,6 +23,7 @@ from agent_framework import (
normalize_messages,
)
from agent_framework._settings import load_settings
from agent_framework._tools import ToolTypes
from agent_framework._types import AgentRunInputs, normalize_tools
from agent_framework.exceptions import ServiceException
from claude_agent_sdk import (
@@ -217,12 +218,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
description: str | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| str
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | str]
| None = None,
tools: ToolTypes | Callable[..., Any] | str | Sequence[ToolTypes | Callable[..., Any] | str] | None = None,
default_options: OptionsT | MutableMapping[str, Any] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
@@ -289,7 +285,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
# Separate built-in tools (strings) from custom tools (callables/FunctionTool)
self._builtin_tools: list[str] = []
self._custom_tools: list[FunctionTool | MutableMapping[str, Any]] = []
self._custom_tools: list[ToolTypes] = []
self._normalize_tools(tools)
self._default_options = opts
@@ -298,12 +294,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
def _normalize_tools(
self,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| str
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | str]
| None,
tools: ToolTypes | Callable[..., Any] | str | Sequence[ToolTypes | Callable[..., Any] | str] | None,
) -> None:
"""Separate built-in tools (strings) from custom tools.
@@ -316,10 +307,10 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
# Normalize to sequence
if isinstance(tools, str):
tools_list: Sequence[Any] = [tools]
elif isinstance(tools, (FunctionTool, MutableMapping)) or callable(tools):
tools_list = [tools]
else:
elif isinstance(tools, Sequence):
tools_list = list(tools)
else:
tools_list = [tools]
for tool in tools_list:
if isinstance(tool, str):
@@ -457,7 +448,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
def _prepare_tools(
self,
tools: list[FunctionTool | MutableMapping[str, Any]],
tools: Sequence[ToolTypes],
) -> tuple[Any, list[str]]:
"""Convert Agent Framework tools to SDK MCP server.
@@ -1,5 +1,13 @@
# Copyright (c) Microsoft. All rights reserved.
"""Public API surface for Agent Framework core.
This module exposes the primary abstractions for agents, chat clients, tools, sessions,
middleware, observability, and workflows. Connector namespaces such as
``agent_framework.azure`` and ``agent_framework.anthropic`` provide provider-specific
integrations, many of which are lazy-loaded from optional packages.
"""
import importlib.metadata
from typing import Final
+12 -57
View File
@@ -37,6 +37,8 @@ from ._sessions import AgentSession, BaseContextProvider, BaseHistoryProvider, I
from ._tools import (
FunctionInvocationLayer,
FunctionTool,
ToolTypes,
normalize_tools,
)
from ._types import (
AgentResponse,
@@ -614,12 +616,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
id: str | None = None,
name: str | None = None,
description: str | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Any
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
**kwargs: Any,
@@ -665,24 +662,14 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
# Get tools from options or named parameter (named param takes precedence)
tools_ = tools if tools is not None else opts.pop("tools", None)
tools_ = cast(
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None,
tools_,
)
# Handle instructions - named parameter takes precedence over options
instructions_ = instructions if instructions is not None else opts.pop("instructions", None)
# We ignore the MCP Servers here and store them separately,
# we add their functions to the tools list at runtime
normalized_tools: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] = ( # type:ignore[reportUnknownVariableType]
[] if tools_ is None else tools_ if isinstance(tools_, list) else [tools_] # type: ignore[list-item]
)
self.mcp_tools: list[MCPTool] = [tool for tool in normalized_tools if isinstance(tool, MCPTool)] # type: ignore[misc]
normalized_tools = normalize_tools(tools_)
self.mcp_tools: list[MCPTool] = [tool for tool in normalized_tools if isinstance(tool, MCPTool)]
agent_tools = [tool for tool in normalized_tools if not isinstance(tool, MCPTool)]
# Build chat options dict
@@ -765,12 +752,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
*,
stream: Literal[False] = ...,
session: AgentSession | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Any
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: ChatOptions[ResponseModelBoundT],
**kwargs: Any,
) -> Awaitable[AgentResponse[ResponseModelBoundT]]: ...
@@ -782,12 +764,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
*,
stream: Literal[False] = ...,
session: AgentSession | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Any
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: OptionsCoT | ChatOptions[None] | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]]: ...
@@ -799,12 +776,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
*,
stream: Literal[True],
session: AgentSession | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Any
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: OptionsCoT | ChatOptions[Any] | None = None,
**kwargs: Any,
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
@@ -815,12 +787,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
*,
stream: bool = False,
session: AgentSession | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Any
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
options: OptionsCoT | ChatOptions[Any] | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
@@ -1000,12 +967,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
*,
messages: AgentRunInputs | None,
session: AgentSession | None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Any
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
options: Mapping[str, Any] | None,
kwargs: dict[str, Any],
) -> _RunContext:
@@ -1035,9 +997,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
)
# Normalize tools
normalized_tools: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] = (
[] if tools_ is None else tools_ if isinstance(tools_, list) else [tools_]
)
normalized_tools = normalize_tools(tools_)
agent_name = self._get_agent_name()
# Resolve final tool list (runtime provided tools + local MCP server tools)
@@ -1343,12 +1303,7 @@ class Agent(
id: str | None = None,
name: str | None = None,
description: str | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Any
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
@@ -10,7 +10,6 @@ from collections.abc import (
Awaitable,
Callable,
Mapping,
MutableMapping,
Sequence,
)
from typing import (
@@ -31,7 +30,7 @@ from pydantic import BaseModel
from ._serialization import SerializationMixin
from ._tools import (
FunctionInvocationConfiguration,
FunctionTool,
ToolTypes,
)
from ._types import (
ChatResponse,
@@ -436,11 +435,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
name: str | None = None,
description: str | None = None,
instructions: str | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsCoT | Mapping[str, Any] | None = None,
context_providers: Sequence[Any] | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
+62 -46
View File
@@ -12,7 +12,6 @@ from collections.abc import (
Awaitable,
Callable,
Mapping,
MutableMapping,
Sequence,
)
from functools import partial, wraps
@@ -25,6 +24,7 @@ from typing import (
Final,
Generic,
Literal,
TypeAlias,
TypedDict,
Union,
get_args,
@@ -58,6 +58,7 @@ else:
if TYPE_CHECKING:
from ._clients import SupportsChatGetResponse
from ._mcp import MCPTool
from ._middleware import FunctionMiddlewarePipeline, FunctionMiddlewareTypes
from ._types import (
ChatOptions,
@@ -69,6 +70,8 @@ if TYPE_CHECKING:
)
ResponseModelBoundT = TypeVar("ResponseModelBoundT", bound=BaseModel)
else:
MCPTool = Any # type: ignore[assignment,misc]
logger = logging.getLogger("agent_framework")
@@ -506,9 +509,7 @@ class FunctionTool(SerializationMixin):
if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED: # type: ignore[name-defined]
attributes.update({
OtelAttr.TOOL_ARGUMENTS: (
json.dumps(serializable_kwargs, default=str, ensure_ascii=False)
if serializable_kwargs
else "None"
json.dumps(serializable_kwargs, default=str, ensure_ascii=False) if serializable_kwargs else "None"
)
})
with get_function_span(attributes=attributes) as span:
@@ -623,14 +624,46 @@ class FunctionTool(SerializationMixin):
return as_dict
ToolTypes: TypeAlias = FunctionTool | MCPTool | Mapping[str, Any] | Any
def normalize_tools(
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
) -> list[ToolTypes]:
"""Normalize tool inputs while preserving non-callable tool objects.
Args:
tools: A single tool or sequence of tools.
Returns:
A normalized list where callable inputs are converted to ``FunctionTool``
using :func:`tool`, and existing tool objects are passed through unchanged.
"""
if not tools:
return []
tool_items = (
list(tools)
if isinstance(tools, Sequence) and not isinstance(tools, (str, bytes, bytearray, Mapping))
else [tools]
)
from ._mcp import MCPTool
normalized: list[ToolTypes] = []
for tool_item in tool_items:
# check known types, these are also callable, so we need to do that first
if isinstance(tool_item, (FunctionTool, Mapping, MCPTool)):
normalized.append(tool_item)
continue
if callable(tool_item):
normalized.append(tool(tool_item))
continue
normalized.append(tool_item)
return normalized
def _tools_to_dict(
tools: (
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None
),
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
) -> list[str | dict[str, Any]] | None:
"""Parse the tools to a dict.
@@ -640,32 +673,20 @@ def _tools_to_dict(
Returns:
A list of tool specifications as dictionaries, or None if no tools provided.
"""
if not tools:
return None
if not isinstance(tools, list):
if isinstance(tools, FunctionTool):
return [tools.to_json_schema_spec()]
if isinstance(tools, SerializationMixin):
return [tools.to_dict()]
if isinstance(tools, dict):
return [tools]
if callable(tools):
return [tool(tools).to_json_schema_spec()]
logger.warning("Can't parse tool.")
normalized_tools = normalize_tools(tools)
if not normalized_tools:
return None
results: list[str | dict[str, Any]] = []
for tool_item in tools:
for tool_item in normalized_tools:
if isinstance(tool_item, FunctionTool):
results.append(tool_item.to_json_schema_spec())
continue
if isinstance(tool_item, SerializationMixin):
results.append(tool_item.to_dict())
continue
if isinstance(tool_item, dict):
results.append(tool_item)
continue
if callable(tool_item):
results.append(tool(tool_item).to_json_schema_spec())
if isinstance(tool_item, Mapping):
results.append(dict(tool_item))
continue
logger.warning("Can't parse tool.")
return results
@@ -1430,20 +1451,12 @@ async def _auto_invoke_function(
def _get_tool_map(
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]],
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]],
) -> dict[str, FunctionTool]:
tool_list: dict[str, FunctionTool] = {}
for tool_item in tools if isinstance(tools, list) else [tools]:
for tool_item in normalize_tools(tools):
if isinstance(tool_item, FunctionTool):
tool_list[tool_item.name] = tool_item
continue
if callable(tool_item):
# Convert to AITool if it's a function or callable
ai_tool = tool(tool_item)
tool_list[ai_tool.name] = ai_tool
return tool_list
@@ -1451,10 +1464,7 @@ async def _try_execute_function_calls(
custom_args: dict[str, Any],
attempt_idx: int,
function_calls: Sequence[Content],
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]],
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]],
config: FunctionInvocationConfiguration,
middleware_pipeline: Any = None, # Optional MiddlewarePipeline to avoid circular imports
) -> tuple[Sequence[Content], bool]:
@@ -1633,15 +1643,16 @@ async def _ensure_response_stream(
return stream
def _extract_tools(options: dict[str, Any] | None) -> Any:
def _extract_tools(
options: dict[str, Any] | None,
) -> ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None:
"""Extract tools from options dict.
Args:
options: The options dict containing chat options.
Returns:
FunctionTool | Callable[..., Any] | MutableMapping[str, Any] |
Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None
ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None
"""
if options and isinstance(options, dict):
return options.get("tools")
@@ -1996,6 +2007,11 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
# Remove additional_function_arguments from options passed to underlying chat client
# It's for tool invocation only and not recognized by chat service APIs
mutable_options.pop("additional_function_arguments", None)
# Support tools passed via kwargs in direct client.get_response(...) calls.
if "tools" in filtered_kwargs:
if mutable_options.get("tools") is None:
mutable_options["tools"] = filtered_kwargs["tools"]
filtered_kwargs.pop("tools", None)
if not stream:
+17 -44
View File
@@ -15,7 +15,8 @@ from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Literal, NewTyp
from pydantic import BaseModel
from ._serialization import SerializationMixin
from ._tools import FunctionTool, tool
from ._tools import ToolTypes
from ._tools import normalize_tools as _normalize_tools
from .exceptions import AdditionItemMismatch, ContentError
if sys.version_info >= (3, 13):
@@ -2871,10 +2872,9 @@ class _ChatOptionsBase(TypedDict, total=False):
# Tool configuration (forward reference to avoid circular import)
tools: (
FunctionTool
ToolTypes
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| Sequence[ToolTypes | Callable[..., Any]]
| None
)
tool_choice: ToolMode | Literal["auto", "required", "none"]
@@ -2963,18 +2963,11 @@ async def validate_chat_options(options: dict[str, Any]) -> dict[str, Any]:
def normalize_tools(
tools: (
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None
),
) -> list[FunctionTool | MutableMapping[str, Any]]:
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
) -> list[ToolTypes]:
"""Normalize tools into a list.
Converts callables to FunctionTool objects and ensures all tools are either
FunctionTool instances or MutableMappings.
Converts callables to FunctionTool objects and preserves existing tool objects.
Args:
tools: Tools to normalize - can be a single tool, callable, or sequence.
@@ -2999,37 +2992,16 @@ def normalize_tools(
# List of tools
tools = normalize_tools([my_tool, another_tool])
"""
final_tools: list[FunctionTool | MutableMapping[str, Any]] = []
if not tools:
return final_tools
if not isinstance(tools, Sequence) or isinstance(tools, (str, MutableMapping)):
# Single tool (not a sequence, or is a mapping which shouldn't be treated as sequence)
if not isinstance(tools, (FunctionTool, MutableMapping)):
return [tool(tools)]
return [tools]
for tool_item in tools:
if isinstance(tool_item, (FunctionTool, MutableMapping)):
final_tools.append(tool_item)
else:
# Convert callable to FunctionTool
final_tools.append(tool(tool_item))
return final_tools
return _normalize_tools(tools)
async def validate_tools(
tools: (
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None
),
) -> list[FunctionTool | MutableMapping[str, Any]]:
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
) -> list[ToolTypes]:
"""Validate and normalize tools into a list.
Converts callables to FunctionTool objects, expands MCP tools to their constituent
functions (connecting them if needed), and ensures all tools are either FunctionTool
instances or MutableMappings.
functions (connecting them if needed), while preserving non-callable tool objects.
Args:
tools: Tools to validate - can be a single tool, callable, or sequence.
@@ -3058,7 +3030,7 @@ async def validate_tools(
normalized = normalize_tools(tools)
# Handle MCP tool expansion (async-only)
final_tools: list[FunctionTool | MutableMapping[str, Any]] = []
final_tools: list[ToolTypes] = []
for tool_ in normalized:
# Import MCPTool here to avoid circular imports
from ._mcp import MCPTool
@@ -3076,20 +3048,21 @@ async def validate_tools(
def validate_tool_mode(
tool_choice: ToolMode | Literal["auto", "required", "none"] | None,
) -> ToolMode:
) -> ToolMode | None:
"""Validate and normalize tool_choice to a ToolMode dict.
Args:
tool_choice: The tool choice value to validate.
Returns:
A ToolMode dict (contains keys: "mode", and optionally "required_function_name").
A ToolMode dict (contains keys: "mode", and optionally
"required_function_name"), or ``None`` when not provided.
Raises:
ContentError: If the tool_choice string is invalid.
"""
if not tool_choice:
return {"mode": "none"}
if tool_choice is None:
return None
if isinstance(tool_choice, str):
if tool_choice not in ("auto", "required", "none"):
raise ContentError(f"Invalid tool choice: {tool_choice}")
@@ -1,5 +1,18 @@
# Copyright (c) Microsoft. All rights reserved.
"""Workflow namespace for built-in Agent Framework orchestration primitives.
This module re-exports objects from workflow implementation modules under
``agent_framework._workflows``.
Supported classes include:
- Workflow
- WorkflowBuilder
- AgentExecutor
- Runner
- WorkflowExecutor
"""
from ._agent import WorkflowAgent
from ._agent_executor import (
AgentExecutor,
@@ -1,11 +1,20 @@
# Copyright (c) Microsoft. All rights reserved.
"""A2A integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-a2a``
Supported classes:
- A2AAgent
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_a2a"
PACKAGE_NAME = "agent-framework-a2a"
_IMPORTS = ["__version__", "A2AAgent"]
_IMPORTS = ["A2AAgent"]
def __getattr__(name: str) -> Any:
@@ -2,10 +2,8 @@
from agent_framework_a2a import (
A2AAgent,
__version__,
)
__all__ = [
"A2AAgent",
"__version__",
]
@@ -1,12 +1,24 @@
# Copyright (c) Microsoft. All rights reserved.
"""AG-UI integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-ag-ui``
Supported classes and functions:
- AgentFrameworkAgent
- AGUIChatClient
- AGUIEventConverter
- AGUIHttpService
- add_agent_framework_fastapi_endpoint
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_ag_ui"
PACKAGE_NAME = "agent-framework-ag-ui"
_IMPORTS = [
"__version__",
"AgentFrameworkAgent",
"add_agent_framework_fastapi_endpoint",
"AGUIChatClient",
@@ -5,7 +5,6 @@ from agent_framework_ag_ui import (
AGUIChatClient,
AGUIEventConverter,
AGUIHttpService,
__version__,
add_agent_framework_fastapi_endpoint,
)
@@ -14,6 +13,5 @@ __all__ = [
"AGUIEventConverter",
"AGUIHttpService",
"AgentFrameworkAgent",
"__version__",
"add_agent_framework_fastapi_endpoint",
]
@@ -0,0 +1,35 @@
# Copyright (c) Microsoft. All rights reserved.
"""Amazon Bedrock integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-bedrock``
Supported classes:
- BedrockChatClient
- BedrockChatOptions
- BedrockGuardrailConfig
- BedrockSettings
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_bedrock"
PACKAGE_NAME = "agent-framework-bedrock"
_IMPORTS = ["BedrockChatClient", "BedrockChatOptions", "BedrockGuardrailConfig", "BedrockSettings"]
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
try:
return getattr(importlib.import_module(IMPORT_PATH), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
) from exc
raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
def __dir__() -> list[str]:
return _IMPORTS
@@ -0,0 +1,15 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_bedrock import (
BedrockChatClient,
BedrockChatOptions,
BedrockGuardrailConfig,
BedrockSettings,
)
__all__ = [
"BedrockChatClient",
"BedrockChatOptions",
"BedrockGuardrailConfig",
"BedrockSettings",
]
@@ -1,23 +1,40 @@
# Copyright (c) Microsoft. All rights reserved.
"""Anthropic integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-anthropic``
- ``agent-framework-claude``
Supported classes:
- AnthropicClient
- AnthropicChatOptions
- ClaudeAgent
- ClaudeAgentOptions
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_anthropic"
PACKAGE_NAME = "agent-framework-anthropic"
_IMPORTS = ["__version__", "AnthropicClient", "AnthropicChatOptions"]
_IMPORTS: dict[str, tuple[str, str]] = {
"AnthropicClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"AnthropicChatOptions": ("agent_framework_anthropic", "agent-framework-anthropic"),
"ClaudeAgent": ("agent_framework_claude", "agent-framework-claude"),
"ClaudeAgentOptions": ("agent_framework_claude", "agent-framework-claude"),
}
def __getattr__(name: str) -> Any:
if name in _IMPORTS:
import_path, package_name = _IMPORTS[name]
try:
return getattr(importlib.import_module(IMPORT_PATH), name)
return getattr(importlib.import_module(import_path), name)
except ModuleNotFoundError as exc:
raise ModuleNotFoundError(
f"The '{PACKAGE_NAME}' package is not installed, please do `pip install {PACKAGE_NAME}`"
f"The '{package_name}' package is not installed, please do `pip install {package_name}`"
) from exc
raise AttributeError(f"Module {IMPORT_PATH} has no attribute {name}.")
raise AttributeError(f"Module `anthropic` has no attribute {name}.")
def __dir__() -> list[str]:
return _IMPORTS
return list(_IMPORTS.keys())
@@ -3,11 +3,12 @@
from agent_framework_anthropic import (
AnthropicChatOptions,
AnthropicClient,
__version__,
)
from agent_framework_claude import ClaudeAgent, ClaudeAgentOptions
__all__ = [
"AnthropicChatOptions",
"AnthropicClient",
"__version__",
"ClaudeAgent",
"ClaudeAgentOptions",
]
@@ -1,5 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.
"""Azure integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from optional Azure connector packages and
built-in core Azure OpenAI modules.
Supported classes include:
- AzureAIClient
- AzureAIAgentClient
- AzureOpenAIChatClient
- AzureOpenAIResponsesClient
- AzureAISearchContextProvider
- DurableAIAgent
"""
import importlib
from typing import Any
@@ -1,11 +1,22 @@
# Copyright (c) Microsoft. All rights reserved.
"""ChatKit integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-chatkit``
Supported classes and functions:
- ThreadItemConverter
- simple_to_agent_input
- stream_agent_response
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_chatkit"
PACKAGE_NAME = "agent-framework-chatkit"
_IMPORTS = ["__version__", "ThreadItemConverter", "simple_to_agent_input", "stream_agent_response"]
_IMPORTS = ["ThreadItemConverter", "simple_to_agent_input", "stream_agent_response"]
def __getattr__(name: str) -> Any:
@@ -2,14 +2,12 @@
from agent_framework_chatkit import (
ThreadItemConverter,
__version__,
simple_to_agent_input,
stream_agent_response,
)
__all__ = [
"ThreadItemConverter",
"__version__",
"simple_to_agent_input",
"stream_agent_response",
]
@@ -1,12 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
"""Declarative integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-declarative``
Supported classes include:
- AgentFactory
- WorkflowFactory
- ExternalInputRequest
- ExternalInputResponse
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_declarative"
PACKAGE_NAME = "agent-framework-declarative"
_IMPORTS = [
"__version__",
"AgentFactory",
"AgentExternalInputRequest",
"AgentExternalInputResponse",
@@ -13,7 +13,6 @@ from agent_framework_declarative import (
ProviderTypeMapping,
WorkflowFactory,
WorkflowState,
__version__,
)
__all__ = [
@@ -29,5 +28,4 @@ __all__ = [
"ProviderTypeMapping",
"WorkflowFactory",
"WorkflowState",
"__version__",
]
@@ -1,5 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.
"""DevUI integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-devui``
Supported classes and functions include:
- DevServer
- AgentFrameworkRequest
- DiscoveryResponse
- ResponseStreamEvent
- serve
- main
"""
import importlib
from typing import Any
@@ -16,7 +30,6 @@ _IMPORTS = [
"main",
"register_cleanup",
"serve",
"__version__",
]
@@ -8,7 +8,6 @@ from agent_framework_devui import (
OpenAIError,
OpenAIResponse,
ResponseStreamEvent,
__version__,
main,
register_cleanup,
serve,
@@ -22,7 +21,6 @@ __all__ = [
"OpenAIError",
"OpenAIResponse",
"ResponseStreamEvent",
"__version__",
"main",
"register_cleanup",
"serve",
@@ -1,5 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
"""Exception hierarchy used across Agent Framework core and connectors."""
import logging
from typing import Any, Literal
@@ -1,5 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.
"""GitHub integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-github-copilot``
Supported classes:
- GitHubCopilotAgent
- GitHubCopilotOptions
- GitHubCopilotSettings
"""
import importlib
from typing import Any
@@ -7,7 +18,6 @@ _IMPORTS: dict[str, tuple[str, str]] = {
"GitHubCopilotAgent": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
"GitHubCopilotOptions": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
"GitHubCopilotSettings": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
"__version__": ("agent_framework_github_copilot", "agent-framework-github-copilot"),
}
@@ -4,12 +4,10 @@ from agent_framework_github_copilot import (
GitHubCopilotAgent,
GitHubCopilotOptions,
GitHubCopilotSettings,
__version__,
)
__all__ = [
"GitHubCopilotAgent",
"GitHubCopilotOptions",
"GitHubCopilotSettings",
"__version__",
]
@@ -1,4 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
"""Lab namespace package for experimental Agent Framework integrations.
This module extends the package path so experimental lab integrations can be
distributed in separate packages under the ``agent_framework.lab`` namespace.
"""
# This makes agent_framework.lab a namespace package
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -1,11 +1,20 @@
# Copyright (c) Microsoft. All rights reserved.
"""Mem0 integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-mem0``
Supported classes:
- Mem0ContextProvider
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_mem0"
PACKAGE_NAME = "agent-framework-mem0"
_IMPORTS = ["__version__", "Mem0ContextProvider"]
_IMPORTS = ["Mem0ContextProvider"]
def __getattr__(name: str) -> Any:
@@ -2,10 +2,8 @@
from agent_framework_mem0 import (
Mem0ContextProvider,
__version__,
)
__all__ = [
"Mem0ContextProvider",
"__version__",
]
@@ -1,11 +1,36 @@
# Copyright (c) Microsoft. All rights reserved.
"""Microsoft integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-copilotstudio``
- ``agent-framework-purview``
- ``agent-framework-foundry-local``
Supported classes:
- CopilotStudioAgent
- PurviewPolicyMiddleware
- PurviewChatPolicyMiddleware
- PurviewSettings
- PurviewAppLocation
- PurviewLocationType
- PurviewAuthenticationError
- PurviewPaymentRequiredError
- PurviewRateLimitError
- PurviewRequestError
- PurviewServiceError
- CacheProvider
- FoundryLocalChatOptions
- FoundryLocalClient
- FoundryLocalSettings
"""
import importlib
from typing import Any
_IMPORTS: dict[str, tuple[str, str]] = {
"CopilotStudioAgent": ("agent_framework_copilotstudio", "agent-framework-copilotstudio"),
"__version__": ("agent_framework_copilotstudio", "agent-framework-copilotstudio"),
"acquire_token": ("agent_framework_copilotstudio", "agent-framework-copilotstudio"),
"PurviewPolicyMiddleware": ("agent_framework_purview", "agent-framework-purview"),
"PurviewChatPolicyMiddleware": ("agent_framework_purview", "agent-framework-purview"),
@@ -18,6 +43,9 @@ _IMPORTS: dict[str, tuple[str, str]] = {
"PurviewRequestError": ("agent_framework_purview", "agent-framework-purview"),
"PurviewServiceError": ("agent_framework_purview", "agent-framework-purview"),
"CacheProvider": ("agent_framework_purview", "agent-framework-purview"),
"FoundryLocalChatOptions": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
"FoundryLocalClient": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
"FoundryLocalSettings": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
}
@@ -2,9 +2,13 @@
from agent_framework_copilotstudio import (
CopilotStudioAgent,
__version__,
acquire_token,
)
from agent_framework_foundry_local import (
FoundryLocalChatOptions,
FoundryLocalClient,
FoundryLocalSettings,
)
from agent_framework_purview import (
CacheProvider,
PurviewAppLocation,
@@ -22,6 +26,9 @@ from agent_framework_purview import (
__all__ = [
"CacheProvider",
"CopilotStudioAgent",
"FoundryLocalChatOptions",
"FoundryLocalClient",
"FoundryLocalSettings",
"PurviewAppLocation",
"PurviewAuthenticationError",
"PurviewChatPolicyMiddleware",
@@ -32,6 +39,5 @@ __all__ = [
"PurviewRequestError",
"PurviewServiceError",
"PurviewSettings",
"__version__",
"acquire_token",
]
@@ -1,5 +1,16 @@
# Copyright (c) Microsoft. All rights reserved.
"""Observability and OpenTelemetry helpers for Agent Framework.
Commonly used exports:
- enable_instrumentation
- configure_otel_providers
- AgentTelemetryLayer
- ChatTelemetryLayer
- get_tracer
- get_meter
"""
from __future__ import annotations
import contextlib
@@ -1128,11 +1139,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
opts: dict[str, Any] = options or {} # type: ignore[assignment]
provider_name = str(self.otel_provider_name)
model_id = kwargs.get("model_id") or opts.get("model_id") or getattr(self, "model_id", None) or "unknown"
service_url = str(
service_url_func()
if (service_url_func := getattr(self, "service_url", None)) and callable(service_url_func)
else "unknown"
)
service_url_func = getattr(self, "service_url", None)
service_url = str(service_url_func() if callable(service_url_func) else "unknown")
attributes = _get_span_attributes(
operation_name=OtelAttr.CHAT_COMPLETION_OPERATION,
provider_name=provider_name,
@@ -1,11 +1,21 @@
# Copyright (c) Microsoft. All rights reserved.
"""Ollama integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-ollama``
Supported classes:
- OllamaChatClient
- OllamaSettings
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_ollama"
PACKAGE_NAME = "agent-framework-ollama"
_IMPORTS = ["__version__", "OllamaChatClient", "OllamaSettings"]
_IMPORTS = ["OllamaChatClient", "OllamaSettings"]
def __getattr__(name: str) -> Any:
@@ -3,11 +3,9 @@
from agent_framework_ollama import (
OllamaChatClient,
OllamaSettings,
__version__,
)
__all__ = [
"OllamaChatClient",
"OllamaSettings",
"__version__",
]
@@ -1,5 +1,17 @@
# Copyright (c) Microsoft. All rights reserved.
"""OpenAI namespace for built-in Agent Framework clients.
This module re-exports objects from the core OpenAI implementation modules in
``agent_framework.openai``.
Supported classes include:
- OpenAIChatClient
- OpenAIResponsesClient
- OpenAIAssistantsClient
- OpenAIAssistantProvider
"""
from ._assistant_provider import OpenAIAssistantProvider
from ._assistants_client import (
AssistantToolResources,
@@ -15,8 +15,7 @@ from agent_framework._settings import SecretString, load_settings
from .._agents import Agent
from .._middleware import MiddlewareTypes
from .._sessions import BaseContextProvider
from .._tools import FunctionTool
from .._types import normalize_tools
from .._tools import FunctionTool, ToolTypes, normalize_tools
from ..exceptions import ServiceInitializationError
from ._assistants_client import OpenAIAssistantsClient
from ._shared import OpenAISettings, from_assistant_tools, to_assistant_tools
@@ -43,13 +42,6 @@ OptionsCoT = TypeVar(
covariant=True,
)
_ToolsType = (
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
)
class OpenAIAssistantProvider(Generic[OptionsCoT]):
"""Provider for creating Agent instances from OpenAI Assistants API.
@@ -203,7 +195,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
model: str,
instructions: str | None = None,
description: str | None = None,
tools: _ToolsType | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
metadata: dict[str, str] | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
@@ -259,7 +251,8 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
"""
# Normalize tools
normalized_tools = normalize_tools(tools)
api_tools = to_assistant_tools(normalized_tools) if normalized_tools else []
assistant_tools = [tool for tool in normalized_tools if isinstance(tool, (FunctionTool, MutableMapping))]
api_tools = to_assistant_tools(assistant_tools) if assistant_tools else []
# Extract response_format from default_options if present
opts = dict(default_options) if default_options else {}
@@ -311,7 +304,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
self,
assistant_id: str,
*,
tools: _ToolsType | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
instructions: str | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
@@ -377,7 +370,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
self,
assistant: Assistant,
*,
tools: _ToolsType | None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
instructions: str | None = None,
default_options: OptionsCoT | None = None,
middleware: Sequence[MiddlewareTypes] | None = None,
@@ -442,7 +435,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
def _validate_function_tools(
self,
assistant_tools: list[Any],
provided_tools: _ToolsType | None,
provided_tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
) -> None:
"""Validate that required function tools are provided.
@@ -493,8 +486,8 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
def _merge_tools(
self,
assistant_tools: list[Any],
user_tools: _ToolsType | None,
) -> list[FunctionTool | MutableMapping[str, Any]]:
user_tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
) -> list[FunctionTool | MutableMapping[str, Any] | Any]:
"""Merge hosted tools from assistant with user-provided function tools.
Args:
@@ -504,7 +497,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
Returns:
A list of all tools (hosted tools + user function implementations).
"""
merged: list[FunctionTool | MutableMapping[str, Any]] = []
merged: list[FunctionTool | MutableMapping[str, Any] | Any] = []
# Add hosted tools from assistant using shared conversion
hosted_tools = from_assistant_tools(assistant_tools)
@@ -520,7 +513,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
def _create_chat_agent_from_assistant(
self,
assistant: Assistant,
tools: list[FunctionTool | MutableMapping[str, Any]] | None,
tools: list[FunctionTool | MutableMapping[str, Any] | Any] | None,
instructions: str | None,
middleware: Sequence[MiddlewareTypes] | None,
context_providers: Sequence[BaseContextProvider] | None,
@@ -36,6 +36,7 @@ from .._tools import (
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
normalize_tools,
)
from .._types import (
ChatOptions,
@@ -686,26 +687,26 @@ class OpenAIAssistantsClient( # type: ignore[misc]
tool_definitions: list[MutableMapping[str, Any]] = []
# Always include tools if provided, regardless of tool_choice
# tool_choice="none" means the model won't call tools, but tools should still be available
if tools is not None:
for tool in tools:
if isinstance(tool, FunctionTool):
tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType]
elif isinstance(tool, MutableMapping):
# Pass through dict-based tools directly (from static factory methods)
tool_definitions.append(tool)
for tool in normalize_tools(tools):
if isinstance(tool, FunctionTool):
tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType]
elif isinstance(tool, MutableMapping):
# Pass through dict-based tools directly (from static factory methods)
tool_definitions.append(tool)
if len(tool_definitions) > 0:
run_options["tools"] = tool_definitions
if (mode := tool_mode["mode"]) == "required" and (
func_name := tool_mode.get("required_function_name")
) is not None:
run_options["tool_choice"] = {
"type": "function",
"function": {"name": func_name},
}
else:
run_options["tool_choice"] = mode
if tool_mode is not None:
if (mode := tool_mode["mode"]) == "required" and (
func_name := tool_mode.get("required_function_name")
) is not None:
run_options["tool_choice"] = {
"type": "function",
"function": {"name": func_name},
}
else:
run_options["tool_choice"] = mode
if response_format is not None:
if isinstance(response_format, dict):
@@ -27,6 +27,8 @@ from .._tools import (
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
ToolTypes,
normalize_tools,
)
from .._types import (
ChatOptions,
@@ -271,21 +273,24 @@ class RawOpenAIChatClient( # type: ignore[misc]
# region content creation
def _prepare_tools_for_openai(self, tools: Sequence[Any]) -> dict[str, Any]:
def _prepare_tools_for_openai(
self,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
) -> dict[str, Any]:
"""Prepare tools for the OpenAI Chat Completions API.
Converts FunctionTool to JSON schema format. Web search tools are routed
to web_search_options parameter. All other tools pass through unchanged.
Args:
tools: Sequence of tools to prepare.
tools: Tool(s) to prepare.
Returns:
Dict containing tools and optionally web_search_options.
"""
chat_tools: list[Any] = []
web_search_options: dict[str, Any] | None = None
for tool in tools:
for tool in normalize_tools(tools):
if isinstance(tool, FunctionTool):
chat_tools.append(tool.to_json_schema_spec())
elif isinstance(tool, MutableMapping) and tool.get("type") == "web_search":
@@ -338,15 +343,16 @@ class RawOpenAIChatClient( # type: ignore[misc]
run_options.pop("tool_choice", None)
elif tool_choice := run_options.pop("tool_choice", None):
tool_mode = validate_tool_mode(tool_choice)
if (mode := tool_mode.get("mode")) == "required" and (
func_name := tool_mode.get("required_function_name")
) is not None:
run_options["tool_choice"] = {
"type": "function",
"function": {"name": func_name},
}
else:
run_options["tool_choice"] = mode
if tool_mode is not None:
if (mode := tool_mode.get("mode")) == "required" and (
func_name := tool_mode.get("required_function_name")
) is not None:
run_options["tool_choice"] = {
"type": "function",
"function": {"name": func_name},
}
else:
run_options["tool_choice"] = mode
# response format
if response_format := options.get("response_format"):
@@ -822,15 +822,16 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
# tool_choice: convert ToolMode to appropriate format
if tool_choice := options.get("tool_choice"):
tool_mode = validate_tool_mode(tool_choice)
if (mode := tool_mode.get("mode")) == "required" and (
func_name := tool_mode.get("required_function_name")
) is not None:
run_options["tool_choice"] = {
"type": "function",
"name": func_name,
}
else:
run_options["tool_choice"] = mode
if tool_mode is not None:
if (mode := tool_mode.get("mode")) == "required" and (
func_name := tool_mode.get("required_function_name")
) is not None:
run_options["tool_choice"] = {
"type": "function",
"name": func_name,
}
else:
run_options["tool_choice"] = mode
else:
run_options.pop("parallel_tool_calls", None)
run_options.pop("tool_choice", None)
@@ -1,12 +1,24 @@
# Copyright (c) Microsoft. All rights reserved.
"""Orchestrations integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-orchestrations``
Supported classes include:
- SequentialBuilder
- ConcurrentBuilder
- GroupChatBuilder
- MagenticBuilder
- HandoffBuilder
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_orchestrations"
PACKAGE_NAME = "agent-framework-orchestrations"
_IMPORTS = [
"__version__",
# Sequential
"SequentialBuilder",
# Concurrent
@@ -35,7 +35,6 @@ from agent_framework_orchestrations import (
MagenticResetSignal,
SequentialBuilder,
StandardMagenticManager,
__version__,
)
__all__ = [
@@ -73,5 +72,4 @@ __all__ = [
"MagenticResetSignal",
"SequentialBuilder",
"StandardMagenticManager",
"__version__",
]
@@ -1,11 +1,21 @@
# Copyright (c) Microsoft. All rights reserved.
"""Redis integration namespace for optional Agent Framework connectors.
This module lazily re-exports objects from:
- ``agent-framework-redis``
Supported classes:
- RedisContextProvider
- RedisHistoryProvider
"""
import importlib
from typing import Any
IMPORT_PATH = "agent_framework_redis"
PACKAGE_NAME = "agent-framework-redis"
_IMPORTS = ["__version__", "RedisContextProvider", "RedisHistoryProvider"]
_IMPORTS = ["RedisContextProvider", "RedisHistoryProvider"]
def __getattr__(name: str) -> Any:
@@ -3,11 +3,9 @@
from agent_framework_redis import (
RedisContextProvider,
RedisHistoryProvider,
__version__,
)
__all__ = [
"RedisContextProvider",
"RedisHistoryProvider",
"__version__",
]
+3
View File
@@ -45,13 +45,16 @@ all = [
"agent-framework-ag-ui",
"agent-framework-azure-ai-search",
"agent-framework-anthropic",
"agent-framework-claude",
"agent-framework-azure-ai",
"agent-framework-azurefunctions",
"agent-framework-bedrock",
"agent-framework-chatkit",
"agent-framework-copilotstudio",
"agent-framework-declarative",
"agent-framework-devui",
"agent-framework-durabletask",
"agent-framework-foundry-local",
"agent-framework-github-copilot",
"agent-framework-lab",
"agent-framework-mem0",
@@ -56,6 +56,36 @@ async def test_base_client_with_function_calling(chat_client_base: SupportsChatG
assert response.messages[2].text == "done"
async def test_base_client_with_function_calling_tools_in_kwargs(chat_client_base: SupportsChatGetResponse):
exec_counter = 0
@tool(name="test_function", approval_mode="never_require")
def ai_func(arg1: str) -> str:
nonlocal exec_counter
exec_counter += 1
return f"Processed {arg1}"
chat_client_base.run_responses = [
ChatResponse(
messages=Message(
role="assistant",
contents=[
Content.from_function_call(call_id="1", name="test_function", arguments='{"arg1": "value1"}')
],
)
),
ChatResponse(messages=Message(role="assistant", text="done")),
]
response = await chat_client_base.get_response("hello", tools=[ai_func])
assert exec_counter == 1
assert len(response.messages) == 3
assert response.messages[1].role == "tool"
assert response.messages[1].contents[0].type == "function_result"
assert response.messages[1].contents[0].result == "Processed value1"
@pytest.mark.parametrize("max_iterations", [3])
async def test_base_client_with_function_calling_resets(chat_client_base: SupportsChatGetResponse):
exec_counter = 0
@@ -921,8 +921,8 @@ def test_chat_options_tool_choice_validation():
}
assert validate_tool_mode({"mode": "none"}) == {"mode": "none"}
# None should return mode==none
assert validate_tool_mode(None) == {"mode": "none"}
# None should remain unset
assert validate_tool_mode(None) is None
with raises(ContentError):
validate_tool_mode("invalid_mode")
@@ -701,6 +701,7 @@ def test_prepare_options_basic(mock_async_openai: MagicMock) -> None:
assert run_options["model"] == "gpt-4"
assert run_options["temperature"] == 0.7
assert run_options["top_p"] == 0.9
assert "tool_choice" not in run_options
assert tool_results is None
@@ -733,6 +734,52 @@ def test_prepare_options_with_tool_tool(mock_async_openai: MagicMock) -> None:
assert run_options["tool_choice"] == "auto"
def test_prepare_options_with_tools_without_tool_choice(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options keeps tool_choice unset when not provided."""
client = create_test_openai_assistants_client(mock_async_openai)
@tool(approval_mode="never_require")
def test_function(query: str) -> str:
"""A test function."""
return f"Result for {query}"
options = {
"tools": [test_function],
}
messages = [Message(role="user", text="Hello")]
run_options, _ = client._prepare_options(messages, options) # type: ignore
assert "tools" in run_options
assert "tool_choice" not in run_options
def test_prepare_options_with_single_tool_tool(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with a single FunctionTool (non-sequence)."""
client = create_test_openai_assistants_client(mock_async_openai)
@tool(approval_mode="never_require")
def test_function(query: str) -> str:
"""A test function."""
return f"Result for {query}"
options = {
"tools": test_function,
"tool_choice": "auto",
}
messages = [Message(role="user", text="Hello")]
run_options, tool_results = client._prepare_options(messages, options) # type: ignore
assert "tools" in run_options
assert len(run_options["tools"]) == 1
assert run_options["tools"][0]["type"] == "function"
assert "function" in run_options["tools"][0]
assert run_options["tool_choice"] == "auto"
assert tool_results is None
def test_prepare_options_with_code_interpreter(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with code interpreter tool."""
client = create_test_openai_assistants_client(mock_async_openai)
@@ -190,6 +190,21 @@ def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None
assert result["tools"] == [dict_tool]
def test_prepare_tools_with_single_function_tool(openai_unit_test_env: dict[str, str]) -> None:
"""Test that a single FunctionTool is accepted for tool preparation."""
client = OpenAIChatClient()
@tool(approval_mode="never_require")
def test_function(query: str) -> str:
"""A test function."""
return f"Result for {query}"
result = client._prepare_tools_for_openai(test_function)
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "function"
@tool(approval_mode="never_require")
def get_story_text() -> str:
"""Returns a story about Emily and David."""
+4
View File
@@ -7,3 +7,7 @@ pip install agent-framework-foundry-local --pre
```
and see the [README](https://github.com/microsoft/agent-framework/tree/main/python/README.md) for more information.
## Foundry Local Sample
See the [Foundry Local provider sample](../../samples/02-agents/providers/foundry_local/foundry_local_agent.py) for a runnable example.
@@ -22,7 +22,7 @@ from agent_framework import (
normalize_messages,
)
from agent_framework._settings import load_settings
from agent_framework._tools import FunctionTool
from agent_framework._tools import FunctionTool, ToolTypes
from agent_framework._types import AgentRunInputs, normalize_tools
from agent_framework.exceptions import ServiceException
from copilot import CopilotClient, CopilotSession
@@ -151,11 +151,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
description: str | None = None,
context_providers: Sequence[BaseContextProvider] | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None = None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
default_options: OptionsT | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
@@ -478,7 +474,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
def _prepare_tools(
self,
tools: list[FunctionTool | MutableMapping[str, Any]],
tools: Sequence[ToolTypes | CopilotTool],
) -> list[CopilotTool]:
"""Convert Agent Framework tools to Copilot SDK tools.
@@ -491,10 +487,12 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
copilot_tools: list[CopilotTool] = []
for tool in tools:
if isinstance(tool, FunctionTool):
copilot_tools.append(self._tool_to_copilot_tool(tool)) # type: ignore
elif isinstance(tool, CopilotTool):
if isinstance(tool, CopilotTool):
copilot_tools.append(tool)
elif isinstance(tool, FunctionTool):
copilot_tools.append(self._tool_to_copilot_tool(tool)) # type: ignore
elif isinstance(tool, MutableMapping):
copilot_tools.append(tool) # type: ignore[arg-type]
# Note: Other tool types (e.g., dict-based hosted tools) are skipped
return copilot_tools
@@ -12,6 +12,8 @@ Hello Agent — Simplest possible agent
This sample creates a minimal agent using AzureOpenAIResponsesClient via an
Azure AI Foundry project endpoint, and runs it in both non-streaming and streaming modes.
There are XML tags in all of the get started samples, those are used to display the same code in the docs repo.
Environment variables:
AZURE_AI_PROJECT_ENDPOINT Your Azure AI Foundry project endpoint
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME Model deployment name (e.g. gpt-4o)
+4 -2
View File
@@ -12,8 +12,8 @@ pip install agent-framework --pre
Set the required environment variables:
```bash
export OPENAI_API_KEY="sk-..."
export OPENAI_RESPONSES_MODEL_ID="gpt-4o" # optional, defaults to gpt-4o
export AZURE_AI_PROJECT_ENDPOINT="https://your-project-endpoint"
export AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME="gpt-4o" # optional, defaults to gpt-4o
```
## Samples
@@ -32,3 +32,5 @@ Run any sample with:
```bash
python 01_hello_agent.py
```
These samples use Azure Foundry models with the Responses API. To switch providers, just replace the client, see [all providers](../02-agents/providers/README.md)
+50 -17
View File
@@ -1,41 +1,74 @@
# Chat Client Examples
This folder contains simple examples demonstrating direct usage of various chat clients.
This folder contains examples for direct chat client usage patterns.
## Examples
| File | Description |
|------|-------------|
| [`azure_assistants_client.py`](azure_assistants_client.py) | Direct usage of Azure Assistants Client for basic chat interactions with Azure OpenAI assistants. |
| [`azure_chat_client.py`](azure_chat_client.py) | Direct usage of Azure Chat Client for chat interactions with Azure OpenAI models. |
| [`azure_responses_client.py`](azure_responses_client.py) | Direct usage of Azure Responses Client for structured response generation with Azure OpenAI models. |
| [`built_in_chat_clients.py`](built_in_chat_clients.py) | Consolidated sample for built-in chat clients. Uses `get_client()` to create the selected client and pass it to `main()`. |
| [`chat_response_cancellation.py`](chat_response_cancellation.py) | Demonstrates how to cancel chat responses during streaming, showing proper cancellation handling and cleanup. |
| [`azure_ai_chat_client.py`](azure_ai_chat_client.py) | Direct usage of Azure AI Chat Client for chat interactions with Azure AI models. |
| [`openai_assistants_client.py`](openai_assistants_client.py) | Direct usage of OpenAI Assistants Client for basic chat interactions with OpenAI assistants. |
| [`openai_chat_client.py`](openai_chat_client.py) | Direct usage of OpenAI Chat Client for chat interactions with OpenAI models. |
| [`openai_responses_client.py`](openai_responses_client.py) | Direct usage of OpenAI Responses Client for structured response generation with OpenAI models. |
| [`custom_chat_client.py`](custom_chat_client.py) | Demonstrates how to create custom chat clients by extending the `BaseChatClient` class. Shows a `EchoingChatClient` implementation and how to integrate it with `Agent` using the `as_agent()` method. |
## Selecting a built-in client
`built_in_chat_clients.py` starts with:
```python
asyncio.run(main("openai_chat"))
```
Change the argument to pick a client:
- `openai_chat`
- `openai_responses`
- `openai_assistants`
- `anthropic`
- `ollama`
- `bedrock`
- `azure_openai_chat`
- `azure_openai_responses`
- `azure_openai_responses_foundry`
- `azure_openai_assistants`
- `azure_ai_agent`
Example:
```bash
uv run samples/02-agents/chat_client/built_in_chat_clients.py
```
## Environment Variables
Depending on which client you're using, set the appropriate environment variables:
Depending on the selected client, set the appropriate environment variables:
**For Azure clients:**
- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint
- `AZURE_OPENAI_CHAT_DEPLOYMENT_NAME`: The name of your Azure OpenAI chat deployment
- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: The name of your Azure OpenAI responses deployment
**For Azure AI client:**
**For Azure OpenAI Foundry responses client (`azure_openai_responses_foundry`):**
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint
- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment
- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME`: The name of your Azure OpenAI responses deployment
**For Azure AI agent client (`azure_ai_agent`):**
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint
- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (used by `azure_ai_agent`)
**For OpenAI clients:**
- `OPENAI_API_KEY`: Your OpenAI API key
- `OPENAI_CHAT_MODEL_ID`: The OpenAI model to use for chat clients (e.g., `gpt-4o`, `gpt-4o-mini`, `gpt-3.5-turbo`)
- `OPENAI_RESPONSES_MODEL_ID`: The OpenAI model to use for responses clients (e.g., `gpt-4o`, `gpt-4o-mini`, `gpt-3.5-turbo`)
- `OPENAI_CHAT_MODEL_ID`: The OpenAI model for `openai_chat` and `openai_assistants`
- `OPENAI_RESPONSES_MODEL_ID`: The OpenAI model for `openai_responses`
**For Ollama client:**
- `OLLAMA_HOST`: Your Ollama server URL (defaults to `http://localhost:11434` if not set)
- `OLLAMA_MODEL_ID`: The Ollama model to use for chat (e.g., `llama3.2`, `llama2`, `codellama`)
**For Anthropic client (`anthropic`):**
- `ANTHROPIC_API_KEY`: Your Anthropic API key
- `ANTHROPIC_CHAT_MODEL_ID`: The Anthropic model ID (for example, `claude-sonnet-4-5`)
> **Note**: For Ollama, ensure you have Ollama installed and running locally with at least one model downloaded. Visit [https://ollama.com/](https://ollama.com/) for installation instructions.
**For Ollama client (`ollama`):**
- `OLLAMA_HOST`: Ollama server URL (defaults to `http://localhost:11434` if unset)
- `OLLAMA_MODEL_ID`: Ollama model name (for example, `mistral`, `qwen2.5:8b`)
**For Bedrock client (`bedrock`):**
- `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 environment variables (for example, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
@@ -1,49 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import tool
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential
from pydantic import Field
"""
Azure AI Chat Client Direct Usage Example
Demonstrates direct AzureAIChatClient usage for chat interactions with Azure AI models.
Shows function calling capabilities with custom business logic.
"""
# 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(
location: Annotated[str, Field(description="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 main() -> None:
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
async with AzureAIAgentClient(credential=AzureCliCredential()) as client:
message = "What's the weather in Amsterdam and in Paris?"
stream = False
print(f"User: {message}")
if stream:
print("Assistant: ", end="")
async for chunk in client.get_response(message, tools=get_weather, stream=True):
if str(chunk):
print(str(chunk), end="")
print("")
else:
response = await client.get_response(message, tools=get_weather)
print(f"Assistant: {response}")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,49 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import tool
from agent_framework.azure import AzureOpenAIAssistantsClient
from azure.identity import AzureCliCredential
from pydantic import Field
"""
Azure Assistants Client Direct Usage Example
Demonstrates direct AzureAssistantsClient usage for chat interactions with Azure OpenAI assistants.
Shows function calling capabilities and automatic assistant creation.
"""
# 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(
location: Annotated[str, Field(description="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 main() -> None:
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
async with AzureOpenAIAssistantsClient(credential=AzureCliCredential()) as client:
message = "What's the weather in Amsterdam and in Paris?"
stream = False
print(f"User: {message}")
if stream:
print("Assistant: ", end="")
async for chunk in client.get_response(message, tools=get_weather, stream=True):
if str(chunk):
print(str(chunk), end="")
print("")
else:
response = await client.get_response(message, tools=get_weather)
print(f"Assistant: {response}")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,49 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import tool
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
from pydantic import Field
"""
Azure Chat Client Direct Usage Example
Demonstrates direct AzureChatClient usage for chat interactions with Azure OpenAI models.
Shows function calling capabilities with custom business logic.
"""
# 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(
location: Annotated[str, Field(description="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 main() -> None:
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
client = AzureOpenAIChatClient(credential=AzureCliCredential())
message = "What's the weather in Amsterdam and in Paris?"
stream = False
print(f"User: {message}")
if stream:
print("Assistant: ", end="")
async for chunk in client.get_response(message, tools=get_weather, stream=True):
if str(chunk):
print(str(chunk), end="")
print("")
else:
response = await client.get_response(message, tools=get_weather)
print(f"Assistant: {response}")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,95 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import tool
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from pydantic import BaseModel
"""
Azure Responses Client Direct Usage Example
Demonstrates direct AzureResponsesClient usage for structured response generation with Azure OpenAI models.
Shows function calling capabilities with custom business logic.
"""
# 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(
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."
@tool(approval_mode="never_require")
def get_time():
"""Get the current time."""
from datetime import datetime
now = datetime.now()
return f"The current date time is {now.strftime('%Y-%m-%d - %H:%M:%S')}."
class WeatherDetail(BaseModel):
"""Structured output for weather information."""
location: str
weather: str
class Weather(BaseModel):
"""Container for multiple outputs."""
date_time: str
weather_details: list[WeatherDetail]
async def main() -> None:
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
client = AzureOpenAIResponsesClient(credential=AzureCliCredential(), api_version="preview")
message = "What's the weather in Amsterdam and in Paris?"
stream = True
print(f"User: {message}")
response = client.get_response(
message,
options={"response_format": Weather, "tools": [get_weather, get_time]},
stream=stream,
)
if stream:
response = await response.get_final_response()
else:
response = await response
if result := response.value:
print(f"Assistant: {result.model_dump_json(indent=2)}")
else:
print(f"Assistant: {response.text}")
# Expected output (time will be different):
"""
User: What's the weather in Amsterdam and in Paris?
Assistant: {
"date_time": "2026-02-06 - 13:30:40",
"weather_details": [
{
"location": "Amsterdam",
"weather": "The weather in Amsterdam is cloudy with a high of 21°C."
},
{
"location": "Paris",
"weather": "The weather in Paris is sunny with a high of 27°C."
}
]
}
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,156 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from random import randint
from typing import Annotated, Any, Literal
from agent_framework import SupportsChatGetResponse, tool
from agent_framework.azure import (
AzureAIAgentClient,
AzureOpenAIAssistantsClient,
)
from agent_framework.openai import OpenAIAssistantsClient
from azure.identity import AzureCliCredential
from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential
from pydantic import Field
"""
Built-in Chat Clients Example
This sample demonstrates how to run the same prompt flow against different built-in
chat clients using a single `get_client` factory.
Select one of these client names:
- openai_chat
- openai_responses
- openai_assistants
- anthropic
- ollama
- bedrock
- azure_openai_chat
- azure_openai_responses
- azure_openai_responses_foundry
- azure_openai_assistants
- azure_ai_agent
"""
ClientName = Literal[
"openai_chat",
"openai_responses",
"openai_assistants",
"anthropic",
"ollama",
"bedrock",
"azure_openai_chat",
"azure_openai_responses",
"azure_openai_responses_foundry",
"azure_openai_assistants",
"azure_ai_agent",
]
# 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(
location: Annotated[str, Field(description="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."
def get_client(client_name: ClientName) -> SupportsChatGetResponse[Any]:
"""Create a built-in chat client from a name."""
from agent_framework.amazon import BedrockChatClient
from agent_framework.anthropic import AnthropicClient
from agent_framework.azure import (
AzureOpenAIChatClient,
AzureOpenAIResponsesClient,
)
from agent_framework.ollama import OllamaChatClient
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
# 1. Create OpenAI clients.
if client_name == "openai_chat":
return OpenAIChatClient()
if client_name == "openai_responses":
return OpenAIResponsesClient()
if client_name == "openai_assistants":
return OpenAIAssistantsClient()
if client_name == "anthropic":
return AnthropicClient()
if client_name == "ollama":
return OllamaChatClient()
if client_name == "bedrock":
return BedrockChatClient()
# 2. Create Azure OpenAI clients.
if client_name == "azure_openai_chat":
return AzureOpenAIChatClient(credential=AzureCliCredential())
if client_name == "azure_openai_responses":
return AzureOpenAIResponsesClient(credential=AzureCliCredential(), api_version="preview")
if client_name == "azure_openai_responses_foundry":
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
if client_name == "azure_openai_assistants":
return AzureOpenAIAssistantsClient(credential=AzureCliCredential())
# 3. Create Azure AI client.
if client_name == "azure_ai_agent":
return AzureAIAgentClient(credential=AsyncAzureCliCredential())
raise ValueError(f"Unsupported client name: {client_name}")
async def main(client_name: ClientName = "openai_chat") -> None:
"""Run a basic prompt using a selected built-in client."""
client = get_client(client_name)
# 1. Configure prompt and streaming mode.
message = "What's the weather in Amsterdam and in Paris?"
stream = os.getenv("STREAM", "false").lower() == "true"
print(f"Client: {client_name}")
print(f"User: {message}")
# 2. Run with context-managed clients.
if isinstance(client, OpenAIAssistantsClient | AzureOpenAIAssistantsClient | AzureAIAgentClient):
async with client:
if stream:
response_stream = client.get_response(message, stream=True, options={"tools": get_weather})
print("Assistant: ", end="")
async for chunk in response_stream:
if chunk.text:
print(chunk.text, end="")
print("")
else:
print(f"Assistant: {await client.get_response(message, stream=False, options={'tools': get_weather})}")
return
# 3. Run with non-context-managed clients.
if stream:
response_stream = client.get_response(message, stream=True, options={"tools": get_weather})
print("Assistant: ", end="")
async for chunk in response_stream:
if chunk.text:
print(chunk.text, end="")
print("")
else:
print(f"Assistant: {await client.get_response(message, stream=False, options={'tools': get_weather})}")
if __name__ == "__main__":
asyncio.run(main("openai_chat"))
"""
Sample output:
User: What's the weather in Amsterdam and in Paris?
Assistant: The weather in Amsterdam is sunny with a high of 25°C.
...and in Paris it is cloudy with a high of 19°C.
"""
@@ -4,7 +4,7 @@ import asyncio
import random
import sys
from collections.abc import AsyncIterable, Awaitable, Mapping, Sequence
from typing import Any, ClassVar, Generic
from typing import Any, ClassVar, TypeAlias, TypedDict
from agent_framework import (
BaseChatClient,
@@ -15,15 +15,9 @@ from agent_framework import (
FunctionInvocationLayer,
Message,
ResponseStream,
Role,
)
from agent_framework._clients import OptionsCoT
from agent_framework.observability import ChatTelemetryLayer
if sys.version_info >= (3, 13):
pass
else:
pass
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
@@ -38,7 +32,18 @@ middleware, telemetry, and function invocation layers explicitly.
"""
class EchoingChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]):
class EchoingChatClientOptions(TypedDict, total=False):
"""Custom options for EchoingChatClient."""
uppercase: bool
suffix: str
stream_delay_seconds: float
OptionsT: TypeAlias = EchoingChatClientOptions
class EchoingChatClient(BaseChatClient[OptionsT]):
"""A custom chat client that echoes messages back with modifications.
This demonstrates how to implement a custom chat client by extending BaseChatClient
@@ -73,7 +78,7 @@ class EchoingChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]):
# Echo the last user message
last_user_message = None
for message in reversed(messages):
if message.role == Role.USER:
if message.role == "user":
last_user_message = message
break
@@ -82,7 +87,13 @@ class EchoingChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]):
else:
response_text = f"{self.prefix} [No text message found]"
response_message = Message(role=Role.ASSISTANT, contents=[Content.from_text(response_text)])
if options.get("uppercase"):
response_text = response_text.upper()
if suffix := options.get("suffix"):
response_text = f"{response_text} {suffix}"
stream_delay_seconds = float(options.get("stream_delay_seconds", 0.05))
response_message = Message(role="assistant", contents=[Content.from_text(response_text)])
response = ChatResponse(
messages=[response_message],
@@ -102,21 +113,20 @@ class EchoingChatClient(BaseChatClient[OptionsCoT], Generic[OptionsCoT]):
for char in response_text_local:
yield ChatResponseUpdate(
contents=[Content.from_text(char)],
role=Role.ASSISTANT,
role="assistant",
response_id=f"echo-stream-resp-{random.randint(1000, 9999)}",
model_id="echo-model-v1",
)
await asyncio.sleep(0.05)
await asyncio.sleep(stream_delay_seconds)
return ResponseStream(_stream(), finalizer=lambda updates: response)
class EchoingChatClientWithLayers( # type: ignore[misc,type-var]
ChatMiddlewareLayer[OptionsCoT],
ChatTelemetryLayer[OptionsCoT],
FunctionInvocationLayer[OptionsCoT],
EchoingChatClient[OptionsCoT],
Generic[OptionsCoT],
class EchoingChatClientWithLayers( # type: ignore[misc]
ChatMiddlewareLayer[OptionsT],
ChatTelemetryLayer[OptionsT],
FunctionInvocationLayer[OptionsT],
EchoingChatClient,
):
"""Echoing chat client that explicitly composes middleware, telemetry, and function layers."""
@@ -134,7 +144,14 @@ async def main() -> None:
# Use the chat client directly
print("Using chat client directly:")
direct_response = await echo_client.get_response("Hello, custom chat client!")
direct_response = await echo_client.get_response(
"Hello, custom chat client!",
options={
"uppercase": True,
"suffix": "(CUSTOM OPTIONS)",
"stream_delay_seconds": 0.02,
},
)
print(f"Direct response: {direct_response.messages[0].text}")
# Create an agent using the custom chat client
@@ -1,47 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import tool
from agent_framework.openai import OpenAIAssistantsClient
from pydantic import Field
"""
OpenAI Assistants Client Direct Usage Example
Demonstrates direct OpenAIAssistantsClient usage for chat interactions with OpenAI assistants.
Shows function calling capabilities and automatic assistant creation.
"""
# 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(
location: Annotated[str, Field(description="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 main() -> None:
async with OpenAIAssistantsClient() as client:
message = "What's the weather in Amsterdam and in Paris?"
stream = False
print(f"User: {message}")
if stream:
print("Assistant: ", end="")
async for chunk in client.get_response(message, tools=get_weather, stream=True):
if str(chunk):
print(str(chunk), end="")
print("")
else:
response = await client.get_response(message, tools=get_weather)
print(f"Assistant: {response}")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,47 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import tool
from agent_framework.openai import OpenAIChatClient
from pydantic import Field
"""
OpenAI Chat Client Direct Usage Example
Demonstrates direct OpenAIChatClient usage for chat interactions with OpenAI models.
Shows function calling capabilities with custom business logic.
"""
# 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(
location: Annotated[str, Field(description="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 main() -> None:
client = OpenAIChatClient()
message = "What's the weather in Amsterdam and in Paris?"
stream = True
print(f"User: {message}")
if stream:
print("Assistant: ", end="")
async for chunk in client.get_response(message, tools=get_weather, stream=True):
if chunk.text:
print(chunk.text, end="")
print("")
else:
response = await client.get_response(message, tools=get_weather)
print(f"Assistant: {response}")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,47 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from random import randint
from typing import Annotated
from agent_framework import tool
from agent_framework.openai import OpenAIResponsesClient
from pydantic import Field
"""
OpenAI Responses Client Direct Usage Example
Demonstrates direct OpenAIResponsesClient usage for structured response generation with OpenAI models.
Shows function calling capabilities with custom business logic.
"""
# 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(
location: Annotated[str, Field(description="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 main() -> None:
client = OpenAIResponsesClient()
message = "What's the weather in Amsterdam and in Paris?"
stream = True
print(f"User: {message}")
print("Assistant: ", end="")
response = client.get_response(message, stream=stream, options={"tools": get_weather})
if stream:
# TODO: review names of the methods, could be related to things like HTTP clients?
response.with_transform_hook(lambda chunk: print(chunk.text, end=""))
await response.get_final_response()
else:
response = await response
print(f"Assistant: {response}")
if __name__ == "__main__":
asyncio.run(main())
@@ -75,6 +75,7 @@ async def main() -> None:
if knowledge_base_name:
# Use existing Knowledge Base - simplest approach
search_provider = AzureAISearchContextProvider(
source_id="search_provider",
endpoint=search_endpoint,
api_key=search_key,
credential=AzureCliCredential() if not search_key else None,
@@ -91,6 +92,7 @@ async def main() -> None:
if not azure_openai_resource_url:
raise ValueError("AZURE_OPENAI_RESOURCE_URL required when using index_name")
search_provider = AzureAISearchContextProvider(
source_id="search_provider",
endpoint=search_endpoint,
index_name=index_name,
api_key=search_key,
@@ -53,6 +53,7 @@ async def main() -> None:
# Create Azure AI Search context provider with semantic mode (recommended, fast)
print("Using SEMANTIC mode (hybrid search + semantic ranking, fast)\n")
search_provider = AzureAISearchContextProvider(
source_id="search_provider",
endpoint=search_endpoint,
index_name=index_name,
api_key=search_key, # Use api_key for API key auth, or credential for managed identity
@@ -39,7 +39,7 @@ async def main() -> None:
name="FriendlyAssistant",
instructions="You are a friendly assistant.",
tools=retrieve_company_report,
context_providers=[Mem0ContextProvider(user_id=user_id)],
context_providers=[Mem0ContextProvider(source_id="mem0", user_id=user_id)],
) as agent,
):
# First ask the agent to retrieve a company report with no previous context.
@@ -10,7 +10,9 @@ from azure.identity.aio import AzureCliCredential
from mem0 import AsyncMemory
# 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.
# 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 retrieve_company_report(company_code: str, detailed: bool) -> str:
if company_code != "CNTS":
@@ -42,7 +44,7 @@ async def main() -> None:
name="FriendlyAssistant",
instructions="You are a friendly assistant.",
tools=retrieve_company_report,
context_providers=[Mem0ContextProvider(user_id=user_id, mem0_client=local_mem0_client)],
context_providers=[Mem0ContextProvider(source_id="mem0", user_id=user_id, mem0_client=local_mem0_client)],
) as agent,
):
# First ask the agent to retrieve a company report with no previous context.
@@ -34,11 +34,14 @@ async def example_global_thread_scope() -> None:
name="GlobalMemoryAssistant",
instructions="You are an assistant that remembers user preferences across conversations.",
tools=get_user_preferences,
context_providers=[Mem0ContextProvider(
user_id=user_id,
thread_id=global_thread_id,
scope_to_per_operation_thread_id=False, # Share memories across all sessions
)],
context_providers=[
Mem0ContextProvider(
source_id="mem0",
user_id=user_id,
thread_id=global_thread_id,
scope_to_per_operation_thread_id=False, # Share memories across all sessions
)
],
) as global_agent,
):
# Store some preferences in the global scope
@@ -72,10 +75,13 @@ async def example_per_operation_thread_scope() -> None:
name="ScopedMemoryAssistant",
instructions="You are an assistant with thread-scoped memory.",
tools=get_user_preferences,
context_providers=[Mem0ContextProvider(
user_id=user_id,
scope_to_per_operation_thread_id=True, # Isolate memories per session
)],
context_providers=[
Mem0ContextProvider(
source_id="mem0",
user_id=user_id,
scope_to_per_operation_thread_id=True, # Isolate memories per session
)
],
) as scoped_agent,
):
# Create a specific session for this scoped provider
@@ -119,16 +125,22 @@ async def example_multiple_agents() -> None:
AzureAIAgentClient(credential=credential).as_agent(
name="PersonalAssistant",
instructions="You are a personal assistant that helps with personal tasks.",
context_providers=[Mem0ContextProvider(
agent_id=agent_id_1,
)],
context_providers=[
Mem0ContextProvider(
source_id="mem0",
agent_id=agent_id_1,
)
],
) as personal_agent,
AzureAIAgentClient(credential=credential).as_agent(
name="WorkAssistant",
instructions="You are a work assistant that helps with professional tasks.",
context_providers=[Mem0ContextProvider(
agent_id=agent_id_2,
)],
context_providers=[
Mem0ContextProvider(
source_id="mem0",
agent_id=agent_id_2,
)
],
) as work_agent,
):
# Store personal information
@@ -20,7 +20,8 @@ This folder contains an example demonstrating how to use the Redis context provi
1. A running Redis with RediSearch (Redis Stack or a managed service)
2. Python environment with Agent Framework Redis extra installed
3. Optional: OpenAI API key if using vector embeddings
3. Azure AI Foundry project endpoint and Azure OpenAI Responses deployment
4. Optional: OpenAI API key if using vector embeddings
### Install the package
@@ -50,6 +51,8 @@ See quickstart: `https://learn.microsoft.com/azure/redis/quickstart-create-manag
### Environment variables
- `AZURE_AI_PROJECT_ENDPOINT` (required): Azure AI Foundry project endpoint for `AzureOpenAIResponsesClient`
- `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME` (required): Azure OpenAI Responses deployment name
- `OPENAI_API_KEY` (optional): Required only if you set `vectorizer_choice="openai"` to enable hybrid search.
### Provider configuration highlights
@@ -70,19 +73,26 @@ The provider supports both fulltext only and hybrid vector search:
2. Agent integration: teaches the agent a preference and verifies it is remembered across turns.
3. Agent + tool: calls a sample tool (flight search) and then asks the agent to recall details remembered from the tool output.
It uses OpenAI for both chat (via `OpenAIChatClient`) and, in some steps, optional embeddings for hybrid search.
It uses `AzureOpenAIResponsesClient` (Foundry project endpoint setup) for chat and, in some steps, optional OpenAI embeddings for hybrid search.
## How to run
1) Start Redis (see options above). For local default, ensure it's reachable at `redis://localhost:6379`.
2) Set your OpenAI key if using embeddings and for the chat client used in the sample:
2) Set Azure Foundry/OpenAI responses environment variables:
```bash
export AZURE_AI_PROJECT_ENDPOINT="https://<resource>.services.ai.azure.com/api/projects/<project>"
export AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME="<deployment-name>"
```
3) (Optional) Set your OpenAI key if using embeddings:
```bash
export OPENAI_API_KEY="<your key>"
```
3) Run the example:
4) Run the example:
```bash
python redis_basics.py
@@ -109,5 +119,6 @@ You should see the agent responses and, when using embeddings, context retrieved
## Troubleshooting
- Ensure at least one of `application_id`, `agent_id`, `user_id`, or `thread_id` is set; the provider requires a scope.
- Verify `AZURE_AI_PROJECT_ENDPOINT` and `AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME` are set for the chat client.
- If using embeddings, verify `OPENAI_API_KEY` is set and reachable.
- Make sure Redis exposes RediSearch (Redis Stack image or managed service with search enabled).
@@ -13,24 +13,25 @@ Requirements:
Environment Variables:
- AZURE_REDIS_HOST: Your Azure Managed Redis host (e.g., myredis.redis.cache.windows.net)
- OPENAI_API_KEY: Your OpenAI API key
- OPENAI_CHAT_MODEL_ID: OpenAI model (e.g., gpt-4o-mini)
- AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint
- AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME: Azure OpenAI Responses deployment name
- AZURE_USER_OBJECT_ID: Your Azure AD User Object ID for authentication
"""
import asyncio
import os
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.redis import RedisHistoryProvider
from azure.identity.aio import AzureCliCredential
from azure.identity import AzureCliCredential
from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential
from redis.credentials import CredentialProvider
class AzureCredentialProvider(CredentialProvider):
"""Credential provider for Azure AD authentication with Redis Enterprise."""
def __init__(self, azure_credential: AzureCliCredential, user_object_id: str):
def __init__(self, azure_credential: AsyncAzureCliCredential, user_object_id: str):
self.azure_credential = azure_credential
self.user_object_id = user_object_id
@@ -57,24 +58,26 @@ async def main() -> None:
return
# Create Azure CLI credential provider (uses 'az login' credentials)
azure_credential = AzureCliCredential()
azure_credential = AsyncAzureCliCredential()
credential_provider = AzureCredentialProvider(azure_credential, user_object_id)
session_id = "azure_test_session"
# Create Azure Redis history provider
history_provider = RedisHistoryProvider(
source_id="redis_memory",
credential_provider=credential_provider,
host=redis_host,
port=10000,
ssl=True,
thread_id=session_id,
key_prefix="chat_messages",
max_messages=100,
)
# Create chat client
client = OpenAIChatClient()
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create agent with Azure Redis history provider
agent = client.as_agent(
@@ -31,13 +31,16 @@ import asyncio
import os
from agent_framework import Message, tool
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.redis import RedisContextProvider
from azure.identity import AzureCliCredential
from redisvl.extensions.cache.embeddings import EmbeddingsCache
from redisvl.utils.vectorize import OpenAITextVectorizer
# 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.
# 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 search_flights(origin_airport_code: str, destination_airport_code: str, detailed: bool = False) -> str:
"""Simulated flight-search tool to demonstrate tool memory.
@@ -88,6 +91,15 @@ def search_flights(origin_airport_code: str, destination_airport_code: str, deta
)
def create_chat_client() -> AzureOpenAIResponsesClient:
"""Create an Azure OpenAI Responses client using a Foundry project endpoint."""
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
async def main() -> None:
"""Walk through provider-only, agent integration, and tool-memory scenarios.
@@ -100,8 +112,8 @@ async def main() -> None:
print("-" * 40)
# Create a provider with partition scope and OpenAI embeddings
# Please set the OPENAI_API_KEY and OPENAI_CHAT_MODEL_ID environment variables to use the OpenAI vectorizer
# Recommend default for OPENAI_CHAT_MODEL_ID is gpt-4o-mini
# Please set OPENAI_API_KEY to use the OpenAI vectorizer.
# For chat responses, also set AZURE_AI_PROJECT_ENDPOINT and AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME.
# We attach an embedding vectorizer so the provider can perform hybrid (text + vector)
# retrieval. If you prefer text-only retrieval, instantiate RedisContextProvider without the
@@ -115,6 +127,7 @@ async def main() -> None:
# scope data for multi-tenant separation; thread_id (set later) narrows to a
# specific conversation.
provider = RedisContextProvider(
source_id="redis_context",
redis_url="redis://localhost:6379",
index_name="redis_basics",
application_id="matrix_of_kermits",
@@ -170,6 +183,7 @@ async def main() -> None:
)
# Recreate a clean index so the next scenario starts fresh
provider = RedisContextProvider(
source_id="redis_context",
redis_url="redis://localhost:6379",
index_name="redis_basics_2",
prefix="context_2",
@@ -183,7 +197,7 @@ async def main() -> None:
)
# Create chat client for the agent
client = OpenAIChatClient(model_id=os.getenv("OPENAI_CHAT_MODEL_ID"), api_key=os.getenv("OPENAI_API_KEY"))
client = create_chat_client()
# Create agent wired to the Redis context provider. The provider automatically
# persists conversational details and surfaces relevant context on each turn.
agent = client.as_agent(
@@ -217,6 +231,7 @@ async def main() -> None:
print("-" * 40)
# Text-only provider (full-text search only). Omits vectorizer and related params.
provider = RedisContextProvider(
source_id="redis_context",
redis_url="redis://localhost:6379",
index_name="redis_basics_3",
prefix="context_3",
@@ -227,7 +242,7 @@ async def main() -> None:
# Create agent exposing the flight search tool. Tool outputs are captured by the
# provider and become retrievable context for later turns.
client = OpenAIChatClient(model_id=os.getenv("OPENAI_CHAT_MODEL_ID"), api_key=os.getenv("OPENAI_API_KEY"))
client = create_chat_client()
agent = client.as_agent(
name="MemoryEnhancedAssistant",
instructions=(
@@ -17,8 +17,9 @@ Run:
import asyncio
import os
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.redis import RedisContextProvider
from azure.identity import AzureCliCredential
from redisvl.extensions.cache.embeddings import EmbeddingsCache
from redisvl.utils.vectorize import OpenAITextVectorizer
@@ -36,9 +37,8 @@ async def main() -> None:
cache=EmbeddingsCache(name="openai_embeddings_cache", redis_url="redis://localhost:6379"),
)
session_id = "test_session"
provider = RedisContextProvider(
source_id="redis_context",
redis_url="redis://localhost:6379",
index_name="redis_conversation",
prefix="redis_conversation",
@@ -49,11 +49,14 @@ async def main() -> None:
vector_field_name="vector",
vector_algorithm="hnsw",
vector_distance_metric="cosine",
thread_id=session_id,
)
# Create chat client for the agent
client = OpenAIChatClient(model_id=os.getenv("OPENAI_CHAT_MODEL_ID"), api_key=os.getenv("OPENAI_API_KEY"))
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create agent wired to the Redis context provider. The provider automatically
# persists conversational details and surfaces relevant context on each turn.
agent = client.as_agent(
@@ -28,15 +28,24 @@ Run:
import asyncio
import os
import uuid
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.redis import RedisContextProvider
from azure.identity import AzureCliCredential
from redisvl.extensions.cache.embeddings import EmbeddingsCache
from redisvl.utils.vectorize import OpenAITextVectorizer
# Please set the OPENAI_API_KEY and OPENAI_CHAT_MODEL_ID environment variables to use the OpenAI vectorizer
# Recommend default for OPENAI_CHAT_MODEL_ID is gpt-4o-mini
# Please set OPENAI_API_KEY to use the OpenAI vectorizer.
# For chat responses, also set AZURE_AI_PROJECT_ENDPOINT and AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME.
def create_chat_client() -> AzureOpenAIResponsesClient:
"""Create an Azure OpenAI Responses client using a Foundry project endpoint."""
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
async def example_global_thread_scope() -> None:
@@ -44,20 +53,15 @@ async def example_global_thread_scope() -> None:
print("1. Global Thread Scope Example:")
print("-" * 40)
global_thread_id = str(uuid.uuid4())
client = OpenAIChatClient(
model_id=os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-4o-mini"),
api_key=os.getenv("OPENAI_API_KEY"),
)
client = create_chat_client()
provider = RedisContextProvider(
source_id="redis_context",
redis_url="redis://localhost:6379",
index_name="redis_threads_global",
application_id="threads_demo_app",
agent_id="threads_demo_agent",
user_id="threads_demo_user",
thread_id=global_thread_id,
scope_to_per_operation_thread_id=False, # Share memories across all sessions
)
@@ -97,10 +101,7 @@ async def example_per_operation_thread_scope() -> None:
print("2. Per-Operation Thread Scope Example:")
print("-" * 40)
client = OpenAIChatClient(
model_id=os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-4o-mini"),
api_key=os.getenv("OPENAI_API_KEY"),
)
client = create_chat_client()
vectorizer = OpenAITextVectorizer(
model="text-embedding-ada-002",
@@ -109,6 +110,7 @@ async def example_per_operation_thread_scope() -> None:
)
provider = RedisContextProvider(
source_id="redis_context",
redis_url="redis://localhost:6379",
index_name="redis_threads_dynamic",
# overwrite_redis_index=True,
@@ -165,10 +167,7 @@ async def example_multiple_agents() -> None:
print("3. Multiple Agents with Different Thread Configurations:")
print("-" * 40)
client = OpenAIChatClient(
model_id=os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-4o-mini"),
api_key=os.getenv("OPENAI_API_KEY"),
)
client = create_chat_client()
vectorizer = OpenAITextVectorizer(
model="text-embedding-ada-002",
@@ -177,6 +176,7 @@ async def example_multiple_agents() -> None:
)
personal_provider = RedisContextProvider(
source_id="redis_context",
redis_url="redis://localhost:6379",
index_name="redis_threads_agents",
application_id="threads_demo_app",
@@ -195,6 +195,7 @@ async def example_multiple_agents() -> None:
)
work_provider = RedisContextProvider(
source_id="redis_context",
redis_url="redis://localhost:6379",
index_name="redis_threads_agents",
application_id="threads_demo_app",
@@ -1,11 +1,13 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from contextlib import suppress
from typing import Any
from agent_framework import Agent, AgentSession, BaseContextProvider, SessionContext, SupportsChatGetResponse
from agent_framework.azure import AzureAIClient
from azure.identity.aio import AzureCliCredential
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from pydantic import BaseModel
@@ -15,19 +17,13 @@ class UserInfo(BaseModel):
class UserInfoMemory(BaseContextProvider):
def __init__(self, client: SupportsChatGetResponse, user_info: UserInfo | None = None, **kwargs: Any):
def __init__(self, source_id: str = "user-info-memory", *, client: SupportsChatGetResponse, **kwargs: Any):
"""Create the memory.
If you pass in kwargs, they will be attempted to be used to create a UserInfo object.
"""
super().__init__("user-info-memory")
super().__init__(source_id)
self._chat_client = client
if user_info:
self.user_info = user_info
elif kwargs:
self.user_info = UserInfo.model_validate(kwargs)
else:
self.user_info = UserInfo()
async def after_run(
self,
@@ -38,12 +34,15 @@ class UserInfoMemory(BaseContextProvider):
state: dict[str, Any],
) -> None:
"""Extract user information from messages after each agent call."""
request_messages = context.get_messages()
# ensure you get all the messages you want to parse from, including the input in this case.
request_messages = context.get_messages(include_input=True, include_response=True)
# Check if we need to extract user info from user messages
user_messages = [msg for msg in request_messages if hasattr(msg, "role") and msg.role == "user"] # type: ignore
if (self.user_info.name is None or self.user_info.age is None) and user_messages:
try:
if (
state[self.source_id]["user_info"].name is None or state[self.source_id]["user_info"].age is None
) and user_messages:
with suppress(Exception):
# Use the chat client to extract structured information
result = await self._chat_client.get_response(
messages=request_messages, # type: ignore
@@ -53,17 +52,12 @@ class UserInfoMemory(BaseContextProvider):
)
# Update user info with extracted data
try:
with suppress(Exception):
extracted = result.value
if self.user_info.name is None and extracted.name:
self.user_info.name = extracted.name
if self.user_info.age is None and extracted.age:
self.user_info.age = extracted.age
except Exception:
pass # Failed to extract, continue without updating
except Exception:
pass # Failed to extract, continue without updating
if state[self.source_id]["user_info"].name is None and extracted.name:
state[self.source_id]["user_info"].name = extracted.name
if state[self.source_id]["user_info"].age is None and extracted.age:
state[self.source_id]["user_info"].age = extracted.age
async def before_run(
self,
@@ -74,55 +68,52 @@ class UserInfoMemory(BaseContextProvider):
state: dict[str, Any],
) -> None:
"""Provide user information context before each agent call."""
instructions: list[str] = []
if state.setdefault(self.source_id, None) is None:
state[self.source_id] = {"user_info": UserInfo()}
if self.user_info.name is None:
instructions.append(
"Ask the user for their name and politely decline to answer any questions until they provide it."
)
else:
instructions.append(f"The user's name is {self.user_info.name}.")
if self.user_info.age is None:
instructions.append(
"Ask the user for their age and politely decline to answer any questions until they provide it."
)
else:
instructions.append(f"The user's age is {self.user_info.age}.")
# Add context with additional instructions
context.extend_instructions(self.source_id, " ".join(instructions))
def serialize(self) -> str:
"""Serialize the user info for session persistence."""
return self.user_info.model_dump_json()
context.extend_instructions(
self.source_id,
"Ask the user for their name and politely decline to answer any questions until they provide it."
if state[self.source_id]["user_info"].name is None
else f"The user's name is {state[self.source_id]['user_info'].name}.",
)
context.extend_instructions(
self.source_id,
"Ask the user for their age and politely decline to answer any questions until they provide it."
if state[self.source_id]["user_info"].age is None
else f"The user's age is {state[self.source_id]['user_info'].age}.",
)
async def main():
async with AzureCliCredential() as credential:
client = AzureAIClient(credential=credential)
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create the memory provider
memory_provider = UserInfoMemory(client)
context_name = "user-info-memory"
# Create the agent with memory
async with Agent(
client=client,
instructions="You are a friendly assistant. Always address the user by their name.",
context_providers=[memory_provider],
) as agent:
# Create a new session for the conversation
session = agent.create_session()
# Create the memory provider
memory_provider = UserInfoMemory(context_name, client=client)
print(await agent.run("Hello, what is the square root of 9?", session=session))
print(await agent.run("My name is Ruaidhrí", session=session))
print(await agent.run("I am 20 years old", session=session))
# Create the agent with memory
async with Agent(
client=client,
instructions="You are a friendly assistant. Always address the user by their name.",
context_providers=[memory_provider],
) as agent:
# Create a new session for the conversation
session = agent.create_session()
# Access the memory component and inspect the memories
if memory_provider:
print()
print(f"MEMORY - User Name: {memory_provider.user_info.name}")
print(f"MEMORY - User Age: {memory_provider.user_info.age}")
for msg in ["Hello, what is the square root of 9?", "My name is Ruaidhrí", "I am 20 years old"]:
print(f"User: {msg}")
print(f"Assistant: {await agent.run(msg, session=session)}")
# Access the memory component and inspect the memories
print()
print(f"MEMORY - User Name: {session.state[context_name]['user_info'].name}")
print(f"MEMORY - User Age: {session.state[context_name]['user_info'].age}")
if __name__ == "__main__":
@@ -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`.
@@ -7,7 +7,7 @@ import asyncio
from random import randint
from typing import TYPE_CHECKING, Annotated
from agent_framework_foundry_local import FoundryLocalClient
from agent_framework.microsoft import FoundryLocalClient
if TYPE_CHECKING:
from agent_framework import Agent
@@ -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:
+3 -5
View File
@@ -3,7 +3,7 @@
import asyncio
from collections.abc import AsyncIterable, Sequence
from agent_framework import ChatResponse, ChatResponseUpdate, Content, ResponseStream, Role
from agent_framework import ChatResponse, ChatResponseUpdate, Content, Message, ResponseStream
"""ResponseStream: A Deep Dive
@@ -256,8 +256,7 @@ async def main() -> None:
"""Result hook that wraps the response text in quotes."""
if response.text:
return ChatResponse(
messages=f'"{response.text}"',
role=Role.ASSISTANT,
messages=[Message(text=f'"{response.text}"', role="assistant")],
additional_properties=response.additional_properties,
)
return response
@@ -294,8 +293,7 @@ async def main() -> None:
# In real code, this would create an AgentResponse
text = "".join(u.text or "" for u in updates)
return ChatResponse(
text=f"[AGENT FINAL] {text}",
role=Role.ASSISTANT,
messages=[Message(text=f"[AGENT FINAL] {text}", role="assistant")],
additional_properties={"layer": "agent"},
)
+7 -4
View File
@@ -22,6 +22,11 @@ which provides:
The sample shows usage with both OpenAI and Anthropic clients, demonstrating
how provider-specific options work for ChatClient and Agent. But the same approach works for other providers too.
The following environment variables are used:
- ANTHROPIC_API_KEY=...
- OPENAI_API_KEY=...
"""
@@ -109,14 +114,13 @@ async def demo_openai_chat_client_reasoning_models() -> None:
print("\n=== OpenAI ChatClient with TypedDict Options ===\n")
# Create OpenAI client
client = OpenAIChatClient[OpenAIReasoningChatOptions]()
client = OpenAIChatClient[OpenAIReasoningChatOptions](model_id="o3")
# With specific options, you get full IDE autocomplete!
# Try typing `client.get_response("Hello", options={` and see the suggestions
response = await client.get_response(
"What is 2 + 2?",
options={
"model_id": "o3",
"max_tokens": 100,
"allow_multiple_tool_calls": True,
# OpenAI-specific options work:
@@ -140,12 +144,11 @@ async def demo_openai_agent() -> None:
# or on the client when constructing the client instance:
# client = OpenAIChatClient[OpenAIReasoningChatOptions]()
agent = Agent[OpenAIReasoningChatOptions](
client=OpenAIChatClient(),
client=OpenAIChatClient(model_id="o3"),
name="weather-assistant",
instructions="You are a helpful assistant. Answer concisely.",
# Options can be set at construction time
default_options={
"model_id": "o3",
"max_tokens": 100,
"allow_multiple_tool_calls": True,
# OpenAI-specific options work:
+11 -5
View File
@@ -343,11 +343,14 @@ all = [
{ name = "agent-framework-azure-ai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-azure-ai-search", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-azurefunctions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-bedrock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-chatkit", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-claude", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-copilotstudio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-declarative", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-devui", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-durabletask", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-foundry-local", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-github-copilot", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-lab", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "agent-framework-mem0", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -365,11 +368,14 @@ requires-dist = [
{ name = "agent-framework-azure-ai", marker = "extra == 'all'", editable = "packages/azure-ai" },
{ name = "agent-framework-azure-ai-search", marker = "extra == 'all'", editable = "packages/azure-ai-search" },
{ name = "agent-framework-azurefunctions", marker = "extra == 'all'", editable = "packages/azurefunctions" },
{ name = "agent-framework-bedrock", marker = "extra == 'all'", editable = "packages/bedrock" },
{ name = "agent-framework-chatkit", marker = "extra == 'all'", editable = "packages/chatkit" },
{ name = "agent-framework-claude", marker = "extra == 'all'", editable = "packages/claude" },
{ name = "agent-framework-copilotstudio", marker = "extra == 'all'", editable = "packages/copilotstudio" },
{ name = "agent-framework-declarative", marker = "extra == 'all'", editable = "packages/declarative" },
{ name = "agent-framework-devui", marker = "extra == 'all'", editable = "packages/devui" },
{ name = "agent-framework-durabletask", marker = "extra == 'all'", editable = "packages/durabletask" },
{ name = "agent-framework-foundry-local", marker = "extra == 'all'", editable = "packages/foundry_local" },
{ name = "agent-framework-github-copilot", marker = "extra == 'all'", editable = "packages/github_copilot" },
{ name = "agent-framework-lab", marker = "extra == 'all'", editable = "packages/lab" },
{ name = "agent-framework-mem0", marker = "extra == 'all'", editable = "packages/mem0" },
@@ -1356,7 +1362,7 @@ name = "clr-loader"
version = "0.2.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" }
wheels = [
@@ -1835,7 +1841,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" },
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -4571,8 +4577,8 @@ name = "powerfx"
version = "0.0.34"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
{ name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" }
wheels = [
@@ -5221,7 +5227,7 @@ name = "pythonnet"
version = "3.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" }
wheels = [