diff --git a/README.md b/README.md
index 24640e2631..be6b8527eb 100644
--- a/README.md
+++ b/README.md
@@ -22,12 +22,13 @@ The easy way to browse through all the samples is to visit the [Microsoft 365 Sa
1. [Scenario specific samples](#Scenario-specific-samples)
## [Microsoft 365 Copilot samples](https://github.com/OfficeDev/Copilot-for-M365-Samples)
-| Name | Description | Level | .NET | TypeScript | JavaScript |
-| ------------------ | :----------------------------------------------------------------------- | :----------: | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
-| Product support | Plugin that allows users to query the Products held in SharePoint Online team site via Microsoft Graph | Basic | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-product-support-sso-csharp) | | |
-| Northwind Inventory | Plugin that allows users to query the Northwind Database | Intermediate | [View](https://github.com/OfficeDev/Copilot-for-M365-Samples/tree/main/samples/msgext-northwind-inventory-csharp) | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-northwind-inventory-ts) | |
-| Document Search | Plugin that enables Hybrid Search (Vector + Semantic) | Advanced | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-doc-search-csharp) | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-doc-search-js) | |
-| Multi Parameters | Plugin that demonstrates how to implement complex utterances and support deep retrieval | Advanced | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-multiparam-csharp) | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-multiparam-ts) | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-multiparam-js) |
+| Name | Description | Level | .NET | TypeScript | JavaScript | Python |
+| ------------------ | :----------------------------------------------------------------------- | :----------: | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
+| Product support | Plugin that allows users to query the Products held in SharePoint Online team site via Microsoft Graph | Basic | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-product-support-sso-csharp) | | | |
+| Northwind Inventory | Plugin that allows users to query the Northwind Database | Intermediate | [View](https://github.com/OfficeDev/Copilot-for-M365-Samples/tree/main/samples/msgext-northwind-inventory-csharp) | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-northwind-inventory-ts) | |[View](https://github.com/OfficeDev/Microsoft-365-Copilot-Samples/tree/main/samples/msgext-northwind-inventory-python) |
+| Trey Research | Declarative Agent with API Plugin | Advanced | [View](https://github.com/OfficeDev/Microsoft-365-Copilot-Samples/tree/main/samples/cext-trey-research-csharp) | [View](https://github.com/OfficeDev/Microsoft-365-Copilot-Samples/tree/main/samples/cext-trey-research) | |[View](https://github.com/OfficeDev/Microsoft-365-Copilot-Samples/tree/main/samples/cext-trey-research-python) |
+| Document Search | Plugin that enables Hybrid Search (Vector + Semantic) | Advanced | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-doc-search-csharp) | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-doc-search-js) | | |
+| Multi Parameters | Plugin that demonstrates how to implement complex utterances and support deep retrieval | Advanced | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-multiparam-csharp) | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-multiparam-ts) | [View](https://github.com/OfficeDev/Copilot-for-M365-Plugins-Samples/tree/main/samples/msgext-multiparam-js) | |
## [Samples built using new generation of Teams development tool - Teams Toolkit](https://github.com/OfficeDev/TeamsFx-Samples)
diff --git a/samples/app-region-selection/python/.env b/samples/app-region-selection/python/.env
new file mode 100644
index 0000000000..660828e3e8
--- /dev/null
+++ b/samples/app-region-selection/python/.env
@@ -0,0 +1,2 @@
+MicrosoftAppId=
+MicrosoftAppPassword=
\ No newline at end of file
diff --git a/samples/app-region-selection/python/.gitignore b/samples/app-region-selection/python/.gitignore
new file mode 100644
index 0000000000..e8442994dd
--- /dev/null
+++ b/samples/app-region-selection/python/.gitignore
@@ -0,0 +1,14 @@
+# TeamsFx files
+env/.env.*.user
+env/.env.local
+appManifest/build/
+
+# python virtual environment
+.venv/
+
+# misc
+.env
+.deployment/
+
+# tmp files
+__pycache__/
\ No newline at end of file
diff --git a/samples/app-region-selection/python/.vscode/extensions.json b/samples/app-region-selection/python/.vscode/extensions.json
new file mode 100644
index 0000000000..bf8c33db9c
--- /dev/null
+++ b/samples/app-region-selection/python/.vscode/extensions.json
@@ -0,0 +1,6 @@
+{
+ "recommendations": [
+ "TeamsDevApp.ms-teams-vscode-extension",
+ "ms-python.python",
+ ]
+}
\ No newline at end of file
diff --git a/samples/app-region-selection/python/.vscode/launch.json b/samples/app-region-selection/python/.vscode/launch.json
new file mode 100644
index 0000000000..6d66d8beb8
--- /dev/null
+++ b/samples/app-region-selection/python/.vscode/launch.json
@@ -0,0 +1,69 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch App (Edge)",
+ "type": "msedge",
+ "request": "launch",
+ "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}",
+ "cascadeTerminateToConfigurations": [
+ "Python: Run App Locally"
+ ],
+ "presentation": {
+ "group": "all",
+ "hidden": true
+ },
+ "internalConsoleOptions": "neverOpen"
+ },
+ {
+ "name": "Launch App (Chrome)",
+ "type": "chrome",
+ "request": "launch",
+ "url": "https://teams.microsoft.com/l/app/${{local:TEAMS_APP_ID}}?installAppPackage=true&webjoin=true&${account-hint}",
+ "cascadeTerminateToConfigurations": [
+ "Python: Run App Locally"
+ ],
+ "presentation": {
+ "group": "all",
+ "hidden": true
+ },
+ "internalConsoleOptions": "neverOpen"
+ },
+ {
+ "name": "Python: Run App Locally",
+ "type": "debugpy",
+ "request": "launch",
+ "program": "${workspaceFolder}/app.py",
+ "cwd": "${workspaceFolder}",
+ "console": "integratedTerminal"
+ }
+ ],
+ "compounds": [
+ {
+ "name": "Debug (Edge)",
+ "configurations": [
+ "Launch App (Edge)",
+ "Python: Run App Locally"
+ ],
+ "preLaunchTask": "Prepare Teams App Resources",
+ "presentation": {
+ "group": "all",
+ "order": 1
+ },
+ "stopAll": true
+ },
+ {
+ "name": "Debug (Chrome)",
+ "configurations": [
+ "Launch App (Chrome)",
+ "Python: Run App Locally"
+ ],
+ "preLaunchTask": "Prepare Teams App Resources",
+ "presentation": {
+ "group": "all",
+ "order": 2
+ },
+ "stopAll": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/samples/app-region-selection/python/.vscode/settings.json b/samples/app-region-selection/python/.vscode/settings.json
new file mode 100644
index 0000000000..3014fd9cf0
--- /dev/null
+++ b/samples/app-region-selection/python/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "debug.onTaskErrors": "abort"
+}
diff --git a/samples/app-region-selection/python/.vscode/tasks.json b/samples/app-region-selection/python/.vscode/tasks.json
new file mode 100644
index 0000000000..2161094dcc
--- /dev/null
+++ b/samples/app-region-selection/python/.vscode/tasks.json
@@ -0,0 +1,78 @@
+// This file is automatically generated by Teams Toolkit.
+// The teamsfx tasks defined in this file require Teams Toolkit version >= 5.0.0.
+// See https://aka.ms/teamsfx-tasks for details on how to customize each task.
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Prepare Teams App Resources",
+ "dependsOn": [
+ "Validate prerequisites",
+ "Start local tunnel",
+ "Provision",
+ "Deploy"
+ ],
+ "dependsOrder": "sequence"
+ },
+ {
+ // Check all required prerequisites.
+ // See https://aka.ms/teamsfx-tasks/check-prerequisites to know the details and how to customize the args.
+ "label": "Validate prerequisites",
+ "type": "teamsfx",
+ "command": "debug-check-prerequisites",
+ "args": {
+ "prerequisites": [
+ "m365Account", // Sign-in prompt for Microsoft 365 account, then validate if the account enables the sideloading permission.
+ "portOccupancy" // Validate available ports to ensure those debug ones are not occupied.
+ ],
+ "portOccupancy": [
+ 3978, // app service port
+ ]
+ }
+ },
+ {
+ // Start the local tunnel service to forward public URL to local port and inspect traffic.
+ // See https://aka.ms/teamsfx-tasks/local-tunnel for the detailed args definitions.
+ "label": "Start local tunnel",
+ "type": "teamsfx",
+ "command": "debug-start-local-tunnel",
+ "args": {
+ "type": "dev-tunnel",
+ "ports": [
+ {
+ "portNumber": 3978,
+ "protocol": "http",
+ "access": "public",
+ "writeToEnvironmentFile": {
+ "endpoint": "BOT_ENDPOINT", // output tunnel endpoint as BOT_ENDPOINT
+ "domain": "BOT_DOMAIN" // output tunnel domain as BOT_DOMAIN
+ }
+ }
+ ],
+ "env": "local"
+ },
+ "isBackground": true,
+ "problemMatcher": "$teamsfx-local-tunnel-watch"
+ },
+ {
+ // Create the debug resources.
+ // See https://aka.ms/teamsfx-tasks/provision to know the details and how to customize the args.
+ "label": "Provision",
+ "type": "teamsfx",
+ "command": "provision",
+ "args": {
+ "env": "local"
+ }
+ },
+ {
+ // Build project.
+ // See https://aka.ms/teamsfx-tasks/deploy to know the details and how to customize the args.
+ "label": "Deploy",
+ "type": "teamsfx",
+ "command": "deploy",
+ "args": {
+ "env": "local"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/samples/app-region-selection/python/ConfigData/Regions.json b/samples/app-region-selection/python/ConfigData/Regions.json
new file mode 100644
index 0000000000..05c64eb096
--- /dev/null
+++ b/samples/app-region-selection/python/ConfigData/Regions.json
@@ -0,0 +1,28 @@
+{
+ "regionDomains": [
+ {
+ "id": 1,
+ "region": "North America",
+ "country": "United States of America (USA)",
+ "domain": ".us"
+ },
+ {
+ "id": 2,
+ "region": "Europe",
+ "country": "United Kingdom (UK)",
+ "domain": ".uk"
+ },
+ {
+ "id": 3,
+ "region": "Asia & Pacific",
+ "country": "India",
+ "domain": ".in"
+ },
+ {
+ "id": 4,
+ "region": "Asia & Pacific",
+ "country": "China",
+ "domain": ".cn"
+ }
+ ]
+}
diff --git a/samples/app-region-selection/python/Images/region-change-bot.png b/samples/app-region-selection/python/Images/region-change-bot.png
new file mode 100644
index 0000000000..8363c1f15f
Binary files /dev/null and b/samples/app-region-selection/python/Images/region-change-bot.png differ
diff --git a/samples/app-region-selection/python/Images/region-config.png b/samples/app-region-selection/python/Images/region-config.png
new file mode 100644
index 0000000000..872bbb13d0
Binary files /dev/null and b/samples/app-region-selection/python/Images/region-config.png differ
diff --git a/samples/app-region-selection/python/Images/region-details-bot.png b/samples/app-region-selection/python/Images/region-details-bot.png
new file mode 100644
index 0000000000..ef1439bf97
Binary files /dev/null and b/samples/app-region-selection/python/Images/region-details-bot.png differ
diff --git a/samples/app-region-selection/python/Images/region-details.png b/samples/app-region-selection/python/Images/region-details.png
new file mode 100644
index 0000000000..11c42533fd
Binary files /dev/null and b/samples/app-region-selection/python/Images/region-details.png differ
diff --git a/samples/app-region-selection/python/Images/region-selection.gif b/samples/app-region-selection/python/Images/region-selection.gif
new file mode 100644
index 0000000000..22d88cfd62
Binary files /dev/null and b/samples/app-region-selection/python/Images/region-selection.gif differ
diff --git a/samples/app-region-selection/python/README.md b/samples/app-region-selection/python/README.md
new file mode 100644
index 0000000000..3284def9e8
--- /dev/null
+++ b/samples/app-region-selection/python/README.md
@@ -0,0 +1,121 @@
+---
+page_type: sample
+description: This Microsoft Teams app allows users to select and set a region using a Bot and Tab.
+products:
+- office-teams
+- office
+- office-365
+languages:
+- python
+extensions:
+ contentType: samples
+ createdDate: "13-02-2025 13:38:25"
+urlFragment: officedev-microsoft-teams-samples-app-region-selection-python
+---
+
+# Region Selection App
+
+A Microsoft Teams sample app for region selection, leveraging both Bot and Tab interactions. The app features Adaptive Cards to facilitate region configuration and provides a seamless experience to manage data center selection through the Teams client.
+
+Bot Framework v4 Region Selection sample.
+
+This bot has been created using [Bot Framework](https://dev.botframework.com), for the region selection for the app's data center using Bot and Tab.
+
+## Included Features
+* Bots
+* Tabs
+* Adaptive Cards
+
+## Interaction with app
+
+
+## Prerequisites
+
+- Microsoft Teams is installed and you have an account
+- [Python SDK](https://www.python.org/downloads/) min version 3.6
+- [dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows) or [ngrok](https://ngrok.com/) latest version or equivalent tunnelling solution
+
+
+## Run the app (Using Teams Toolkit for Visual Studio Code)
+
+The simplest way to run this sample in Teams is to use Teams Toolkit for Visual Studio Code.
+
+1. Ensure you have downloaded and installed [Visual Studio Code](https://code.visualstudio.com/docs/setup/setup-overview)
+1. Install the [Teams Toolkit extension](https://marketplace.visualstudio.com/items?itemName=TeamsDevApp.ms-teams-vscode-extension) and [Python Extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python)
+1. Select **File > Open Folder** in VS Code and choose this samples directory from the repo
+1. Press **CTRL+Shift+P** to open the command box and enter **Python: Create Environment** to create and activate your desired virtual environment. Remember to select `requirements.txt` as dependencies to install when creating the virtual environment.
+1. Using the extension, sign in with your Microsoft 365 account where you have permissions to upload custom apps
+1. Select **Debug > Start Debugging** or **F5** to run the app in a Teams web client.
+1. In the browser that launches, select the **Add** button to install the app to Teams.
+
+> If you do not have permission to upload custom apps (sideloading), Teams Toolkit will recommend creating and using a Microsoft 365 Developer Program account - a free program to get your own dev environment sandbox that includes Teams.
+
+## Run the app (Manually Uploading to Teams)
+
+> Note these instructions are for running the sample on your local machine, the tunnelling solution is required because
+the Teams service needs to call into the bot.
+
+1) Clone the repository
+
+ ```bash
+ git clone https://github.com/OfficeDev/Microsoft-Teams-Samples.git
+ ```
+
+2) Run ngrok - point to port 3978
+
+ ```bash
+ ngrok http 3978 --host-header="localhost:3978"
+ ```
+
+ Alternatively, you can also use the `dev tunnels`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below:
+
+ ```bash
+ devtunnel host -p 3978 --allow-anonymous
+ ```
+
+3) Create [Azure Bot resource resource](https://docs.microsoft.com/azure/bot-service/bot-service-quickstart-registration) in Azure
+ - Use the current `https` URL you were given by running the tunneling application. Append with the path `/api/messages` used by this sample
+ - Ensure that you've [enabled the Teams Channel](https://docs.microsoft.com/azure/bot-service/channel-connect-teams?view=azure-bot-service-4.0)
+ - __*If you don't have an Azure account*__ you can use this [Azure free account here](https://azure.microsoft.com/free/)
+
+4) In a terminal, go to `samples\app-region-selection`
+
+5) Activate your desired virtual environment
+
+6) Install dependencies by running ```pip install -r requirements.txt``` in the project folder.
+
+7) Update the `config.py` configuration for the bot to use the Microsoft App Id and App Password from the Bot Framework registration. (Note the App Password is referred to as the "client secret" in the azure portal and you can always create a new client secret anytime.)
+
+8) __*This step is specific to Teams.*__
+ - **Edit** the `manifest.json` contained in the `appManifest` folder to replace your Microsoft App Id (that was created when you registered your bot earlier) *everywhere* you see the place holder string `${{AAD_APP_CLIENT_ID}}` and `${{TEAMS_APP_ID}}` (depending on the scenario the Microsoft App Id may occur multiple times in the `manifest.json`)
+ - **Zip** up the contents of the `appManifest` folder to create a `manifest.zip`
+ - **Upload** the `manifest.zip` to Teams (in the Apps view click "Upload a custom app")
+
+9) Run your bot with `python app.py`
+
+## Running the sample
+
+Install the Region Selection App manifest in Microsoft Teams. @mention the region selection bot to start the conversation.
+- Bot sends an Adaptive card in chat
+
+- Select the region from the card. Bot sets the selected region and notify user in chat
+
+
+## Interacting with Region Selection Tab
+
+- Set up the region selection app as a Tab in channel
+
+- Tab will display the selected region
+
+
+## Deploy the bot to Azure
+
+To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions.
+
+
+## Further reading
+- [Overview for Microsoft Teams App](https://docs.microsoft.com/microsoftteams/platform/build-your-first-app/build-first-app-overview)
+- [Build a Configurable Tab for Microsoft Teams App](https://docs.microsoft.com/microsoftteams/platform/build-your-first-app/build-channel-tab)
+- [Build a Bot](https://docs.microsoft.com/microsoftteams/platform/build-your-first-app/build-bot)
+
+
+
\ No newline at end of file
diff --git a/samples/app-region-selection/python/app.py b/samples/app-region-selection/python/app.py
new file mode 100644
index 0000000000..707d0039a4
--- /dev/null
+++ b/samples/app-region-selection/python/app.py
@@ -0,0 +1,115 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import sys
+import traceback
+import json
+import os
+import requests
+from http import HTTPStatus
+from flask import Flask, request, render_template, redirect, url_for, jsonify
+from aiohttp import web
+from botbuilder.core import (
+ BotFrameworkAdapterSettings,
+ TurnContext,
+ BotFrameworkAdapter,
+ MemoryStorage,
+ UserState,
+)
+from botbuilder.schema import Activity
+from botbuilder.core.integration import aiohttp_error_middleware
+from bots import RegionSelectionTab
+from config import DefaultConfig
+
+# Load configuration
+CONFIG = DefaultConfig()
+
+# Flask App Setup
+app = Flask(__name__)
+
+# Bot Adapter
+SETTINGS = BotFrameworkAdapterSettings(CONFIG.APP_ID, CONFIG.APP_PASSWORD)
+ADAPTER = BotFrameworkAdapter(SETTINGS)
+
+# Error Handling
+async def on_error(context: TurnContext, error: Exception):
+ print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
+ traceback.print_exc()
+ try:
+ await context.send_activity("The bot encountered an error or bug.")
+ except Exception as send_error:
+ print(f"Failed to send error message: {send_error}", file=sys.stderr)
+
+ADAPTER.on_turn_error = on_error
+
+# Bot State
+memory = MemoryStorage()
+user_state = UserState(memory)
+BOT = RegionSelectionTab(user_state)
+
+# JSON Data for Regions
+CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'ConfigData', 'Regions.json')
+
+def load_regions():
+ with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
+ return json.load(f)
+
+# **🔹 Flask Web Routes**
+@app.route('/configure')
+def index():
+ domainlist = load_regions()
+ return render_template('index.html', regionDomains=domainlist['regionDomains'])
+
+@app.route('/welcome')
+def welcome():
+ selected_domain = request.args.get('selectedDomain')
+ if not selected_domain:
+ return redirect(url_for('index'))
+ return render_template('welcome.html', selected_domain=selected_domain)
+
+# **🔹 Proxy `/api/messages` to Aiohttp Bot Backend**
+BOT_API_URL = "http://localhost:5001/api/messages" # Change if needed
+
+@app.route("/api/messages", methods=["POST"])
+def proxy_messages():
+ try:
+ response = requests.post(BOT_API_URL, json=request.json, headers=request.headers)
+ return (response.text, response.status_code, response.headers.items())
+ except Exception as e:
+ return jsonify({"error": str(e)}), 500
+
+# **🔹 Aiohttp Bot Server**
+async def messages(request: web.Request) -> web.Response:
+ if "application/json" in request.headers["Content-Type"]:
+ body = await request.json()
+ else:
+ return web.Response(status=HTTPStatus.UNSUPPORTED_MEDIA_TYPE)
+
+ activity = Activity().deserialize(body)
+ auth_header = request.headers.get("Authorization", "")
+
+ try:
+ response = await ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
+ if response:
+ return web.json_response(data=response.body, status=response.status)
+ return web.Response(status=HTTPStatus.OK)
+ except Exception as e:
+ print(f"Exception in messages: {e}", file=sys.stderr)
+ return web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
+
+APP = web.Application(middlewares=[aiohttp_error_middleware])
+APP.router.add_post("/api/messages", messages)
+
+# **🔹 Start Both Flask & Aiohttp**
+if __name__ == "__main__":
+ import threading
+
+ def run_flask():
+ app.run(host="0.0.0.0", port=3978, debug=False, use_reloader=False)
+
+ def run_aiohttp():
+ web.run_app(APP, host="0.0.0.0", port=5001)
+
+ # Start Flask & Aiohttp in Parallel
+ threading.Thread(target=run_flask, daemon=True).start()
+ run_aiohttp()
\ No newline at end of file
diff --git a/samples/app-region-selection/python/appManifest/color.png b/samples/app-region-selection/python/appManifest/color.png
new file mode 100644
index 0000000000..b8cf81afbe
Binary files /dev/null and b/samples/app-region-selection/python/appManifest/color.png differ
diff --git a/samples/app-region-selection/python/appManifest/manifest.json b/samples/app-region-selection/python/appManifest/manifest.json
new file mode 100644
index 0000000000..36033e7418
--- /dev/null
+++ b/samples/app-region-selection/python/appManifest/manifest.json
@@ -0,0 +1,55 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
+ "manifestVersion": "1.16",
+ "version": "1.0.0",
+ "id": "${{TEAMS_APP_ID}}",
+ "packageName": "com.microsoft.com.testapp",
+ "developer": {
+ "name": "Microsoft",
+ "websiteUrl": "https://www.microsoft.com",
+ "privacyUrl": "https://www.microsoft.com/privacy",
+ "termsOfUseUrl": "https://www.microsoft.com/termsofuse"
+ },
+ "icons": {
+ "color": "color.png",
+ "outline": "outline.png"
+ },
+ "name": {
+ "short": "Region Selection App",
+ "full": "It will help to select Data center's region"
+ },
+ "description": {
+ "short": "Microsoft Teams app for region selection using Bot and Tab with adaptive cards.",
+ "full": "This Microsoft Teams app allows users to select and set a region using a Bot and Tab."
+ },
+ "accentColor": "#FFFFFF",
+ "configurableTabs": [
+ {
+ "configurationUrl": "https://${{BOT_DOMAIN}}/configure",
+ "canUpdateConfiguration": true,
+ "scopes": [
+ "team",
+ "groupchat"
+ ]
+ }
+ ],
+ "bots": [
+ {
+ "botId": "${{AAD_APP_CLIENT_ID}}",
+ "scopes": [
+ "personal",
+ "team",
+ "groupchat"
+ ],
+ "supportsFiles": false,
+ "isNotificationOnly": false
+ }
+ ],
+ "permissions": [
+ "identity",
+ "messageTeamMembers"
+ ],
+ "validDomains": [
+ "${{BOT_DOMAIN}}"
+ ]
+}
\ No newline at end of file
diff --git a/samples/app-region-selection/python/appManifest/outline.png b/samples/app-region-selection/python/appManifest/outline.png
new file mode 100644
index 0000000000..2c3bf6fa65
Binary files /dev/null and b/samples/app-region-selection/python/appManifest/outline.png differ
diff --git a/samples/app-region-selection/python/assets/sample.json b/samples/app-region-selection/python/assets/sample.json
new file mode 100644
index 0000000000..0ff97e26e5
--- /dev/null
+++ b/samples/app-region-selection/python/assets/sample.json
@@ -0,0 +1,68 @@
+[
+ {
+ "name": "officedev-microsoft-teams-samples-app-region-selection-python",
+ "source": "officeDev",
+ "title": "Bot and tab to select a region",
+ "shortDescription": "This Microsoft Teams app allows users to select and set a region using a Bot and Tab.",
+ "url": "https://github.com/OfficeDev/Microsoft-Teams-Samples/tree/main/samples/app-region-selection/python",
+ "longDescription": [
+ "This Microsoft Teams sample app enables region selection for data centers using both a Bot and a Tab, with interactions through adaptive cards. It demonstrates how to build configurable tabs and conversational bots with Microsoft Teams SDK."
+ ],
+ "creationDateTime": "2025-02-13",
+ "updateDateTime": "2025-02-13",
+ "products": [
+ "Teams"
+ ],
+ "metadata": [
+ {
+ "key": "TEAMS-SAMPLE-SOURCE",
+ "value": "OfficeDev"
+ },
+ {
+ "key": "TEAMS-SERVER-LANGUAGE",
+ "value": "python"
+ },
+ {
+ "key": "TEAMS-SERVER-PLATFORM",
+ "value": "python"
+ },
+ {
+ "key": "TEAMS-FEATURES",
+ "value": "bot"
+ }
+ ],
+ "thumbnails": [
+ {
+ "type": "image",
+ "order": 100,
+ "url": "https://raw.githubusercontent.com/OfficeDev/Microsoft-Teams-Samples/main/samples/app-region-selection/python/RegionSectionApp/Images/region-selection.gif",
+ "alt": "Solution UX showing bot and tab to select a region"
+ }
+ ],
+ "authors": [
+ {
+ "gitHubAccount": "OfficeDev",
+ "pictureUrl": "https://avatars.githubusercontent.com/u/6789362?s=200&v=4",
+ "name": "OfficeDev"
+ }
+ ],
+ "references": [
+ {
+ "name": "Teams developer documentation",
+ "url": "https://aka.ms/TeamsPlatformDocs"
+ },
+ {
+ "name": "Teams developer questions",
+ "url": "https://aka.ms/TeamsPlatformFeedback"
+ },
+ {
+ "name": "Teams development videos from Microsoft",
+ "url": "https://aka.ms/sample-ref-teams-vids-from-microsoft"
+ },
+ {
+ "name": "Teams development videos from the community",
+ "url": "https://aka.ms/community/videos/m365powerplatform"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/samples/app-region-selection/python/bots/__init__.py b/samples/app-region-selection/python/bots/__init__.py
new file mode 100644
index 0000000000..bb97829b7c
--- /dev/null
+++ b/samples/app-region-selection/python/bots/__init__.py
@@ -0,0 +1,3 @@
+from .bot_activity_handler import RegionSelectionTab
+
+__all__ = ["RegionSelectionTab"]
\ No newline at end of file
diff --git a/samples/app-region-selection/python/bots/bot_activity_handler.py b/samples/app-region-selection/python/bots/bot_activity_handler.py
new file mode 100644
index 0000000000..2ccbbf0b21
--- /dev/null
+++ b/samples/app-region-selection/python/bots/bot_activity_handler.py
@@ -0,0 +1,185 @@
+import json
+from botbuilder.core import ActivityHandler, TurnContext, UserState, MessageFactory, CardFactory
+from botbuilder.schema import HeroCard, CardAction, ActionTypes
+from typing import List
+import os
+
+class RegionSelectionTab(ActivityHandler):
+ def __init__(self, user_state: UserState):
+ self._user_state = user_state
+
+ async def on_message_activity(self, turn_context: TurnContext):
+ welcome_user_state_accessor = self._user_state.create_property("WelcomeUserState")
+ did_bot_welcome_user = await welcome_user_state_accessor.get(turn_context, lambda: WelcomeUserState())
+
+ text = turn_context.activity.text.lower()
+
+ if did_bot_welcome_user.did_user_selected_domain and text in ["change", "yes"]:
+ await self.send_change_domain_confirmation_card(turn_context)
+ return
+
+ if text in ["reset", "change", "yes"]:
+ await self.send_domain_lists_card(turn_context)
+ elif text in ["no", "cancel"]:
+ await self.welcome_card(turn_context)
+ else:
+ await self.send_welcome_intro_card(turn_context)
+
+ async def on_members_added(self, members_added: List, turn_context: TurnContext):
+ for member in members_added:
+ if member.id != turn_context.activity.recipient.id:
+ welcome_user_state_accessor = self._user_state.create_property("WelcomeUserState")
+ did_bot_welcome_user = await welcome_user_state_accessor.get(turn_context, lambda: WelcomeUserState())
+
+ if did_bot_welcome_user.did_user_selected_domain:
+ did_bot_welcome_user.did_user_selected_domain = False
+ did_bot_welcome_user.selected_region = ""
+ did_bot_welcome_user.selected_domain = ""
+
+ await self.send_welcome_intro_card(turn_context)
+
+ async def send_welcome_intro_card(self, turn_context: TurnContext):
+ domain, region = "", ""
+ if turn_context.activity.text and self.is_any_domain_selected(turn_context.activity.text):
+ await self.welcome_card(turn_context)
+ return
+
+ welcome_user_state_accessor = self._user_state.create_property("WelcomeUserState")
+ did_bot_welcome_user = await welcome_user_state_accessor.get(turn_context, lambda: WelcomeUserState())
+
+ if did_bot_welcome_user.did_user_selected_domain:
+ domain = did_bot_welcome_user.selected_domain
+ region = did_bot_welcome_user.selected_region
+ else:
+ domain, region = self.get_default_info(turn_context)
+
+ welcome_msg = f"Your default Region is {region}."
+ card = HeroCard(
+ title="Welcome to Region Selection App!",
+ subtitle="This will help you to choose your data center's region.",
+ text=welcome_msg + " Would you like to change region?",
+ buttons=[
+ CardAction(type=ActionTypes.message_back, title="Yes", text="Yes"),
+ CardAction(type=ActionTypes.message_back, title="No", text="No")
+ ]
+ )
+
+ response = MessageFactory.attachment(CardFactory.hero_card(card))
+ await turn_context.send_activity(response)
+
+ def get_default_info(self, turn_context: TurnContext):
+ service_url = turn_context.activity.service_url
+ domain = service_url[service_url.rfind(".") + 1:].strip("/")
+ region = turn_context.activity.locale
+ return domain, region
+
+ def get_selected_info(self, text):
+ domain, region = "", ""
+ if len(text.split("-")) > 1:
+ domain = text.split("-")[0].strip()
+
+ # Read JSON data from file
+ file_path = os.path.join(os.getcwd(), "ConfigData", "Regions.json")
+ with open(file_path, 'r') as file:
+ json_data = json.load(file)
+ selected_info = next((item for item in json_data["regionDomains"] if item["region"] == domain), None)
+
+ if selected_info:
+ region = selected_info["region"]
+ domain = selected_info["domain"]
+
+ return domain, region
+
+ async def send_domain_lists_card(self, turn_context: TurnContext):
+ # Read JSON data from file
+ file_path = os.path.join(os.getcwd(), "ConfigData", "Regions.json")
+ with open(file_path, 'r') as file:
+ json_data = json.load(file)
+
+ region_button_list = [
+ CardAction(
+ type=ActionTypes.message_back,
+ title=f"{item['region']} - {item['country']}",
+ text=f"{item['region']} - {item['country']}",
+ display_text=f"{item['region']} - {item['country']}",
+ image="https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0"
+ ) for item in json_data["regionDomains"]
+ ]
+
+ card = HeroCard(
+ text="Please select your region,",
+ buttons=region_button_list
+ )
+
+ response = MessageFactory.attachment(CardFactory.hero_card(card))
+ await turn_context.send_activity(response)
+
+ async def welcome_card(self, turn_context: TurnContext):
+ welcome_user_state_accessor = self._user_state.create_property("WelcomeUserState")
+ did_bot_welcome_user = await welcome_user_state_accessor.get(turn_context, lambda: WelcomeUserState())
+
+ user_name = turn_context.activity.from_property.name
+ domain_name, region_name = "", ""
+
+ # Get selected info
+ domain_name, region_name = self.get_selected_info(turn_context.activity.text)
+
+ if not domain_name and did_bot_welcome_user.did_user_selected_domain:
+ domain_name = did_bot_welcome_user.selected_domain
+ region_name = did_bot_welcome_user.selected_region
+
+ if not domain_name:
+ domain_name, region_name = self.get_default_info(turn_context)
+
+ card = HeroCard(
+ title=f"Welcome {user_name}, ",
+ subtitle=f"You are in {region_name} Region's Data Center",
+ text="If you want to change data center's region, please enter text 'Change'"
+ )
+
+ did_bot_welcome_user.did_user_selected_domain = True
+ did_bot_welcome_user.selected_domain = domain_name
+ did_bot_welcome_user.selected_region = region_name
+
+ await self._user_state.save_changes(turn_context)
+ response = MessageFactory.attachment(CardFactory.hero_card(card))
+ await turn_context.send_activity(response)
+
+ async def send_change_domain_confirmation_card(self, turn_context: TurnContext):
+ user_name = turn_context.activity.from_property.name
+ welcome_user_state_accessor = self._user_state.create_property("WelcomeUserState")
+ did_bot_welcome_user = await welcome_user_state_accessor.get(turn_context, lambda: WelcomeUserState())
+
+ domain_button_list = [
+ CardAction(type=ActionTypes.message_back, title="Reset", text="Reset"),
+ CardAction(type=ActionTypes.message_back, title="Cancel", text="Cancel")
+ ]
+
+ card = HeroCard(
+ text=f"Hi {user_name}, You have already selected your data center region: {did_bot_welcome_user.selected_region}. Would you like to change this?",
+ buttons=domain_button_list
+ )
+
+ response = MessageFactory.attachment(CardFactory.hero_card(card))
+ await turn_context.send_activity(response)
+
+ def is_any_domain_selected(self, text):
+ domain = ""
+ if len(text.split("-")) > 1:
+ domain = text.split("-")[0].strip()
+
+ if not domain:
+ return False
+
+ file_path = os.path.join(os.getcwd(), "ConfigData", "Regions.json")
+ with open(file_path, 'r') as file:
+ json_data = json.load(file)
+ is_domain_selected = any(item["region"] == domain for item in json_data["regionDomains"])
+
+ return is_domain_selected
+
+class WelcomeUserState:
+ def __init__(self):
+ self.did_user_selected_domain = False
+ self.selected_region = ""
+ self.selected_domain = ""
\ No newline at end of file
diff --git a/samples/app-region-selection/python/config.py b/samples/app-region-selection/python/config.py
new file mode 100644
index 0000000000..7815d9e28d
--- /dev/null
+++ b/samples/app-region-selection/python/config.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python3
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import os
+
+""" Bot Configuration """
+
+
+class DefaultConfig:
+ """ Bot Configuration """
+
+ PORT = 3978
+ APP_ID = os.environ.get("MicrosoftAppId", "<
Welcome to Region Selection App!
+
+