From df18abea287e0661a404d6b6dc988c3d65772d6c Mon Sep 17 00:00:00 2001 From: aifanatic Date: Thu, 22 Oct 2026 07:38:44 -0500 Subject: [PATCH] Python: Fix M365 agent sample startup with current SDKs - aiohttp's Application takes middlewares=, not middleware= (TypeError at startup). - FoundryChatClient needs a credential; pass AzureCliCredential and document the FOUNDRY_* settings the sample actually reads instead of OpenAI ones. - The M365 Agents SDK now requires a connection manager for CloudAdapter and AgentApplication; give anonymous mode an empty service connection. - ClaimsIdentity's signature is now (claims, authentication_type, ...); build a claim-less anonymous identity so the adapter skips token acquisition. Co-Authored-By: Claude Opus 5.5 --- .../05-end-to-end/m365-agent/README.md | 9 ++-- .../m365-agent/m365_agent_demo/app.py | 53 +++++++++---------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/python/samples/05-end-to-end/m365-agent/README.md b/python/samples/05-end-to-end/m365-agent/README.md index ec9da47331c..891eac8ff8e 100644 --- a/python/samples/05-end-to-end/m365-agent/README.md +++ b/python/samples/05-end-to-end/m365-agent/README.md @@ -8,7 +8,8 @@ This sample demonstrates a simple Weather Forecast Agent built with the Python M - [uv](https://github.com/astral-sh/uv) for fast dependency management - [devtunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) - `agentsplayground` for playground/testing -- Access to OpenAI or Azure OpenAI with a model like `gpt-4o-mini` +- A Microsoft Foundry project with a deployed model like `gpt-4o-mini` +- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli), signed in with `az login` ## Configuration @@ -19,9 +20,9 @@ Set the following environment variables: export PORT=3978 export USE_ANONYMOUS_MODE=True # set to false if using auth -# OpenAI -export OPENAI_API_KEY="..." -export OPENAI_CHAT_COMPLETION_MODEL="..." +# Microsoft Foundry (authenticates with your `az login` session) +export FOUNDRY_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/" +export FOUNDRY_MODEL="gpt-4o-mini" ``` ## Installing Dependencies diff --git a/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py b/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py index e4e77a3d78c..fa459ad9ff8 100644 --- a/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py +++ b/python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py @@ -11,7 +11,7 @@ # /// # Copyright (c) Microsoft. All rights reserved. # Run with any PEP 723 compatible runner, e.g.: -# uv run samples/demos/m365-agent/m365_agent_demo/app.py +# uv run samples/05-end-to-end/m365-agent/m365_agent_demo/app.py import os from dataclasses import dataclass @@ -22,13 +22,14 @@ from agent_framework.foundry import FoundryChatClient from aiohttp import web from aiohttp.web_middlewares import middleware +from azure.identity import AzureCliCredential from dotenv import load_dotenv from microsoft_agents.activity import load_configuration_from_env from microsoft_agents.authentication.msal import MsalConnectionManager from microsoft_agents.hosting.aiohttp import CloudAdapter, start_agent_process from microsoft_agents.hosting.core import ( AgentApplication, - AuthenticationConstants, + AgentAuthConfiguration, Authorization, ClaimsIdentity, MemoryStorage, @@ -55,7 +56,7 @@ - Supports both anonymous and authenticated scenarios. - Uses aiohttp for web hosting. -To run, set the appropriate environment variables (check .env.example file) for authentication or use +To run, set the environment variables listed in the README for authentication or use anonymous mode for local testing. """ @@ -101,51 +102,53 @@ def get_weather( def build_agent() -> Agent: """Create and return the chat agent instance with weather tool registered.""" - _client = FoundryChatClient() + # Reads FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL from the environment; run `az login` first. + _client = FoundryChatClient(credential=AzureCliCredential()) return Agent( client=_client, name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather ) -def build_connection_manager(config: AppConfig) -> MsalConnectionManager | None: - """Build the connection manager unless running in anonymous mode. +def build_connection_manager(config: AppConfig) -> MsalConnectionManager: + """Build the connection manager. Args: config: Application configuration. Returns: - MsalConnectionManager | None: Connection manager when authenticated mode is enabled. + MsalConnectionManager: Connection manager configured from the environment, or an empty + placeholder connection in anonymous mode. """ if config.use_anonymous_mode: - return None + # CloudAdapter requires a connection manager, but anonymous requests never request tokens, + # so an empty service connection is enough for local testing. + return MsalConnectionManager(connections_configurations={"SERVICE_CONNECTION": AgentAuthConfiguration()}) return MsalConnectionManager(**config.agents_sdk_config) -def build_adapter(connection_manager: MsalConnectionManager | None) -> CloudAdapter: - """Instantiate the CloudAdapter with the optional connection manager.""" +def build_adapter(connection_manager: MsalConnectionManager) -> CloudAdapter: + """Instantiate the CloudAdapter with the connection manager.""" return CloudAdapter(connection_manager=connection_manager) def build_authorization( - storage: MemoryStorage, connection_manager: MsalConnectionManager | None, config: AppConfig -) -> Authorization | None: - """Create Authorization component if not in anonymous mode. + storage: MemoryStorage, connection_manager: MsalConnectionManager, config: AppConfig +) -> Authorization: + """Create the Authorization component. Args: storage: State storage backend. - connection_manager: Optional connection manager. + connection_manager: Connection manager. config: Application configuration. Returns: - Authorization | None: Authorization component when enabled. + Authorization: Authorization component. """ - if config.use_anonymous_mode: - return None return Authorization(storage, connection_manager, **config.agents_sdk_config) def build_agent_application( storage: MemoryStorage, adapter: CloudAdapter, - authorization: Authorization | None, + authorization: Authorization, config: AppConfig, ) -> AgentApplication[TurnState]: """Compose and return the AgentApplication instance. @@ -153,7 +156,7 @@ def build_agent_application( Args: storage: Storage implementation. adapter: CloudAdapter handling requests. - authorization: Optional authorization component. + authorization: Authorization component. config: App configuration. Returns: AgentApplication[TurnState]: Configured agent application. @@ -176,14 +179,8 @@ def build_anonymous_claims_middleware(use_anonymous_mode: bool): async def anonymous_claims_middleware(request, handler): """Inject claims for anonymous users if anonymous mode is active.""" if use_anonymous_mode: - request["claims_identity"] = ClaimsIdentity( - { - AuthenticationConstants.AUDIENCE_CLAIM: "anonymous", - AuthenticationConstants.APP_ID_CLAIM: "anonymous-app", - }, - False, - "Anonymous", - ) + # An anonymous identity with no claims makes the adapter skip token acquisition. + request["claims_identity"] = ClaimsIdentity(authentication_type="Anonymous") return await handler(request) return anonymous_claims_middleware @@ -198,7 +195,7 @@ def create_app(config: AppConfig) -> web.Application: web.Application: Fully initialized web application. """ middleware_fn = build_anonymous_claims_middleware(config.use_anonymous_mode) - app = web.Application(middleware=[middleware_fn]) + app = web.Application(middlewares=[middleware_fn]) storage = MemoryStorage() agent = build_agent()