-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Python: Added M365 Agent SDK Hosting sample #2292
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
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
9e9b630
Added M365 Agent SDK Hosting sample
dmytrostruk f760ea5
Merge branch 'main' into m365-python-sample
dmytrostruk cb931fe
Merge branch 'main' into m365-python-sample
dmytrostruk b3e0d82
Merge branch 'main' into m365-python-sample
dmytrostruk d6e7565
Merge branch 'main' into m365-python-sample
dmytrostruk 00dda14
Addressed PR feedback
dmytrostruk fcec361
Added inline dependencies
dmytrostruk 5a13b7a
Addressed PR feedback
dmytrostruk b505abc
Merge branch 'main' into m365-python-sample
dmytrostruk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.