From 2c09ab81a9d1623933708b4b1b427c4d16fd8a36 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 16 Feb 2026 14:22:59 +0100 Subject: [PATCH 1/5] 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> --- python/packages/bedrock/AGENTS.md | 4 +- python/packages/bedrock/README.md | 2 +- .../agent_framework_bedrock/_chat_client.py | 2 +- python/packages/bedrock/samples/__init__.py | 0 .../bedrock/samples/bedrock_sample.py | 45 ----- .../packages/core/agent_framework/_agents.py | 69 ++------ .../packages/core/agent_framework/_clients.py | 9 +- .../packages/core/agent_framework/_tools.py | 107 ++++++------ .../packages/core/agent_framework/_types.py | 61 ++----- .../core/agent_framework/amazon/__init__.py | 23 +++ .../openai/_assistant_provider.py | 29 ++-- .../openai/_assistants_client.py | 33 ++-- .../agent_framework/openai/_chat_client.py | 11 +- python/packages/core/pyproject.toml | 1 + .../core/test_function_invocation_logic.py | 30 ++++ python/packages/core/tests/core/test_types.py | 4 +- .../openai/test_openai_assistants_client.py | 47 ++++++ .../tests/openai/test_openai_chat_client.py | 15 ++ .../samples/01-get-started/01_hello_agent.py | 2 + python/samples/01-get-started/README.md | 6 +- .../samples/02-agents/chat_client/README.md | 67 ++++++-- .../chat_client/azure_ai_chat_client.py | 49 ------ .../chat_client/azure_assistants_client.py | 49 ------ .../chat_client/azure_chat_client.py | 49 ------ .../chat_client/azure_responses_client.py | 95 ----------- .../chat_client/built_in_chat_clients.py | 156 ++++++++++++++++++ .../chat_client/custom_chat_client.py | 55 +++--- .../chat_client/openai_assistants_client.py | 47 ------ .../chat_client/openai_chat_client.py | 47 ------ .../chat_client/openai_responses_client.py | 47 ------ .../azure_ai_with_search_context_agentic.py | 2 + .../azure_ai_with_search_context_semantic.py | 1 + .../context_providers/mem0/mem0_basic.py | 2 +- .../context_providers/mem0/mem0_oss.py | 6 +- .../context_providers/mem0/mem0_sessions.py | 42 +++-- .../context_providers/redis/README.md | 19 ++- .../redis/azure_redis_conversation.py | 23 +-- .../context_providers/redis/redis_basics.py | 27 ++- .../redis/redis_conversation.py | 13 +- .../context_providers/redis/redis_sessions.py | 39 ++--- .../simple_context_provider.py | 129 +++++++-------- python/samples/02-agents/providers/README.md | 18 ++ .../02-agents/providers/amazon/README.md | 17 ++ .../providers/amazon/bedrock_chat_client.py | 61 +++++++ python/samples/02-agents/response_stream.py | 8 +- python/samples/02-agents/typed_options.py | 11 +- python/uv.lock | 12 +- 47 files changed, 783 insertions(+), 808 deletions(-) delete mode 100644 python/packages/bedrock/samples/__init__.py delete mode 100644 python/packages/bedrock/samples/bedrock_sample.py create mode 100644 python/packages/core/agent_framework/amazon/__init__.py delete mode 100644 python/samples/02-agents/chat_client/azure_ai_chat_client.py delete mode 100644 python/samples/02-agents/chat_client/azure_assistants_client.py delete mode 100644 python/samples/02-agents/chat_client/azure_chat_client.py delete mode 100644 python/samples/02-agents/chat_client/azure_responses_client.py create mode 100644 python/samples/02-agents/chat_client/built_in_chat_clients.py delete mode 100644 python/samples/02-agents/chat_client/openai_assistants_client.py delete mode 100644 python/samples/02-agents/chat_client/openai_chat_client.py delete mode 100644 python/samples/02-agents/chat_client/openai_responses_client.py create mode 100644 python/samples/02-agents/providers/README.md create mode 100644 python/samples/02-agents/providers/amazon/README.md create mode 100644 python/samples/02-agents/providers/amazon/bedrock_chat_client.py diff --git a/python/packages/bedrock/AGENTS.md b/python/packages/bedrock/AGENTS.md index 69c0a1a692..0ab072ed24 100644 --- a/python/packages/bedrock/AGENTS.md +++ b/python/packages/bedrock/AGENTS.md @@ -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 ``` diff --git a/python/packages/bedrock/README.md b/python/packages/bedrock/README.md index 6bcd9ff53a..0e49f8c5cf 100644 --- a/python/packages/bedrock/README.md +++ b/python/packages/bedrock/README.md @@ -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 script](../../samples/02-agents/providers/bedrock/bedrock_chat_client.py) for a runnable end-to-end script that: - Loads credentials from the `BEDROCK_*` environment variables - Instantiates `BedrockChatClient` diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index 73bf9cc428..334cd25248 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -260,7 +260,7 @@ def __init__( 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="") diff --git a/python/packages/bedrock/samples/__init__.py b/python/packages/bedrock/samples/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/packages/bedrock/samples/bedrock_sample.py b/python/packages/bedrock/samples/bedrock_sample.py deleted file mode 100644 index 188e6bf1da..0000000000 --- a/python/packages/bedrock/samples/bedrock_sample.py +++ /dev/null @@ -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 "") - 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()) diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index 0b1b20bed8..d64bb8ced4 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -37,6 +37,8 @@ from ._tools import ( FunctionInvocationLayer, FunctionTool, + ToolTypes, + normalize_tools, ) from ._types import ( AgentResponse, @@ -614,12 +616,7 @@ def __init__( 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 @@ def __init__( # 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 @@ def run( *, 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 @@ def run( *, 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 @@ def run( *, 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 @@ def run( *, 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 @@ async def _prepare_run_context( *, 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 @@ async def _prepare_run_context( ) # 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 @@ def __init__( 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, diff --git a/python/packages/core/agent_framework/_clients.py b/python/packages/core/agent_framework/_clients.py index c8956a67e3..b407be11cf 100644 --- a/python/packages/core/agent_framework/_clients.py +++ b/python/packages/core/agent_framework/_clients.py @@ -10,7 +10,6 @@ Awaitable, Callable, Mapping, - MutableMapping, Sequence, ) from typing import ( @@ -31,7 +30,7 @@ from ._serialization import SerializationMixin from ._tools import ( FunctionInvocationConfiguration, - FunctionTool, + ToolTypes, ) from ._types import ( ChatResponse, @@ -436,11 +435,7 @@ def as_agent( 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, diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 8f941b41e0..b9a4d0a541 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -12,7 +12,6 @@ Awaitable, Callable, Mapping, - MutableMapping, Sequence, ) from functools import partial, wraps @@ -58,6 +57,7 @@ if TYPE_CHECKING: from ._clients import SupportsChatGetResponse + from ._mcp import MCPTool from ._middleware import FunctionMiddlewarePipeline, FunctionMiddlewareTypes from ._types import ( ChatOptions, @@ -69,6 +69,8 @@ ) ResponseModelBoundT = TypeVar("ResponseModelBoundT", bound=BaseModel) +else: + MCPTool = Any # type: ignore[assignment,misc] logger = logging.getLogger("agent_framework") @@ -506,9 +508,7 @@ async def invoke( 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 +623,46 @@ def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) return as_dict +ToolTypes = 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 +672,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 +1450,12 @@ async def final_function_handler(context_obj: Any) -> Any: 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 +1463,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 +1642,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 +2006,11 @@ def get_response( # 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: diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index a3786e8838..fb43b4e9e4 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -15,7 +15,8 @@ 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 my_tool(x: int) -> int: # 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 @@ def my_tool(x: int) -> int: 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 @@ def my_tool(x: int) -> int: 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}") diff --git a/python/packages/core/agent_framework/amazon/__init__.py b/python/packages/core/agent_framework/amazon/__init__.py new file mode 100644 index 0000000000..21ee049f9b --- /dev/null +++ b/python/packages/core/agent_framework/amazon/__init__.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft. All rights reserved. + +import importlib +from typing import Any + +IMPORT_PATH = "agent_framework_bedrock" +PACKAGE_NAME = "agent-framework-bedrock" +_IMPORTS = ["__version__", "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 diff --git a/python/packages/core/agent_framework/openai/_assistant_provider.py b/python/packages/core/agent_framework/openai/_assistant_provider.py index 6b6c2ef687..66f477dcc1 100644 --- a/python/packages/core/agent_framework/openai/_assistant_provider.py +++ b/python/packages/core/agent_framework/openai/_assistant_provider.py @@ -15,8 +15,7 @@ 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 @@ 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 @@ async def create_agent( 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 @@ async def create_agent( """ # 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 @@ async def get_agent( 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 @@ def as_agent( 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 @@ def as_agent( 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 @@ def _validate_function_tools( 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 @@ def _merge_tools( 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 @@ def _merge_tools( 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, diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py index 6135f9cac2..49ba32166e 100644 --- a/python/packages/core/agent_framework/openai/_assistants_client.py +++ b/python/packages/core/agent_framework/openai/_assistants_client.py @@ -36,6 +36,7 @@ FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, + normalize_tools, ) from .._types import ( ChatOptions, @@ -686,26 +687,26 @@ def _prepare_options( 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): diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index ee8f60999c..16cbec3afb 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -27,6 +27,8 @@ FunctionInvocationConfiguration, FunctionInvocationLayer, FunctionTool, + ToolTypes, + normalize_tools, ) from .._types import ( ChatOptions, @@ -271,21 +273,24 @@ async def _get_response() -> ChatResponse: # 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": diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml index bb3bb394b9..c62aa26da8 100644 --- a/python/packages/core/pyproject.toml +++ b/python/packages/core/pyproject.toml @@ -47,6 +47,7 @@ all = [ "agent-framework-anthropic", "agent-framework-azure-ai", "agent-framework-azurefunctions", + "agent-framework-bedrock", "agent-framework-chatkit", "agent-framework-copilotstudio", "agent-framework-declarative", diff --git a/python/packages/core/tests/core/test_function_invocation_logic.py b/python/packages/core/tests/core/test_function_invocation_logic.py index 593aa6f737..2e7601f787 100644 --- a/python/packages/core/tests/core/test_function_invocation_logic.py +++ b/python/packages/core/tests/core/test_function_invocation_logic.py @@ -56,6 +56,36 @@ def ai_func(arg1: str) -> str: 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 diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 7a5acdedf7..8a8885b919 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -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") diff --git a/python/packages/core/tests/openai/test_openai_assistants_client.py b/python/packages/core/tests/openai/test_openai_assistants_client.py index 80e2020d02..4521a3c693 100644 --- a/python/packages/core/tests/openai/test_openai_assistants_client.py +++ b/python/packages/core/tests/openai/test_openai_assistants_client.py @@ -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_function(query: str) -> str: 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) diff --git a/python/packages/core/tests/openai/test_openai_chat_client.py b/python/packages/core/tests/openai/test_openai_chat_client.py index c5ee81bfce..a6482367e9 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client.py +++ b/python/packages/core/tests/openai/test_openai_chat_client.py @@ -190,6 +190,21 @@ class UnsupportedTool: 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.""" diff --git a/python/samples/01-get-started/01_hello_agent.py b/python/samples/01-get-started/01_hello_agent.py index a3353a5e87..45b0cf410a 100644 --- a/python/samples/01-get-started/01_hello_agent.py +++ b/python/samples/01-get-started/01_hello_agent.py @@ -12,6 +12,8 @@ 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) diff --git a/python/samples/01-get-started/README.md b/python/samples/01-get-started/README.md index 774368e15f..5ba119e016 100644 --- a/python/samples/01-get-started/README.md +++ b/python/samples/01-get-started/README.md @@ -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) diff --git a/python/samples/02-agents/chat_client/README.md b/python/samples/02-agents/chat_client/README.md index 5bf9b471ad..e03d532812 100644 --- a/python/samples/02-agents/chat_client/README.md +++ b/python/samples/02-agents/chat_client/README.md @@ -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 Anthropic client (`anthropic`):** +- `ANTHROPIC_API_KEY`: Your Anthropic API key +- `ANTHROPIC_CHAT_MODEL_ID`: The Anthropic model ID (for example, `claude-sonnet-4-5`) -**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 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`) -> **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 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`) diff --git a/python/samples/02-agents/chat_client/azure_ai_chat_client.py b/python/samples/02-agents/chat_client/azure_ai_chat_client.py deleted file mode 100644 index 236d93f1a7..0000000000 --- a/python/samples/02-agents/chat_client/azure_ai_chat_client.py +++ /dev/null @@ -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()) diff --git a/python/samples/02-agents/chat_client/azure_assistants_client.py b/python/samples/02-agents/chat_client/azure_assistants_client.py deleted file mode 100644 index 66034e8eee..0000000000 --- a/python/samples/02-agents/chat_client/azure_assistants_client.py +++ /dev/null @@ -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()) diff --git a/python/samples/02-agents/chat_client/azure_chat_client.py b/python/samples/02-agents/chat_client/azure_chat_client.py deleted file mode 100644 index 675df29774..0000000000 --- a/python/samples/02-agents/chat_client/azure_chat_client.py +++ /dev/null @@ -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()) diff --git a/python/samples/02-agents/chat_client/azure_responses_client.py b/python/samples/02-agents/chat_client/azure_responses_client.py deleted file mode 100644 index 7ab4212a3a..0000000000 --- a/python/samples/02-agents/chat_client/azure_responses_client.py +++ /dev/null @@ -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()) diff --git a/python/samples/02-agents/chat_client/built_in_chat_clients.py b/python/samples/02-agents/chat_client/built_in_chat_clients.py new file mode 100644 index 0000000000..06be683781 --- /dev/null +++ b/python/samples/02-agents/chat_client/built_in_chat_clients.py @@ -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. +""" diff --git a/python/samples/02-agents/chat_client/custom_chat_client.py b/python/samples/02-agents/chat_client/custom_chat_client.py index b6c69bd0ac..ae009c059a 100644 --- a/python/samples/02-agents/chat_client/custom_chat_client.py +++ b/python/samples/02-agents/chat_client/custom_chat_client.py @@ -4,7 +4,7 @@ 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 @@ 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 @@ """ -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 @@ def _inner_get_response( # 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 @@ def _inner_get_response( 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 @@ async def _stream() -> AsyncIterable[ChatResponseUpdate]: 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 diff --git a/python/samples/02-agents/chat_client/openai_assistants_client.py b/python/samples/02-agents/chat_client/openai_assistants_client.py deleted file mode 100644 index 7783743950..0000000000 --- a/python/samples/02-agents/chat_client/openai_assistants_client.py +++ /dev/null @@ -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()) diff --git a/python/samples/02-agents/chat_client/openai_chat_client.py b/python/samples/02-agents/chat_client/openai_chat_client.py deleted file mode 100644 index e784c17ae2..0000000000 --- a/python/samples/02-agents/chat_client/openai_chat_client.py +++ /dev/null @@ -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()) diff --git a/python/samples/02-agents/chat_client/openai_responses_client.py b/python/samples/02-agents/chat_client/openai_responses_client.py deleted file mode 100644 index ba589e1c2f..0000000000 --- a/python/samples/02-agents/chat_client/openai_responses_client.py +++ /dev/null @@ -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()) diff --git a/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py b/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py index 5a4503f920..02bac618df 100644 --- a/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py +++ b/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_agentic.py @@ -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, diff --git a/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py b/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py index 8309d5197c..e9763531fb 100644 --- a/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py +++ b/python/samples/02-agents/context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py @@ -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 diff --git a/python/samples/02-agents/context_providers/mem0/mem0_basic.py b/python/samples/02-agents/context_providers/mem0/mem0_basic.py index b4e99e0a9f..773443f2be 100644 --- a/python/samples/02-agents/context_providers/mem0/mem0_basic.py +++ b/python/samples/02-agents/context_providers/mem0/mem0_basic.py @@ -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. diff --git a/python/samples/02-agents/context_providers/mem0/mem0_oss.py b/python/samples/02-agents/context_providers/mem0/mem0_oss.py index 1b03ac5fc1..8bcedc5214 100644 --- a/python/samples/02-agents/context_providers/mem0/mem0_oss.py +++ b/python/samples/02-agents/context_providers/mem0/mem0_oss.py @@ -10,7 +10,9 @@ 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. diff --git a/python/samples/02-agents/context_providers/mem0/mem0_sessions.py b/python/samples/02-agents/context_providers/mem0/mem0_sessions.py index cc5548e979..c2f1ddd69b 100644 --- a/python/samples/02-agents/context_providers/mem0/mem0_sessions.py +++ b/python/samples/02-agents/context_providers/mem0/mem0_sessions.py @@ -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 diff --git a/python/samples/02-agents/context_providers/redis/README.md b/python/samples/02-agents/context_providers/redis/README.md index b7b25c8d77..060061c908 100644 --- a/python/samples/02-agents/context_providers/redis/README.md +++ b/python/samples/02-agents/context_providers/redis/README.md @@ -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 full‑text 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://.services.ai.azure.com/api/projects/" +export AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME="" +``` + +3) (Optional) Set your OpenAI key if using embeddings: ```bash export OPENAI_API_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). diff --git a/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py b/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py index ce569be8cb..d5e3bd6f8e 100644 --- a/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py +++ b/python/samples/02-agents/context_providers/redis/azure_redis_conversation.py @@ -13,24 +13,25 @@ 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( diff --git a/python/samples/02-agents/context_providers/redis/redis_basics.py b/python/samples/02-agents/context_providers/redis/redis_basics.py index 5f78d65320..079108bd15 100644 --- a/python/samples/02-agents/context_providers/redis/redis_basics.py +++ b/python/samples/02-agents/context_providers/redis/redis_basics.py @@ -31,13 +31,16 @@ 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=( diff --git a/python/samples/02-agents/context_providers/redis/redis_conversation.py b/python/samples/02-agents/context_providers/redis/redis_conversation.py index 2d345d9930..f25dbbbe52 100644 --- a/python/samples/02-agents/context_providers/redis/redis_conversation.py +++ b/python/samples/02-agents/context_providers/redis/redis_conversation.py @@ -17,8 +17,9 @@ 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( diff --git a/python/samples/02-agents/context_providers/redis/redis_sessions.py b/python/samples/02-agents/context_providers/redis/redis_sessions.py index 34179048d9..aa1b7501f8 100644 --- a/python/samples/02-agents/context_providers/redis/redis_sessions.py +++ b/python/samples/02-agents/context_providers/redis/redis_sessions.py @@ -28,15 +28,24 @@ 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", diff --git a/python/samples/02-agents/context_providers/simple_context_provider.py b/python/samples/02-agents/context_providers/simple_context_provider.py index fd2a7ce747..88368b40ce 100644 --- a/python/samples/02-agents/context_providers/simple_context_provider.py +++ b/python/samples/02-agents/context_providers/simple_context_provider.py @@ -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 = None, **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 @@ async def after_run( 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 @@ async def after_run( ) # 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 @@ async def before_run( state: dict[str, Any], ) -> None: """Provide user information context before each agent call.""" - instructions: list[str] = [] - - 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() + if state.setdefault(self.source_id, None) is None: + state[self.source_id] = {"user_info": UserInfo()} + + 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) - - # Create the memory provider - memory_provider = UserInfoMemory(client) - - # 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() - - 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)) - - # 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}") + client = AzureOpenAIResponsesClient( + project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], + deployment_name=os.environ["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + credential=AzureCliCredential(), + ) + + context_name = "user-info-memory" + + # Create the memory provider + memory_provider = UserInfoMemory(context_name, client=client) + + # 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() + + 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__": diff --git a/python/samples/02-agents/providers/README.md b/python/samples/02-agents/providers/README.md new file mode 100644 index 0000000000..11cb3701c2 --- /dev/null +++ b/python/samples/02-agents/providers/README.md @@ -0,0 +1,18 @@ +# 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. | +| [`bedrock/`](bedrock/) | 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. | +| [`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. diff --git a/python/samples/02-agents/providers/amazon/README.md b/python/samples/02-agents/providers/amazon/README.md new file mode 100644 index 0000000000..ab10aeecfd --- /dev/null +++ b/python/samples/02-agents/providers/amazon/README.md @@ -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`) diff --git a/python/samples/02-agents/providers/amazon/bedrock_chat_client.py b/python/samples/02-agents/providers/amazon/bedrock_chat_client.py new file mode 100644 index 0000000000..1913d55dea --- /dev/null +++ b/python/samples/02-agents/providers/amazon/bedrock_chat_client.py @@ -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. +""" diff --git a/python/samples/02-agents/response_stream.py b/python/samples/02-agents/response_stream.py index 1b26ac5e90..840dc18b26 100644 --- a/python/samples/02-agents/response_stream.py +++ b/python/samples/02-agents/response_stream.py @@ -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 @@ def wrap_in_quotes_hook(response: ChatResponse) -> ChatResponse: """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 @@ def to_agent_response(updates: Sequence[ChatResponseUpdate]) -> ChatResponse: # 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"}, ) diff --git a/python/samples/02-agents/typed_options.py b/python/samples/02-agents/typed_options.py index e111222601..58bb116055 100644 --- a/python/samples/02-agents/typed_options.py +++ b/python/samples/02-agents/typed_options.py @@ -22,6 +22,11 @@ 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: diff --git a/python/uv.lock b/python/uv.lock index 40c021172b..8ecd0b545d 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -343,6 +343,7 @@ 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-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'" }, @@ -365,6 +366,7 @@ 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-copilotstudio", marker = "extra == 'all'", editable = "packages/copilotstudio" }, { name = "agent-framework-declarative", marker = "extra == 'all'", editable = "packages/declarative" }, @@ -1356,7 +1358,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 +1837,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 +4573,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 +5223,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 = [ From c3b1536e902623941b6a557d1d49c214cf26d4f0 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 16 Feb 2026 14:30:01 +0100 Subject: [PATCH 2/5] small fix in sample --- .../02-agents/context_providers/simple_context_provider.py | 2 +- python/samples/02-agents/providers/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/samples/02-agents/context_providers/simple_context_provider.py b/python/samples/02-agents/context_providers/simple_context_provider.py index 88368b40ce..db9e93bc88 100644 --- a/python/samples/02-agents/context_providers/simple_context_provider.py +++ b/python/samples/02-agents/context_providers/simple_context_provider.py @@ -17,7 +17,7 @@ class UserInfo(BaseModel): class UserInfoMemory(BaseContextProvider): - def __init__(self, source_id: str = "user-info-memory", client: SupportsChatGetResponse = 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. diff --git a/python/samples/02-agents/providers/README.md b/python/samples/02-agents/providers/README.md index 11cb3701c2..235eb1292a 100644 --- a/python/samples/02-agents/providers/README.md +++ b/python/samples/02-agents/providers/README.md @@ -5,7 +5,7 @@ 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. | -| [`bedrock/`](bedrock/) | AWS Bedrock samples using `BedrockChatClient`, including tool-enabled agent usage. | +| [`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. | From 694574573098555768b9b38ebaa9e656c8553d50 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 16 Feb 2026 15:46:44 +0100 Subject: [PATCH 3/5] Finalize provider, samples, and core cleanup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/CODING_STANDARD.md | 15 ++++++++ .../agent_framework_anthropic/_chat_client.py | 2 ++ .../_agent_provider.py | 36 ++++++++----------- .../agent_framework_azure_ai/_chat_client.py | 7 ++-- .../agent_framework_azure_ai/_client.py | 9 ++--- .../_project_provider.py | 35 ++++++------------ .../claude/agent_framework_claude/_agent.py | 25 +++++-------- .../packages/core/agent_framework/__init__.py | 8 +++++ .../packages/core/agent_framework/_tools.py | 3 +- .../agent_framework/_workflows/__init__.py | 13 +++++++ .../core/agent_framework/a2a/__init__.py | 11 +++++- .../core/agent_framework/a2a/__init__.pyi | 2 -- .../core/agent_framework/ag_ui/__init__.py | 14 +++++++- .../core/agent_framework/ag_ui/__init__.pyi | 2 -- .../core/agent_framework/amazon/__init__.py | 14 +++++++- .../core/agent_framework/amazon/__init__.pyi | 15 ++++++++ .../agent_framework/anthropic/__init__.py | 31 ++++++++++++---- .../agent_framework/anthropic/__init__.pyi | 5 +-- .../core/agent_framework/azure/__init__.py | 14 ++++++++ .../core/agent_framework/chatkit/__init__.py | 13 ++++++- .../core/agent_framework/chatkit/__init__.pyi | 2 -- .../agent_framework/declarative/__init__.py | 13 ++++++- .../agent_framework/declarative/__init__.pyi | 2 -- .../core/agent_framework/devui/__init__.py | 15 +++++++- .../core/agent_framework/devui/__init__.pyi | 2 -- .../core/agent_framework/exceptions.py | 2 ++ .../core/agent_framework/github/__init__.py | 12 ++++++- .../core/agent_framework/github/__init__.pyi | 2 -- .../core/agent_framework/lab/__init__.py | 6 ++++ .../core/agent_framework/mem0/__init__.py | 11 +++++- .../core/agent_framework/mem0/__init__.pyi | 2 -- .../agent_framework/microsoft/__init__.py | 30 +++++++++++++++- .../agent_framework/microsoft/__init__.pyi | 10 ++++-- .../core/agent_framework/observability.py | 18 +++++++--- .../core/agent_framework/ollama/__init__.py | 12 ++++++- .../core/agent_framework/ollama/__init__.pyi | 2 -- .../core/agent_framework/openai/__init__.py | 12 +++++++ .../agent_framework/openai/_chat_client.py | 19 +++++----- .../openai/_responses_client.py | 19 +++++----- .../orchestrations/__init__.py | 14 +++++++- .../orchestrations/__init__.pyi | 2 -- .../core/agent_framework/redis/__init__.py | 12 ++++++- .../core/agent_framework/redis/__init__.pyi | 2 -- python/packages/core/pyproject.toml | 2 ++ python/packages/foundry_local/README.md | 4 +++ .../agent_framework_github_copilot/_agent.py | 14 +++----- python/samples/02-agents/providers/README.md | 1 + .../anthropic/anthropic_claude_basic.py | 2 +- .../anthropic/anthropic_claude_with_mcp.py | 2 +- ...hropic_claude_with_multiple_permissions.py | 2 +- .../anthropic_claude_with_session.py | 2 +- .../anthropic/anthropic_claude_with_shell.py | 2 +- .../anthropic/anthropic_claude_with_tools.py | 2 +- .../anthropic/anthropic_claude_with_url.py | 2 +- .../providers/foundry_local/README.md | 22 ++++++++++++ .../foundry_local}/foundry_local_agent.py | 2 +- ...penai_responses_client_image_generation.py | 20 ++++++----- python/uv.lock | 4 +++ 58 files changed, 411 insertions(+), 167 deletions(-) create mode 100644 python/packages/core/agent_framework/amazon/__init__.pyi create mode 100644 python/samples/02-agents/providers/foundry_local/README.md rename python/{packages/foundry_local/samples => samples/02-agents/providers/foundry_local}/foundry_local_agent.py (97%) diff --git a/python/CODING_STANDARD.md b/python/CODING_STANDARD.md index 028b914ba9..846d29aac1 100644 --- a/python/CODING_STANDARD.md +++ b/python/CODING_STANDARD.md @@ -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//`) 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 diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index a6b0c8e7d3..4e7ffafc2d 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -720,6 +720,8 @@ def _prepare_tools_for_anthropic(self, options: Mapping[str, Any]) -> dict[str, 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": diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py index f5c5201531..65bcb9d6c0 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py @@ -16,6 +16,7 @@ ) 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 @@ async def create_agent( 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 @@ async def create_agent( 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 @@ async def get_agent( 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 @@ async def get_agent( 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 @@ def as_agent( 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 @@ def _to_chat_agent_from_agent( 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 @@ def _merge_tools( 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 @@ def _merge_tools( 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. diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index b548885dae..c6028442fc 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -34,6 +34,7 @@ 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 @@ def as_agent( 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, diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 3b2f3c0e10..afbbf6cea3 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -5,7 +5,7 @@ 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 @@ 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 @@ def as_agent( 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, diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py index 1a71df1f79..b3f7b35147 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py @@ -17,6 +17,7 @@ ) 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 @@ async def create_agent( 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 @@ async def create_agent( 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 @@ async def get_agent( *, 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 @@ async def get_agent( 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 @@ def as_agent( 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 @@ def _to_chat_agent_from_details( 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 @@ def _merge_tools( 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 @@ def _merge_tools( 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 diff --git a/python/packages/claude/agent_framework_claude/_agent.py b/python/packages/claude/agent_framework_claude/_agent.py index 6ad805c5d5..204b364962 100644 --- a/python/packages/claude/agent_framework_claude/_agent.py +++ b/python/packages/claude/agent_framework_claude/_agent.py @@ -23,6 +23,7 @@ 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 @@ def __init__( 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 @@ def __init__( # 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 @@ def __init__( 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 @@ def _normalize_tools( # 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 @@ def _prepare_client_options(self, resume_session_id: str | None = None) -> SDKOp 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. diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py index bc53b10d16..2cc7d0d713 100644 --- a/python/packages/core/agent_framework/__init__.py +++ b/python/packages/core/agent_framework/__init__.py @@ -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 diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index b9a4d0a541..b5d110aad1 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -24,6 +24,7 @@ Final, Generic, Literal, + TypeAlias, TypedDict, Union, get_args, @@ -623,7 +624,7 @@ def to_dict(self, *, exclude: set[str] | None = None, exclude_none: bool = True) return as_dict -ToolTypes = FunctionTool | MCPTool | Mapping[str, Any] | Any +ToolTypes: TypeAlias = FunctionTool | MCPTool | Mapping[str, Any] | Any def normalize_tools( diff --git a/python/packages/core/agent_framework/_workflows/__init__.py b/python/packages/core/agent_framework/_workflows/__init__.py index 3eb65335c9..2dfa029840 100644 --- a/python/packages/core/agent_framework/_workflows/__init__.py +++ b/python/packages/core/agent_framework/_workflows/__init__.py @@ -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, diff --git a/python/packages/core/agent_framework/a2a/__init__.py b/python/packages/core/agent_framework/a2a/__init__.py index f06ee08a0b..7c7de63456 100644 --- a/python/packages/core/agent_framework/a2a/__init__.py +++ b/python/packages/core/agent_framework/a2a/__init__.py @@ -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: diff --git a/python/packages/core/agent_framework/a2a/__init__.pyi b/python/packages/core/agent_framework/a2a/__init__.pyi index 8121fc0eb7..5a54bb22a9 100644 --- a/python/packages/core/agent_framework/a2a/__init__.pyi +++ b/python/packages/core/agent_framework/a2a/__init__.pyi @@ -2,10 +2,8 @@ from agent_framework_a2a import ( A2AAgent, - __version__, ) __all__ = [ "A2AAgent", - "__version__", ] diff --git a/python/packages/core/agent_framework/ag_ui/__init__.py b/python/packages/core/agent_framework/ag_ui/__init__.py index b469bb8a60..0f6991c9b9 100644 --- a/python/packages/core/agent_framework/ag_ui/__init__.py +++ b/python/packages/core/agent_framework/ag_ui/__init__.py @@ -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", diff --git a/python/packages/core/agent_framework/ag_ui/__init__.pyi b/python/packages/core/agent_framework/ag_ui/__init__.pyi index d7b6acafec..6e1948ce86 100644 --- a/python/packages/core/agent_framework/ag_ui/__init__.pyi +++ b/python/packages/core/agent_framework/ag_ui/__init__.pyi @@ -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", ] diff --git a/python/packages/core/agent_framework/amazon/__init__.py b/python/packages/core/agent_framework/amazon/__init__.py index 21ee049f9b..42324acf96 100644 --- a/python/packages/core/agent_framework/amazon/__init__.py +++ b/python/packages/core/agent_framework/amazon/__init__.py @@ -1,11 +1,23 @@ # 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 = ["__version__", "BedrockChatClient", "BedrockChatOptions", "BedrockGuardrailConfig", "BedrockSettings"] +_IMPORTS = ["BedrockChatClient", "BedrockChatOptions", "BedrockGuardrailConfig", "BedrockSettings"] def __getattr__(name: str) -> Any: diff --git a/python/packages/core/agent_framework/amazon/__init__.pyi b/python/packages/core/agent_framework/amazon/__init__.pyi new file mode 100644 index 0000000000..c691334da5 --- /dev/null +++ b/python/packages/core/agent_framework/amazon/__init__.pyi @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft. All rights reserved. + +from agent_framework_bedrock import ( + BedrockChatClient, + BedrockChatOptions, + BedrockGuardrailConfig, + BedrockSettings, +) + +__all__ = [ + "BedrockChatClient", + "BedrockChatOptions", + "BedrockGuardrailConfig", + "BedrockSettings", +] diff --git a/python/packages/core/agent_framework/anthropic/__init__.py b/python/packages/core/agent_framework/anthropic/__init__.py index ea03e6cdf0..242554cf16 100644 --- a/python/packages/core/agent_framework/anthropic/__init__.py +++ b/python/packages/core/agent_framework/anthropic/__init__.py @@ -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()) diff --git a/python/packages/core/agent_framework/anthropic/__init__.pyi b/python/packages/core/agent_framework/anthropic/__init__.pyi index 3d790ebb07..29d70f62b8 100644 --- a/python/packages/core/agent_framework/anthropic/__init__.pyi +++ b/python/packages/core/agent_framework/anthropic/__init__.pyi @@ -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", ] diff --git a/python/packages/core/agent_framework/azure/__init__.py b/python/packages/core/agent_framework/azure/__init__.py index 93d7dc1e0d..7ac132bf3a 100644 --- a/python/packages/core/agent_framework/azure/__init__.py +++ b/python/packages/core/agent_framework/azure/__init__.py @@ -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 diff --git a/python/packages/core/agent_framework/chatkit/__init__.py b/python/packages/core/agent_framework/chatkit/__init__.py index 024454be5c..7363b82361 100644 --- a/python/packages/core/agent_framework/chatkit/__init__.py +++ b/python/packages/core/agent_framework/chatkit/__init__.py @@ -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: diff --git a/python/packages/core/agent_framework/chatkit/__init__.pyi b/python/packages/core/agent_framework/chatkit/__init__.pyi index 2fba862a1b..0fdeff2523 100644 --- a/python/packages/core/agent_framework/chatkit/__init__.pyi +++ b/python/packages/core/agent_framework/chatkit/__init__.pyi @@ -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", ] diff --git a/python/packages/core/agent_framework/declarative/__init__.py b/python/packages/core/agent_framework/declarative/__init__.py index 4ad557c0f7..a4fa5ba9f8 100644 --- a/python/packages/core/agent_framework/declarative/__init__.py +++ b/python/packages/core/agent_framework/declarative/__init__.py @@ -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", diff --git a/python/packages/core/agent_framework/declarative/__init__.pyi b/python/packages/core/agent_framework/declarative/__init__.pyi index 8d2b717c99..214bb132ab 100644 --- a/python/packages/core/agent_framework/declarative/__init__.pyi +++ b/python/packages/core/agent_framework/declarative/__init__.pyi @@ -13,7 +13,6 @@ from agent_framework_declarative import ( ProviderTypeMapping, WorkflowFactory, WorkflowState, - __version__, ) __all__ = [ @@ -29,5 +28,4 @@ __all__ = [ "ProviderTypeMapping", "WorkflowFactory", "WorkflowState", - "__version__", ] diff --git a/python/packages/core/agent_framework/devui/__init__.py b/python/packages/core/agent_framework/devui/__init__.py index cd18b0c5da..ce8215f941 100644 --- a/python/packages/core/agent_framework/devui/__init__.py +++ b/python/packages/core/agent_framework/devui/__init__.py @@ -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 @@ "main", "register_cleanup", "serve", - "__version__", ] diff --git a/python/packages/core/agent_framework/devui/__init__.pyi b/python/packages/core/agent_framework/devui/__init__.pyi index 9396af54bb..d6828ce96f 100644 --- a/python/packages/core/agent_framework/devui/__init__.pyi +++ b/python/packages/core/agent_framework/devui/__init__.pyi @@ -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", diff --git a/python/packages/core/agent_framework/exceptions.py b/python/packages/core/agent_framework/exceptions.py index 21e50e571e..5e9cba7572 100644 --- a/python/packages/core/agent_framework/exceptions.py +++ b/python/packages/core/agent_framework/exceptions.py @@ -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 diff --git a/python/packages/core/agent_framework/github/__init__.py b/python/packages/core/agent_framework/github/__init__.py index f763ebf6d4..831a838c0d 100644 --- a/python/packages/core/agent_framework/github/__init__.py +++ b/python/packages/core/agent_framework/github/__init__.py @@ -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 @@ "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"), } diff --git a/python/packages/core/agent_framework/github/__init__.pyi b/python/packages/core/agent_framework/github/__init__.pyi index 60a32ed620..567ab9490d 100644 --- a/python/packages/core/agent_framework/github/__init__.pyi +++ b/python/packages/core/agent_framework/github/__init__.pyi @@ -4,12 +4,10 @@ from agent_framework_github_copilot import ( GitHubCopilotAgent, GitHubCopilotOptions, GitHubCopilotSettings, - __version__, ) __all__ = [ "GitHubCopilotAgent", "GitHubCopilotOptions", "GitHubCopilotSettings", - "__version__", ] diff --git a/python/packages/core/agent_framework/lab/__init__.py b/python/packages/core/agent_framework/lab/__init__.py index 0d8d8fbaa4..80ed9251a8 100644 --- a/python/packages/core/agent_framework/lab/__init__.py +++ b/python/packages/core/agent_framework/lab/__init__.py @@ -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__) diff --git a/python/packages/core/agent_framework/mem0/__init__.py b/python/packages/core/agent_framework/mem0/__init__.py index dddc742ef0..56714d2224 100644 --- a/python/packages/core/agent_framework/mem0/__init__.py +++ b/python/packages/core/agent_framework/mem0/__init__.py @@ -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: diff --git a/python/packages/core/agent_framework/mem0/__init__.pyi b/python/packages/core/agent_framework/mem0/__init__.pyi index 18ee3bf2bd..20a68e1b06 100644 --- a/python/packages/core/agent_framework/mem0/__init__.pyi +++ b/python/packages/core/agent_framework/mem0/__init__.pyi @@ -2,10 +2,8 @@ from agent_framework_mem0 import ( Mem0ContextProvider, - __version__, ) __all__ = [ "Mem0ContextProvider", - "__version__", ] diff --git a/python/packages/core/agent_framework/microsoft/__init__.py b/python/packages/core/agent_framework/microsoft/__init__.py index 689faf6fd7..cf61e518d9 100644 --- a/python/packages/core/agent_framework/microsoft/__init__.py +++ b/python/packages/core/agent_framework/microsoft/__init__.py @@ -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 @@ "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"), } diff --git a/python/packages/core/agent_framework/microsoft/__init__.pyi b/python/packages/core/agent_framework/microsoft/__init__.pyi index 2d2ec42d4d..be3abe523c 100644 --- a/python/packages/core/agent_framework/microsoft/__init__.pyi +++ b/python/packages/core/agent_framework/microsoft/__init__.pyi @@ -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", ] diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index 7973a781a4..0127a101df 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -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 @@ def get_response( 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, diff --git a/python/packages/core/agent_framework/ollama/__init__.py b/python/packages/core/agent_framework/ollama/__init__.py index eae73853c2..1c6ba6820c 100644 --- a/python/packages/core/agent_framework/ollama/__init__.py +++ b/python/packages/core/agent_framework/ollama/__init__.py @@ -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: diff --git a/python/packages/core/agent_framework/ollama/__init__.pyi b/python/packages/core/agent_framework/ollama/__init__.pyi index 3a1e7824d6..ed439f3b36 100644 --- a/python/packages/core/agent_framework/ollama/__init__.pyi +++ b/python/packages/core/agent_framework/ollama/__init__.pyi @@ -3,11 +3,9 @@ from agent_framework_ollama import ( OllamaChatClient, OllamaSettings, - __version__, ) __all__ = [ "OllamaChatClient", "OllamaSettings", - "__version__", ] diff --git a/python/packages/core/agent_framework/openai/__init__.py b/python/packages/core/agent_framework/openai/__init__.py index c5e196b022..2d9cf09648 100644 --- a/python/packages/core/agent_framework/openai/__init__.py +++ b/python/packages/core/agent_framework/openai/__init__.py @@ -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, diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index 16cbec3afb..750910d651 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -343,15 +343,16 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An 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"): diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index 0d970e38d2..bdda41a173 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -822,15 +822,16 @@ async def _prepare_options( # 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) diff --git a/python/packages/core/agent_framework/orchestrations/__init__.py b/python/packages/core/agent_framework/orchestrations/__init__.py index 6e220bac93..fa3561f22f 100644 --- a/python/packages/core/agent_framework/orchestrations/__init__.py +++ b/python/packages/core/agent_framework/orchestrations/__init__.py @@ -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 diff --git a/python/packages/core/agent_framework/orchestrations/__init__.pyi b/python/packages/core/agent_framework/orchestrations/__init__.pyi index cf26847972..79e9378dec 100644 --- a/python/packages/core/agent_framework/orchestrations/__init__.pyi +++ b/python/packages/core/agent_framework/orchestrations/__init__.pyi @@ -35,7 +35,6 @@ from agent_framework_orchestrations import ( MagenticResetSignal, SequentialBuilder, StandardMagenticManager, - __version__, ) __all__ = [ @@ -73,5 +72,4 @@ __all__ = [ "MagenticResetSignal", "SequentialBuilder", "StandardMagenticManager", - "__version__", ] diff --git a/python/packages/core/agent_framework/redis/__init__.py b/python/packages/core/agent_framework/redis/__init__.py index 9f96b3455f..d5e5de238c 100644 --- a/python/packages/core/agent_framework/redis/__init__.py +++ b/python/packages/core/agent_framework/redis/__init__.py @@ -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: diff --git a/python/packages/core/agent_framework/redis/__init__.pyi b/python/packages/core/agent_framework/redis/__init__.pyi index fc62badb76..bdf36624ba 100644 --- a/python/packages/core/agent_framework/redis/__init__.pyi +++ b/python/packages/core/agent_framework/redis/__init__.pyi @@ -3,11 +3,9 @@ from agent_framework_redis import ( RedisContextProvider, RedisHistoryProvider, - __version__, ) __all__ = [ "RedisContextProvider", "RedisHistoryProvider", - "__version__", ] diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml index c62aa26da8..a883be8979 100644 --- a/python/packages/core/pyproject.toml +++ b/python/packages/core/pyproject.toml @@ -45,6 +45,7 @@ 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", @@ -53,6 +54,7 @@ all = [ "agent-framework-declarative", "agent-framework-devui", "agent-framework-durabletask", + "agent-framework-foundry-local", "agent-framework-github-copilot", "agent-framework-lab", "agent-framework-mem0", diff --git a/python/packages/foundry_local/README.md b/python/packages/foundry_local/README.md index c65e5a0386..cf4f340bd3 100644 --- a/python/packages/foundry_local/README.md +++ b/python/packages/foundry_local/README.md @@ -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. diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 26323e915d..f90c28f471 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -22,7 +22,7 @@ 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 @@ def __init__( 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 @@ def _prepare_system_message( def _prepare_tools( self, - tools: list[FunctionTool | MutableMapping[str, Any]], + tools: Sequence[ToolTypes], ) -> list[CopilotTool]: """Convert Agent Framework tools to Copilot SDK tools. @@ -493,8 +489,8 @@ def _prepare_tools( for tool in tools: if isinstance(tool, FunctionTool): copilot_tools.append(self._tool_to_copilot_tool(tool)) # type: ignore - elif isinstance(tool, CopilotTool): - copilot_tools.append(tool) + 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 diff --git a/python/samples/02-agents/providers/README.md b/python/samples/02-agents/providers/README.md index 235eb1292a..20a598b08e 100644 --- a/python/samples/02-agents/providers/README.md +++ b/python/samples/02-agents/providers/README.md @@ -11,6 +11,7 @@ This directory groups provider-specific samples for Agent Framework. | [`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. | diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_basic.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_basic.py index 8bea9263de..b040bbd299 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_claude_basic.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_basic.py @@ -19,7 +19,7 @@ from typing import Annotated from agent_framework import tool -from agent_framework_claude import ClaudeAgent +from agent_framework.anthropic import ClaudeAgent @tool diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_mcp.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_mcp.py index f47dbd1648..455f4fd190 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_mcp.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_mcp.py @@ -19,7 +19,7 @@ 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 diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_multiple_permissions.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_multiple_permissions.py index e4e2d10605..e4165089c0 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_multiple_permissions.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_multiple_permissions.py @@ -22,7 +22,7 @@ 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 diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_session.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_session.py index 623be4f299..6ff1fff25a 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_session.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_session.py @@ -13,7 +13,7 @@ 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 diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_shell.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_shell.py index 849a96c593..86f8be9794 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_shell.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_shell.py @@ -14,7 +14,7 @@ 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 diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_tools.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_tools.py index 15b9cbc5dc..8ce13ef54e 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_tools.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_tools.py @@ -17,7 +17,7 @@ import asyncio -from agent_framework_claude import ClaudeAgent +from agent_framework.anthropic import ClaudeAgent async def main() -> None: diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_url.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_url.py index 102785ca94..bcb6b8b03e 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_url.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_url.py @@ -16,7 +16,7 @@ import asyncio -from agent_framework_claude import ClaudeAgent +from agent_framework.anthropic import ClaudeAgent async def main() -> None: diff --git a/python/samples/02-agents/providers/foundry_local/README.md b/python/samples/02-agents/providers/foundry_local/README.md new file mode 100644 index 0000000000..72451a9a69 --- /dev/null +++ b/python/samples/02-agents/providers/foundry_local/README.md @@ -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`. diff --git a/python/packages/foundry_local/samples/foundry_local_agent.py b/python/samples/02-agents/providers/foundry_local/foundry_local_agent.py similarity index 97% rename from python/packages/foundry_local/samples/foundry_local_agent.py rename to python/samples/02-agents/providers/foundry_local/foundry_local_agent.py index bca1d469d9..0ea0c15bc2 100644 --- a/python/packages/foundry_local/samples/foundry_local_agent.py +++ b/python/samples/02-agents/providers/foundry_local/foundry_local_agent.py @@ -7,7 +7,7 @@ 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 diff --git a/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py b/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py index 1e015b3762..e6f54677e4 100644 --- a/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py +++ b/python/samples/02-agents/providers/openai/openai_responses_client_image_generation.py @@ -6,7 +6,6 @@ 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 @@ """ -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: diff --git a/python/uv.lock b/python/uv.lock index 8ecd0b545d..a5bccab41e 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -345,10 +345,12 @@ all = [ { 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'" }, @@ -368,10 +370,12 @@ requires-dist = [ { 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" }, From ee8f4c917c3ffcc01461be133312a18780387224 Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 16 Feb 2026 16:06:48 +0100 Subject: [PATCH 4/5] Fix CopilotTool passthrough in agent Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../github_copilot/agent_framework_github_copilot/_agent.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index f90c28f471..36a2ee80b7 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -474,7 +474,7 @@ def _prepare_system_message( def _prepare_tools( self, - tools: Sequence[ToolTypes], + tools: Sequence[ToolTypes | CopilotTool], ) -> list[CopilotTool]: """Convert Agent Framework tools to Copilot SDK tools. @@ -487,7 +487,9 @@ def _prepare_tools( copilot_tools: list[CopilotTool] = [] for tool in tools: - if isinstance(tool, FunctionTool): + 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] From 46274d78f1296381cb0facfca7a91048e5e76e3b Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 16 Feb 2026 16:22:01 +0100 Subject: [PATCH 5/5] fix link --- python/packages/bedrock/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/packages/bedrock/README.md b/python/packages/bedrock/README.md index 0e49f8c5cf..10a3bd9f25 100644 --- a/python/packages/bedrock/README.md +++ b/python/packages/bedrock/README.md @@ -12,7 +12,7 @@ The Bedrock integration enables Microsoft Agent Framework applications to call A ### Basic Usage Example -See the [Bedrock sample script](../../samples/02-agents/providers/bedrock/bedrock_chat_client.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`