Repository navigation
Python: Fix M365 agent sample startup and anonymous mode #9598
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,17 @@ | ||
| # /// script | ||
| # requires-python = ">=3.11" | ||
| # dependencies = [ | ||
| # "microsoft-agents-hosting-aiohttp", | ||
| # "microsoft-agents-hosting-core", | ||
| # "microsoft-agents-authentication-msal", | ||
| # "microsoft-agents-activity", | ||
| # "microsoft-agents-hosting-aiohttp>=1.4.0", | ||
| # "microsoft-agents-hosting-core>=1.4.0", | ||
| # "microsoft-agents-authentication-msal>=1.4.0", | ||
| # "microsoft-agents-activity>=1.4.0", | ||
| # "agent-framework-foundry", | ||
| # "aiohttp" | ||
| # ] | ||
| # /// | ||
| # 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.aiohttp import CloudAdapter, jwt_authorization_decorator, 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,59 +102,61 @@ 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()}) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Naveen Chatlapalli (@1aifanatic), this placeholder is still selected for anonymous agentic activities. In M365 Agents SDK 1.9.0, |
||
| 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. | ||
|
|
||
| Args: | ||
| storage: Storage implementation. | ||
| adapter: CloudAdapter handling requests. | ||
| authorization: Optional authorization component. | ||
| authorization: Authorization component. | ||
| config: App configuration. | ||
| Returns: | ||
| AgentApplication[TurnState]: Configured agent application. | ||
|
|
@@ -163,30 +166,12 @@ def build_agent_application( | |
| ) | ||
|
|
||
|
|
||
| def build_anonymous_claims_middleware(use_anonymous_mode: bool): | ||
| """Return a middleware that injects anonymous claims when enabled. | ||
|
|
||
| Args: | ||
| use_anonymous_mode: Whether to apply anonymous identity for each request. | ||
| Returns: | ||
| Callable: Aiohttp middleware function. | ||
| """ | ||
|
|
||
| @middleware | ||
| 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", | ||
| ) | ||
| return await handler(request) | ||
|
|
||
| return anonymous_claims_middleware | ||
| @middleware | ||
| async def anonymous_claims_middleware(request, handler): | ||
| """Inject an anonymous identity for local testing without authentication.""" | ||
| # An anonymous identity with no claims makes the adapter skip token acquisition. | ||
| request["claims_identity"] = ClaimsIdentity(authentication_type="Anonymous") | ||
| return await handler(request) | ||
|
|
||
|
|
||
| def create_app(config: AppConfig) -> web.Application: | ||
|
|
@@ -197,8 +182,7 @@ def create_app(config: AppConfig) -> web.Application: | |
| Returns: | ||
| 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=[anonymous_claims_middleware] if config.use_anonymous_mode else []) | ||
|
|
||
| storage = MemoryStorage() | ||
| agent = build_agent() | ||
|
|
@@ -224,6 +208,12 @@ async def health(request: web.Request) -> web.Response: | |
| async def entry_point(req: web.Request) -> web.Response: | ||
| return await start_agent_process(req, req.app["agent_app"], req.app["adapter"]) | ||
|
|
||
| if not config.use_anonymous_mode: | ||
| # Validate the bearer token on every incoming activity; requests with a missing or | ||
| # invalid token get a 401 before they reach the agent. | ||
| app["agent_configuration"] = connection_manager.get_default_connection_configuration() | ||
| entry_point = jwt_authorization_decorator(entry_point) | ||
|
|
||
| app.add_routes([ | ||
| web.get("/api/health", health), | ||
| web.get("/api/messages", lambda _: web.Response(status=200)), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naveen Chatlapalli (@1aifanatic), the README now correctly documents these Foundry settings, but
.env.examplestill lists the old OpenAI variables and omitsFOUNDRY_PROJECT_ENDPOINTandFOUNDRY_MODEL. Please update the template as part of this change so following it produces a runnable configuration.