Skip to content
Merged
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
17 changes: 17 additions & 0 deletions python/samples/demos/m365-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# OpenAI Configuration
OPENAI_API_KEY=
OPENAI_CHAT_MODEL_ID=

# Agent 365 Agentic Authentication Configuration
USE_ANONYMOUS_MODE=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID=
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=

AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=https://graph.microsoft.com/.default

CONNECTIONSMAP_0_SERVICEURL=*
CONNECTIONSMAP_0_CONNECTION=SERVICE_CONNECTION
100 changes: 100 additions & 0 deletions python/samples/demos/m365-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Microsoft Agent Framework Python Weather Agent sample (M365 Agents SDK)

This sample demonstrates a simple Weather Forecast Agent built with the Python Microsoft Agent Framework, exposed through the Microsoft 365 Agents SDK compatible endpoints. The agent accepts natural language requests for a weather forecast and responds with a textual answer. It supports multi-turn conversations to gather required information.

## Prerequisites

- Python 3.11+
- [uv](https://github.com/astral-sh/uv) for fast dependency management
- [devtunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows)
- [Microsoft 365 Agents Toolkit](https://github.com/OfficeDev/microsoft-365-agents-toolkit) for playground/testing
- Access to OpenAI or Azure OpenAI with a model like `gpt-4o-mini`

## Configuration

Set the following environment variables:

```bash
# Common
export PORT=3978
export USE_ANONYMOUS_MODE=True # set to false if using auth

# OpenAI
export OPENAI_API_KEY="..."
export OPENAI_CHAT_MODEL_ID="..."
```

## Installing Dependencies

From the repository root or the sample folder:

```bash
uv sync
```

## Running the Agent Locally

```bash
# Activate environment first if not already
source .venv/bin/activate # (Windows PowerShell: .venv\Scripts\Activate.ps1)

# Run the weather agent demo
python m365_agent_demo/app.py
```

The agent starts on `http://localhost:3978`. Health check: `GET /api/health`.

## QuickStart using Agents Playground

1. Install (if not already):

```bash
winget install agentsplayground
```

2. Start the Python agent locally: `python m365_agent_demo/app.py`
3. Start the playground: `agentsplayground`
4. Chat with the Weather Agent.

## QuickStart using WebChat (Azure Bot)

To test via WebChat you can provision an Azure Bot and point its messaging endpoint to your agent.

1. Create an Azure Bot (choose Client Secret auth for local tunneling).
2. Create a `.env` file in this sample folder with the following (replace placeholders):

```bash
# Authentication / Agentic configuration
USE_ANONYMOUS_MODE=False
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID="<client-id>"
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET="<client-secret>"
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID="<tenant-id>"
CONNECTIONS__SERVICE_CONNECTION__SETTINGS__SCOPES=https://graph.microsoft.com/.default

AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__TYPE=AgenticUserAuthorization
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__SCOPES=https://graph.microsoft.com/.default
AGENTAPPLICATION__USERAUTHORIZATION__HANDLERS__AGENTIC__SETTINGS__ALTERNATEBLUEPRINTCONNECTIONNAME=https://graph.microsoft.com/.default
```

3. Host dev tunnel:

```bash
devtunnel host -p 3978 --allow-anonymous
```

4. Set the bot Messaging endpoint to: `https://<tunnel-host>/api/messages`
5. Run your local agent: `python m365_agent_demo/app.py`
6. Use "Test in WebChat" in Azure Portal.

> Federated Credentials or Managed Identity auth types typically require deployment to Azure App Service instead of tunneling.

## Troubleshooting

- 404 on `/api/messages`: Ensure you are POSTing and using the correct tunnel URL.
- Empty responses: Check model key / quota and ensure environment variables are set.
- Auth errors when anonymous disabled: Validate MSAL config matches your Azure Bot registration.

## Further Reading

- [Microsoft 365 Agents SDK](https://learn.microsoft.com/microsoft-365/agents-sdk/)
- [Devtunnel docs](https://learn.microsoft.com/azure/developer/dev-tunnels/)
238 changes: 238 additions & 0 deletions python/samples/demos/m365-agent/m365_agent_demo/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# Copyright (c) Microsoft. All rights reserved.
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "microsoft-agents-hosting-aiohttp",
# "microsoft-agents-hosting-core",
# "microsoft-agents-authentication-msal",
# "microsoft-agents-activity",
# "agent-framework-core",
# "aiohttp"
# ]
# ///

import os
from dataclasses import dataclass
from random import randint
from typing import Annotated

from agent_framework import ChatAgent
from agent_framework.openai import OpenAIChatClient
from aiohttp import web
from aiohttp.web_middlewares import middleware
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,
Authorization,
ClaimsIdentity,
MemoryStorage,
TurnContext,
TurnState,
)
from pydantic import Field

"""
Demo application using Microsoft Agent 365 SDK.

This sample demonstrates how to build an AI agent using the Agent Framework,
integrating with Microsoft 365 authentication and hosting components.

The agent provides a simple weather tool and can be run in either anonymous mode
(no authentication required) or authenticated mode using MSAL and Azure AD.

Key features:
- Loads configuration from environment variables.
- Demonstrates agent creation and tool registration.
- 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
anonymous mode for local testing.
"""


@dataclass
class AppConfig:
use_anonymous_mode: bool
port: int
agents_sdk_config: dict


def load_app_config() -> AppConfig:
"""Load application configuration from environment variables.

Returns:
AppConfig: Consolidated configuration including anonymous mode flag, port, and SDK config.
"""
agents_sdk_config = load_configuration_from_env(os.environ)
use_anonymous_mode = os.environ.get("USE_ANONYMOUS_MODE", "true").lower() == "true"
port_str = os.getenv("PORT", "3978")
try:
port = int(port_str)
except ValueError:
port = 3978
return AppConfig(use_anonymous_mode=use_anonymous_mode, port=port, agents_sdk_config=agents_sdk_config)


def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
"""Generate a mock weather report for the provided location.

Args:
location: The geographic location name.
Returns:
str: Human-readable weather summary.
"""
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 build_agent() -> ChatAgent:
"""Create and return the chat agent instance with weather tool registered."""
return OpenAIChatClient().create_agent(
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.

Args:
config: Application configuration.
Returns:
MsalConnectionManager | None: Connection manager when authenticated mode is enabled.
"""
if config.use_anonymous_mode:
return None
return MsalConnectionManager(**config.agents_sdk_config)


def build_adapter(connection_manager: MsalConnectionManager | None) -> CloudAdapter:
"""Instantiate the CloudAdapter with the optional 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.

Args:
storage: State storage backend.
connection_manager: Optional connection manager.
config: Application configuration.
Returns:
Authorization | None: Authorization component when enabled.
"""
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,
config: AppConfig,
) -> AgentApplication[TurnState]:
"""Compose and return the AgentApplication instance.

Args:
storage: Storage implementation.
adapter: CloudAdapter handling requests.
authorization: Optional authorization component.
config: App configuration.
Returns:
AgentApplication[TurnState]: Configured agent application.
"""
return AgentApplication[TurnState](
storage=storage, adapter=adapter, authorization=authorization, **config.agents_sdk_config
)


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


def create_app(config: AppConfig) -> web.Application:
"""Create and configure the aiohttp web application.

Args:
config: Loaded application configuration.
Returns:
web.Application: Fully initialized web application.
"""
middleware_fn = build_anonymous_claims_middleware(config.use_anonymous_mode)
app = web.Application(middlewares=[middleware_fn])

storage = MemoryStorage()
agent = build_agent()
connection_manager = build_connection_manager(config)
adapter = build_adapter(connection_manager)
authorization = build_authorization(storage, connection_manager, config)
agent_app = build_agent_application(storage, adapter, authorization, config)

@agent_app.activity("message")
async def on_message(context: TurnContext, _: TurnState):
user_message = context.activity.text or ""
if not user_message.strip():
return

response = await agent.run(user_message)
response_text = response.text

await context.send_activity(response_text)

async def health(request: web.Request) -> web.Response:
return web.json_response({"status": "ok"})

async def entry_point(req: web.Request) -> web.Response:
return await start_agent_process(req, req.app["agent_app"], req.app["adapter"])

app.add_routes([
web.get("/api/health", health),
web.get("/api/messages", lambda _: web.Response(status=200)),
web.post("/api/messages", entry_point),
])

app["agent_app"] = agent_app
app["adapter"] = adapter

return app


def main() -> None:
"""Entry point: load configuration, build app, and start server."""
config = load_app_config()
app = create_app(config)
web.run_app(app, host="localhost", port=config.port)


if __name__ == "__main__":
main()