mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Added M365 Agent SDK Hosting sample (#2292)
* Added M365 Agent SDK Hosting sample * Addressed PR feedback * Added inline dependencies * Addressed PR feedback
This commit is contained in:
committed by
GitHub
Unverified
parent
4e339f841a
commit
84e2c0cc22
@@ -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
|
||||
@@ -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/)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user