Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions python/samples/05-end-to-end/m365-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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://<your-resource>.services.ai.azure.com/api/projects/<your-project>"
export FOUNDRY_MODEL="gpt-4o-mini"
```

## Installing Dependencies
Expand Down
53 changes: 25 additions & 28 deletions python/samples/05-end-to-end/m365-agent/m365_agent_demo/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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.
"""

Expand Down Expand Up @@ -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()})
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.
Expand All @@ -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
Expand All @@ -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()
Expand Down
Loading