diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 669ad04b..4499c2c0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,4 +3,7 @@ # https://github.com/blog/2392-introducing-code-owners # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -* @adilei @HenryJammes \ No newline at end of file +* @adilei @HenryJammes + +# ESS team owns their own content +/EmployeeSelfServiceAgent/ @microsoft/ess-contributors \ No newline at end of file diff --git a/README.md b/README.md index 40b75354..dfde9e66 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Older samples and labs, largely focused on Power Virtual Agents, have been moved | Guidance documentation | [aka.ms/CopilotStudioGuidance](https://aka.ms/CopilotStudioGuidance) | | Try Copilot Studio | [aka.ms/TryCopilotStudio](https://aka.ms/TryCopilotStudio) | | Copilot Acceleration Team technical blog | [aka.ms/TheCustomEngine](https://aka.ms/TheCustomEngine) | +| Samples browser | [microsoft.github.io/CopilotStudioSamples](https://microsoft.github.io/CopilotStudioSamples/) | ## Microsoft Copilot Studio and Agents SDK links diff --git a/_config.yml b/_config.yml index 33613916..c1721dcb 100644 --- a/_config.yml +++ b/_config.yml @@ -5,6 +5,10 @@ baseurl: "/CopilotStudioSamples" theme: just-the-docs logo: "/assets/images/logo.png" + +# Mermaid diagram support +mermaid: + version: "11" favicon_ico: "/favicon.ico" # Default layout for all pages diff --git a/contact-center/README.md b/contact-center/README.md index d8e23fdf..0ff95b51 100644 --- a/contact-center/README.md +++ b/contact-center/README.md @@ -13,7 +13,7 @@ Integrate Copilot Studio agents with contact center platforms and live agent han | Folder | Description | |--------|-------------| -| [Skill Handoff](./servicenow/HandoverToLiveAgentUsingSkill/) | Skill-based handoff to live agents using M365 Agents SDK | -| [Salesforce](./servicenow/Salesforce/) | Salesforce Einstein Bot integration via DirectLine API | -| [ServiceNow](./servicenow/ServiceNow/) | ServiceNow Virtual Agent integration via DirectLine | +| [Skill Handoff](./skill-handoff/) | Skill-based handoff to live agents using M365 Agents SDK | +| [Salesforce](./salesforce/) | Salesforce Einstein Bot integration via DirectLine API | +| [ServiceNow](./servicenow/) | ServiceNow Virtual Agent integration via DirectLine | | [Genesys Handoff](./genesys-handoff/) | Live agent handoff to Genesys (.NET) — *M365 Agents SDK repo* | diff --git a/contact-center/servicenow/Salesforce/ApexClasses/DL_GetActivity.cls b/contact-center/salesforce/ApexClasses/DL_GetActivity.cls similarity index 100% rename from contact-center/servicenow/Salesforce/ApexClasses/DL_GetActivity.cls rename to contact-center/salesforce/ApexClasses/DL_GetActivity.cls diff --git a/contact-center/servicenow/Salesforce/ApexClasses/DL_GetConversation.cls b/contact-center/salesforce/ApexClasses/DL_GetConversation.cls similarity index 100% rename from contact-center/servicenow/Salesforce/ApexClasses/DL_GetConversation.cls rename to contact-center/salesforce/ApexClasses/DL_GetConversation.cls diff --git a/contact-center/servicenow/Salesforce/ApexClasses/DL_PostActivity.cls b/contact-center/salesforce/ApexClasses/DL_PostActivity.cls similarity index 100% rename from contact-center/servicenow/Salesforce/ApexClasses/DL_PostActivity.cls rename to contact-center/salesforce/ApexClasses/DL_PostActivity.cls diff --git a/contact-center/servicenow/Salesforce/Metadata/classes/DL_GetActivity.cls-meta.xml b/contact-center/salesforce/Metadata/classes/DL_GetActivity.cls-meta.xml similarity index 100% rename from contact-center/servicenow/Salesforce/Metadata/classes/DL_GetActivity.cls-meta.xml rename to contact-center/salesforce/Metadata/classes/DL_GetActivity.cls-meta.xml diff --git a/contact-center/servicenow/Salesforce/Metadata/classes/DL_GetConversation.cls-meta.xml b/contact-center/salesforce/Metadata/classes/DL_GetConversation.cls-meta.xml similarity index 100% rename from contact-center/servicenow/Salesforce/Metadata/classes/DL_GetConversation.cls-meta.xml rename to contact-center/salesforce/Metadata/classes/DL_GetConversation.cls-meta.xml diff --git a/contact-center/servicenow/Salesforce/Metadata/classes/DL_PostActivity.cls-meta.xml b/contact-center/salesforce/Metadata/classes/DL_PostActivity.cls-meta.xml similarity index 100% rename from contact-center/servicenow/Salesforce/Metadata/classes/DL_PostActivity.cls-meta.xml rename to contact-center/salesforce/Metadata/classes/DL_PostActivity.cls-meta.xml diff --git a/contact-center/servicenow/Salesforce/Metadata/externalCredentials/Directline.externalCredential-meta.xml b/contact-center/salesforce/Metadata/externalCredentials/Directline.externalCredential-meta.xml similarity index 100% rename from contact-center/servicenow/Salesforce/Metadata/externalCredentials/Directline.externalCredential-meta.xml rename to contact-center/salesforce/Metadata/externalCredentials/Directline.externalCredential-meta.xml diff --git a/contact-center/servicenow/Salesforce/Metadata/namedCredentials/Directline.namedCredential-meta.xml b/contact-center/salesforce/Metadata/namedCredentials/Directline.namedCredential-meta.xml similarity index 100% rename from contact-center/servicenow/Salesforce/Metadata/namedCredentials/Directline.namedCredential-meta.xml rename to contact-center/salesforce/Metadata/namedCredentials/Directline.namedCredential-meta.xml diff --git a/contact-center/servicenow/Salesforce/Metadata/remoteSiteSettings/DirectLine.remoteSite-meta.xml b/contact-center/salesforce/Metadata/remoteSiteSettings/DirectLine.remoteSite-meta.xml similarity index 100% rename from contact-center/servicenow/Salesforce/Metadata/remoteSiteSettings/DirectLine.remoteSite-meta.xml rename to contact-center/salesforce/Metadata/remoteSiteSettings/DirectLine.remoteSite-meta.xml diff --git a/contact-center/servicenow/Salesforce/README.md b/contact-center/salesforce/README.md similarity index 100% rename from contact-center/servicenow/Salesforce/README.md rename to contact-center/salesforce/README.md diff --git a/contact-center/servicenow/Salesforce/scripts/deploy.ps1 b/contact-center/salesforce/scripts/deploy.ps1 similarity index 100% rename from contact-center/servicenow/Salesforce/scripts/deploy.ps1 rename to contact-center/salesforce/scripts/deploy.ps1 diff --git a/contact-center/servicenow/Salesforce/scripts/deploy.sh b/contact-center/salesforce/scripts/deploy.sh similarity index 100% rename from contact-center/servicenow/Salesforce/scripts/deploy.sh rename to contact-center/salesforce/scripts/deploy.sh diff --git a/contact-center/servicenow/Salesforce/scripts/grant-bot-permissions.ps1 b/contact-center/salesforce/scripts/grant-bot-permissions.ps1 similarity index 100% rename from contact-center/servicenow/Salesforce/scripts/grant-bot-permissions.ps1 rename to contact-center/salesforce/scripts/grant-bot-permissions.ps1 diff --git a/contact-center/servicenow/Salesforce/scripts/grant-bot-permissions.sh b/contact-center/salesforce/scripts/grant-bot-permissions.sh similarity index 100% rename from contact-center/servicenow/Salesforce/scripts/grant-bot-permissions.sh rename to contact-center/salesforce/scripts/grant-bot-permissions.sh diff --git a/contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.funcignore b/contact-center/servicenow/DirectLineAzureFunction/.funcignore similarity index 100% rename from contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.funcignore rename to contact-center/servicenow/DirectLineAzureFunction/.funcignore diff --git a/contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.gitignore b/contact-center/servicenow/DirectLineAzureFunction/.gitignore similarity index 100% rename from contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.gitignore rename to contact-center/servicenow/DirectLineAzureFunction/.gitignore diff --git a/contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.vscode/extensions.json b/contact-center/servicenow/DirectLineAzureFunction/.vscode/extensions.json similarity index 100% rename from contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.vscode/extensions.json rename to contact-center/servicenow/DirectLineAzureFunction/.vscode/extensions.json diff --git a/contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.vscode/launch.json b/contact-center/servicenow/DirectLineAzureFunction/.vscode/launch.json similarity index 100% rename from contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.vscode/launch.json rename to contact-center/servicenow/DirectLineAzureFunction/.vscode/launch.json diff --git a/contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.vscode/settings.json b/contact-center/servicenow/DirectLineAzureFunction/.vscode/settings.json similarity index 100% rename from contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.vscode/settings.json rename to contact-center/servicenow/DirectLineAzureFunction/.vscode/settings.json diff --git a/contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.vscode/tasks.json b/contact-center/servicenow/DirectLineAzureFunction/.vscode/tasks.json similarity index 100% rename from contact-center/servicenow/ServiceNow/DirectLineAzureFunction/.vscode/tasks.json rename to contact-center/servicenow/DirectLineAzureFunction/.vscode/tasks.json diff --git a/contact-center/servicenow/ServiceNow/DirectLineAzureFunction/host.json b/contact-center/servicenow/DirectLineAzureFunction/host.json similarity index 100% rename from contact-center/servicenow/ServiceNow/DirectLineAzureFunction/host.json rename to contact-center/servicenow/DirectLineAzureFunction/host.json diff --git a/contact-center/servicenow/ServiceNow/DirectLineAzureFunction/package.json b/contact-center/servicenow/DirectLineAzureFunction/package.json similarity index 100% rename from contact-center/servicenow/ServiceNow/DirectLineAzureFunction/package.json rename to contact-center/servicenow/DirectLineAzureFunction/package.json diff --git a/contact-center/servicenow/ServiceNow/DirectLineAzureFunction/src/functions/relayToDirectLine.js b/contact-center/servicenow/DirectLineAzureFunction/src/functions/relayToDirectLine.js similarity index 100% rename from contact-center/servicenow/ServiceNow/DirectLineAzureFunction/src/functions/relayToDirectLine.js rename to contact-center/servicenow/DirectLineAzureFunction/src/functions/relayToDirectLine.js diff --git a/contact-center/servicenow/ServiceNow/DirectLineAzureFunction/src/index.js b/contact-center/servicenow/DirectLineAzureFunction/src/index.js similarity index 100% rename from contact-center/servicenow/ServiceNow/DirectLineAzureFunction/src/index.js rename to contact-center/servicenow/DirectLineAzureFunction/src/index.js diff --git a/contact-center/servicenow/README.md b/contact-center/servicenow/README.md index dbd64973..ea33f2e6 100644 --- a/contact-center/servicenow/README.md +++ b/contact-center/servicenow/README.md @@ -1,15 +1,17 @@ --- -nav_exclude: true -search_exclude: true +title: ServiceNow +parent: Contact Center +nav_order: 3 --- -# Engagement Hub Integrations +# Copilot Studio - ServiceNow Integration Samples -Integrate Copilot Studio agents with engagement hub platforms including ServiceNow and Salesforce. +This folder contains sample code for integrating Microsoft Copilot Studio with ServiceNow Virtual Agent, enabling seamless handoff from virtual agent to live agent. -## Contents +## Assets Included -| Folder | Description | -|--------|-------------| -| [HandoverToLiveAgentUsingSkill/](./HandoverToLiveAgentUsingSkill/) | Skill-based handoff to live agents | -| [Salesforce/](./Salesforce/) | Salesforce Einstein Bot integration via DirectLine API | -| [ServiceNow/](./ServiceNow/) | ServiceNow integration via DirectLine | +| Asset | Description | File | +|-------|-------------|------| +| **Azure Function** | Relay service that bridges ServiceNow with the Direct Line API | [`DirectLineAzureFunction/relayToDirectLine`](./DirectLineAzureFunction/) | +| **ServiceNow Script Include** | Custom transformer that detects `handoff.initiate` events and triggers agent escalation | [`ScriptIncludes/CustomDirectLineInboundTransformer.js`](./ScriptIncludes/CustomDirectLineInboundTransformer.js) | + +These samples are companion code for the official documentation at: [Microsoft Learn - Copilot Studio with ServiceNow](https://learn.microsoft.com/en-us/microsoft-copilot-studio/customer-copilot-servicenow) diff --git a/contact-center/servicenow/Salesforce/CLAUDE.md b/contact-center/servicenow/Salesforce/CLAUDE.md deleted file mode 100644 index 7be3ca96..00000000 --- a/contact-center/servicenow/Salesforce/CLAUDE.md +++ /dev/null @@ -1,162 +0,0 @@ -# Salesforce Integration - Copilot Studio - -## Project Overview - -This project contains Apex classes and deployment scripts for integrating Microsoft Copilot Studio with Salesforce Einstein Bots via the DirectLine API. - -**Related repositories:** -- **Samples**: `/Users/administrator/projects/CopilotStudioSamples/IntegrateWithEngagementHub/Salesforce/` -- **Documentation**: `/Users/administrator/projects/docs/businessapps-copilot-docs-pr/copilot-studio/customer-copilot-salesforce-handoff.md` - ---- - -## Important Notes - -### Agentforce Support Removed (February 2026) - -Agentforce integration code was removed from the repo because there is no reliable integration path at this time. The research findings below are preserved as institutional knowledge for if/when Agentforce integration becomes viable. - -#### Original Agentforce Accommodation (February 2026) - -We had added full support for Salesforce Agentforce alongside the existing Einstein Bots support, but removed it due to the lack of a reliable integration path. - -#### Key Research Findings - -| Aspect | Einstein Bots | Agentforce | -|--------|--------------|------------| -| **Permission Set** | `sfdc_chatbot_service_permset` (auto-created) | Custom permission set assigned to `EinsteinServiceAgent` user | -| **User Model** | Uses system permission set | Auto-generated user per agent (format: `EinsteinServiceAgent-[AgentId]`) | -| **Fallback Mechanism** | **Confused** topic | **Fallback** topic | -| **AI Architecture** | Rule-based (NLU) | LLM-based (generative) | -| **Prerequisites** | Service Cloud License | Einstein Copilot + Data Cloud | - -**Critical finding:** The same Apex `@InvocableMethod` classes work with both platforms. The integration code is identical—only the permission configuration and topic names differ. - -#### What We Changed - -**1. MS Learn Documentation** (`customer-copilot-salesforce-handoff.md`) - -- Updated title: "Integrate with Salesforce Einstein Bot or Agentforce" -- Added introduction explaining both platforms are supported -- Added MS Learn tabs (`# [Einstein Bots](#tab/einsteinBots)` / `# [Agentforce](#tab/agentforce)`) to: - - Prerequisites (different license requirements) - - Step 5: Grant permissions (different permission set approach) - - Step 6: Set up bot/agent (different setup locations) - - Step 7: Connect to Copilot Studio (Confused vs Fallback topic) - - Step 8: Handle agent handoff (different escalation docs) -- Added new Step 4: Configure Named Credential (was missing from original doc) -- Steps 1-4 remain unchanged (identical for both platforms) - -**2. Deployment Scripts** - -`grant-bot-permissions.sh` / `grant-bot-permissions.ps1`: -- Added `--agentforce` / `-Agentforce` flag -- Added `--agent-name` / `-AgentName` optional filter -- Agentforce mode: - - Creates `CopilotStudio_DirectLine` permission set - - Queries for `EinsteinServiceAgent` users via SOQL - - Auto-assigns permission set if single agent found - - Lists agents if multiple found (requires `--agent-name` filter) - -`deploy.sh` / `deploy.ps1`: -- Added `--agentforce` / `-Agentforce` flag -- Added `--agent-name` / `-AgentName` pass-through -- Shows mode at startup -- Links to correct MS Learn tab in output - -**3. README Updates** - -- Updated intro to mention both platforms -- Added Quick Start examples for both modes -- Updated "What Gets Deployed" to explain both modes -- Renamed "Agentforce Compatibility" to "Agentforce Support" (now fully automated) - -#### Agentforce Agent Creation Order - -**Important:** The Agentforce agent must be created in the Salesforce UI *before* the script can assign permissions to it. The `EinsteinServiceAgent` user is auto-generated when you create an Agentforce agent. - -**Recommended workflow:** -1. Run `deploy.sh --agentforce` to deploy metadata and create permission set -2. Create Agentforce agent in Salesforce Setup → Agents -3. Re-run `grant-bot-permissions.sh --agentforce` to assign permission set to agent user - -**Alternative:** Manually assign the `CopilotStudio_DirectLine` permission set to your agent's user in Setup → Users. - -#### SOQL Queries for Agentforce - -```sql --- Find EinsteinServiceAgent users -SELECT Id, Username FROM User WHERE Username LIKE 'EinsteinServiceAgent%' LIMIT 10 - --- Filter by agent name -SELECT Id, Username FROM User WHERE Username LIKE 'EinsteinServiceAgent%' AND Name LIKE '%AgentName%' LIMIT 10 - --- Check permission set assignment -SELECT Id FROM PermissionSetAssignment WHERE AssigneeId = 'userId' AND PermissionSetId = 'permsetId' -``` - -#### API Limitation: SetupEntityType - -The `SetupEntityType` field on `SetupEntityAccess` is read-only via the API. When creating access records, omit this field - Salesforce auto-determines the type from `SetupEntityId`. - -#### MS Learn Tab Syntax - -```markdown -# [Einstein Bots](#tab/einsteinBots) - -Content for Einstein Bots... - -# [Agentforce](#tab/agentforce) - -Content for Agentforce... - ---- -``` - -Tab IDs are used in URLs: `?tabs=einsteinBots` or `?tabs=agentforce` - ---- - -## File Structure - -``` -Salesforce/ -├── ApexClasses/ -│ ├── DL_GetConversation.cls # Starts DirectLine conversation -│ ├── DL_PostActivity.cls # Sends user message to Copilot Studio -│ └── DL_GetActivity.cls # Retrieves bot response (with polling) -├── Metadata/ -│ ├── classes/ # Apex class metadata XML -│ ├── remoteSiteSettings/ # DirectLine remote site -│ ├── externalCredentials/ # Directline External Credential -│ └── namedCredentials/ # Directline Named Credential -├── scripts/ -│ ├── deploy.sh # Main deployment (macOS/Linux) -│ ├── deploy.ps1 # Main deployment (Windows) -│ ├── grant-bot-permissions.sh # Permission grant (macOS/Linux) -│ └── grant-bot-permissions.ps1 # Permission grant (Windows) -├── README.md -└── CLAUDE.md # This file -``` - ---- - -## Usage Examples - -```bash -# Deploy everything (Einstein Bots) -./scripts/deploy.sh - -# Just grant permissions (skip deployment) -./scripts/grant-bot-permissions.sh -``` - ---- - -## Links - -- [MS Learn Documentation](https://learn.microsoft.com/en-us/microsoft-copilot-studio/customer-copilot-salesforce-handoff) -- [Einstein Bots Tab](https://learn.microsoft.com/en-us/microsoft-copilot-studio/customer-copilot-salesforce-handoff?tabs=einsteinBots) -- [Agentforce Tab](https://learn.microsoft.com/en-us/microsoft-copilot-studio/customer-copilot-salesforce-handoff?tabs=agentforce) -- [Salesforce Einstein Bots Docs](https://help.salesforce.com/s/articleView?language=en_US&id=sf.bots_service_enhanced.htm&type=5) -- [Salesforce Agentforce Docs](https://help.salesforce.com/s/articleView?id=sf.copilot_intro.htm&type=5) diff --git a/contact-center/servicenow/ServiceNow/ScriptIncludes/CustomDirectLineInboundTransformer.js b/contact-center/servicenow/ScriptIncludes/CustomDirectLineInboundTransformer.js similarity index 100% rename from contact-center/servicenow/ServiceNow/ScriptIncludes/CustomDirectLineInboundTransformer.js rename to contact-center/servicenow/ScriptIncludes/CustomDirectLineInboundTransformer.js diff --git a/contact-center/servicenow/ServiceNow/README.md b/contact-center/servicenow/ServiceNow/README.md deleted file mode 100644 index ea33f2e6..00000000 --- a/contact-center/servicenow/ServiceNow/README.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: ServiceNow -parent: Contact Center -nav_order: 3 ---- -# Copilot Studio - ServiceNow Integration Samples - -This folder contains sample code for integrating Microsoft Copilot Studio with ServiceNow Virtual Agent, enabling seamless handoff from virtual agent to live agent. - -## Assets Included - -| Asset | Description | File | -|-------|-------------|------| -| **Azure Function** | Relay service that bridges ServiceNow with the Direct Line API | [`DirectLineAzureFunction/relayToDirectLine`](./DirectLineAzureFunction/) | -| **ServiceNow Script Include** | Custom transformer that detects `handoff.initiate` events and triggers agent escalation | [`ScriptIncludes/CustomDirectLineInboundTransformer.js`](./ScriptIncludes/CustomDirectLineInboundTransformer.js) | - -These samples are companion code for the official documentation at: [Microsoft Learn - Copilot Studio with ServiceNow](https://learn.microsoft.com/en-us/microsoft-copilot-studio/customer-copilot-servicenow) diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/ContosoLiveChatApp.csproj b/contact-center/skill-handoff/ContosoLiveChatApp/ContosoLiveChatApp.csproj similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/ContosoLiveChatApp.csproj rename to contact-center/skill-handoff/ContosoLiveChatApp/ContosoLiveChatApp.csproj diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/Controllers/ChatController.cs b/contact-center/skill-handoff/ContosoLiveChatApp/Controllers/ChatController.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/Controllers/ChatController.cs rename to contact-center/skill-handoff/ContosoLiveChatApp/Controllers/ChatController.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/Models/ChatMessage.cs b/contact-center/skill-handoff/ContosoLiveChatApp/Models/ChatMessage.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/Models/ChatMessage.cs rename to contact-center/skill-handoff/ContosoLiveChatApp/Models/ChatMessage.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/Program.cs b/contact-center/skill-handoff/ContosoLiveChatApp/Program.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/Program.cs rename to contact-center/skill-handoff/ContosoLiveChatApp/Program.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/README.md b/contact-center/skill-handoff/ContosoLiveChatApp/README.md similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/README.md rename to contact-center/skill-handoff/ContosoLiveChatApp/README.md diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/Services/ChatStorageService.cs b/contact-center/skill-handoff/ContosoLiveChatApp/Services/ChatStorageService.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/Services/ChatStorageService.cs rename to contact-center/skill-handoff/ContosoLiveChatApp/Services/ChatStorageService.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/Services/WebhookService.cs b/contact-center/skill-handoff/ContosoLiveChatApp/Services/WebhookService.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/Services/WebhookService.cs rename to contact-center/skill-handoff/ContosoLiveChatApp/Services/WebhookService.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/appsettings.json b/contact-center/skill-handoff/ContosoLiveChatApp/appsettings.json similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/appsettings.json rename to contact-center/skill-handoff/ContosoLiveChatApp/appsettings.json diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/wwwroot/index.html b/contact-center/skill-handoff/ContosoLiveChatApp/wwwroot/index.html similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/ContosoLiveChatApp/wwwroot/index.html rename to contact-center/skill-handoff/ContosoLiveChatApp/wwwroot/index.html diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverAgentSample.zip b/contact-center/skill-handoff/HandoverAgentSample.zip similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverAgentSample.zip rename to contact-center/skill-handoff/HandoverAgentSample.zip diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/CopilotStudio/ConversationManager.cs b/contact-center/skill-handoff/HandoverToLiveAgentSample/CopilotStudio/ConversationManager.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/CopilotStudio/ConversationManager.cs rename to contact-center/skill-handoff/HandoverToLiveAgentSample/CopilotStudio/ConversationManager.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/CopilotStudio/CopilotStudioAgent.cs b/contact-center/skill-handoff/HandoverToLiveAgentSample/CopilotStudio/CopilotStudioAgent.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/CopilotStudio/CopilotStudioAgent.cs rename to contact-center/skill-handoff/HandoverToLiveAgentSample/CopilotStudio/CopilotStudioAgent.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/CopilotStudio/MsTeamsProactiveMesssage.cs b/contact-center/skill-handoff/HandoverToLiveAgentSample/CopilotStudio/MsTeamsProactiveMesssage.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/CopilotStudio/MsTeamsProactiveMesssage.cs rename to contact-center/skill-handoff/HandoverToLiveAgentSample/CopilotStudio/MsTeamsProactiveMesssage.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/HandoverToLiveAgentSample.csproj b/contact-center/skill-handoff/HandoverToLiveAgentSample/HandoverToLiveAgentSample.csproj similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/HandoverToLiveAgentSample.csproj rename to contact-center/skill-handoff/HandoverToLiveAgentSample/HandoverToLiveAgentSample.csproj diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/LiveChat/LiveChatService.cs b/contact-center/skill-handoff/HandoverToLiveAgentSample/LiveChat/LiveChatService.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/LiveChat/LiveChatService.cs rename to contact-center/skill-handoff/HandoverToLiveAgentSample/LiveChat/LiveChatService.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/LiveChat/LiveChatWebhookController.cs b/contact-center/skill-handoff/HandoverToLiveAgentSample/LiveChat/LiveChatWebhookController.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/LiveChat/LiveChatWebhookController.cs rename to contact-center/skill-handoff/HandoverToLiveAgentSample/LiveChat/LiveChatWebhookController.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/Program.cs b/contact-center/skill-handoff/HandoverToLiveAgentSample/Program.cs similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/Program.cs rename to contact-center/skill-handoff/HandoverToLiveAgentSample/Program.cs diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/README.md b/contact-center/skill-handoff/HandoverToLiveAgentSample/README.md similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/README.md rename to contact-center/skill-handoff/HandoverToLiveAgentSample/README.md diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/appsettings.json b/contact-center/skill-handoff/HandoverToLiveAgentSample/appsettings.json similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/appsettings.json rename to contact-center/skill-handoff/HandoverToLiveAgentSample/appsettings.json diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/wwwroot/skill-manifest.json b/contact-center/skill-handoff/HandoverToLiveAgentSample/wwwroot/skill-manifest.json similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/HandoverToLiveAgentSample/wwwroot/skill-manifest.json rename to contact-center/skill-handoff/HandoverToLiveAgentSample/wwwroot/skill-manifest.json diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/README.md b/contact-center/skill-handoff/README.md similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/README.md rename to contact-center/skill-handoff/README.md diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/img/app_registration_setup.png b/contact-center/skill-handoff/img/app_registration_setup.png similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/img/app_registration_setup.png rename to contact-center/skill-handoff/img/app_registration_setup.png diff --git a/contact-center/servicenow/HandoverToLiveAgentUsingSkill/img/solution_import.png b/contact-center/skill-handoff/img/solution_import.png similarity index 100% rename from contact-center/servicenow/HandoverToLiveAgentUsingSkill/img/solution_import.png rename to contact-center/skill-handoff/img/solution_import.png diff --git a/extensibility/agents-sdk/relay-bot/AdapterWithErrorHandler.cs b/extensibility/agents-sdk/relay-bot/AdapterWithErrorHandler.cs index 57a27b15..f9741868 100644 --- a/extensibility/agents-sdk/relay-bot/AdapterWithErrorHandler.cs +++ b/extensibility/agents-sdk/relay-bot/AdapterWithErrorHandler.cs @@ -2,15 +2,15 @@ // Licensed under the MIT License. using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Extensions.Configuration; +using Microsoft.Bot.Connector.Authentication; using Microsoft.Extensions.Logging; namespace Microsoft.PowerVirtualAgents.Samples.RelayBotSample { - public class AdapterWithErrorHandler : BotFrameworkHttpAdapter + public class AdapterWithErrorHandler : CloudAdapter { - public AdapterWithErrorHandler(IConfiguration configuration, ILogger logger) - : base(configuration, logger) + public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger logger) + : base(auth, logger) { OnTurnError = async (turnContext, exception) => { diff --git a/extensibility/agents-sdk/relay-bot/Program.cs b/extensibility/agents-sdk/relay-bot/Program.cs index 5ed15a40..17ddf2a8 100644 --- a/extensibility/agents-sdk/relay-bot/Program.cs +++ b/extensibility/agents-sdk/relay-bot/Program.cs @@ -1,20 +1,62 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.PowerVirtualAgents.Samples.RelayBotSample; +using Microsoft.PowerVirtualAgents.Samples.RelayBotSample.Bots; -namespace Microsoft.PowerVirtualAgents.Samples.RelayBotSample -{ - public class Program +var builder = WebApplication.CreateBuilder(args); + +//---- Configure services ---- +builder.Services.AddHttpClient(); +builder.Services.AddControllers() + .AddNewtonsoftJson(options => { - public static void Main(string[] args) - { - CreateWebHostBuilder(args).Build().Run(); - } - - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup(); - } + options.SerializerSettings.MaxDepth = HttpHelper.BotMessageSerializerSettings.MaxDepth; + }); + + +builder.Services.AddSingleton(); + +// Create the Bot Adapter with error handling enabled. +builder.Services.AddSingleton(); + +// Create the bot as a transient. In this case the ASP Controller is expecting an IBot. +builder.Services.AddSingleton(); + +// Create the singleton instance of BotService from appsettings +var botService = new BotService(); +builder.Configuration.Bind("BotService", (object)botService); +builder.Services.AddSingleton(botService); + +// Create the singleton instance of ConversationPool from appsettings +var conversationManager = new ConversationManager(); +builder.Configuration.Bind("ConversationPool", conversationManager); +builder.Services.AddSingleton(conversationManager); + +var app = builder.Build(); + +//--- - Configure the HTTP request pipeline ---- +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} +else +{ + app.UseHsts(); } + +app.UseDefaultFiles() + .UseStaticFiles() + .UseWebSockets() + .UseRouting(); + +app.MapControllers(); + +app.Run(); diff --git a/extensibility/agents-sdk/relay-bot/README.md b/extensibility/agents-sdk/relay-bot/README.md index 69641a2c..a1092559 100644 --- a/extensibility/agents-sdk/relay-bot/README.md +++ b/extensibility/agents-sdk/relay-bot/README.md @@ -10,15 +10,15 @@ Deprecated {: .label .label-red } {: .caution } -> This sample is deprecated and will be replaced with a modernized M365 Agents SDK sample. +> This sample uses the archived Bot Framework SDK (`Microsoft.Bot.Builder`). It will be replaced with an M365 Agents SDK implementation. -Sample of connecting Bot Framework v4 bot to a Power Virtual Agent bot. +Sample of connecting an Azure Bot Service bot to a Copilot Studio agent using the Direct Line API. -This bot has been created based on [Bot Framework](https://dev.botframework.com), it shows how to create an Azure Bot Service bot that connects to Power Virtual Agents bot +This bot has been created based on [Bot Framework](https://dev.botframework.com), it shows how to create an Azure Bot Service bot that connects to a Copilot Studio agent ## Prerequisites -- [.NET Core SDK](https://dotnet.microsoft.com/download) version 2.1 +- [.NET SDK](https://dotnet.microsoft.com/download) version 10.0 ```bash # determine dotnet version @@ -30,10 +30,10 @@ This bot has been created based on [Bot Framework](https://dev.botframework.com) - Clone the repository ```bash - git clone https://github.com/microsoft/PowerVirtualAgentsSample.git + git clone https://github.com/microsoft/CopilotStudioSamples.git ``` -- In a terminal, navigate to `BYOBSample/` +- In a terminal, navigate to `extensibility/agents-sdk/relay-bot` - Update file appsettings.json with your Power Virtual Agent bot id, tenant id, bot name and other settings. To retrieve your bot's bot ID and tenant ID, click on left side pane's ***Manage***, click ***Channels*** and click on the Azure Bot Service channel that you need to connect to. @@ -54,7 +54,7 @@ This bot has been created based on [Bot Framework](https://dev.botframework.com) - Launch Visual Studio - File -> Open -> Project/Solution - - Navigate to `BYOBSample/` folder + - Navigate to `extensibility/agents-sdk/relay-bot` folder - Select `SampleBot.csproj` file - Press `F5` to run the project diff --git a/extensibility/agents-sdk/relay-bot/SampleBot.csproj b/extensibility/agents-sdk/relay-bot/SampleBot.csproj index cefada66..41a17caf 100644 --- a/extensibility/agents-sdk/relay-bot/SampleBot.csproj +++ b/extensibility/agents-sdk/relay-bot/SampleBot.csproj @@ -1,14 +1,14 @@ - netcoreapp2.1 + net10.0 latest - - - + + + diff --git a/extensibility/agents-sdk/relay-bot/Startup.cs b/extensibility/agents-sdk/relay-bot/Startup.cs deleted file mode 100644 index c23b24c0..00000000 --- a/extensibility/agents-sdk/relay-bot/Startup.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Bot.Builder; -using Microsoft.Bot.Builder.Integration.AspNet.Core; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.PowerVirtualAgents.Samples.RelayBotSample.Bots; - -namespace Microsoft.PowerVirtualAgents.Samples.RelayBotSample -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); - - // Create the Bot Framework Adapter with error handling enabled. - services.AddSingleton(); - - // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. - services.AddSingleton(); - - // Create the singleton instance of BotService from appsettings - var botService = new BotService(); - Configuration.Bind("BotService", (object)botService); - services.AddSingleton(botService); - - // Create the singleton instance of ConversationPool from appsettings - var conversationManager = new ConversationManager(); - Configuration.Bind("ConversationPool", conversationManager); - services.AddSingleton(conversationManager); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseHsts(); - } - - app.UseDefaultFiles(); - app.UseStaticFiles(); - - app.UseMvc(); - } - } -} diff --git a/extensibility/agents-sdk/relay-bot/appsettings.json b/extensibility/agents-sdk/relay-bot/appsettings.json index 5d1da922..d366a013 100644 --- a/extensibility/agents-sdk/relay-bot/appsettings.json +++ b/extensibility/agents-sdk/relay-bot/appsettings.json @@ -1,6 +1,11 @@ { + // Configuration for Azure bot service + "MicrosoftAppType": "", "MicrosoftAppId": "", "MicrosoftAppPassword": "", + "MicrosoftAppTenantId": "", + + // Configuration for MCS Bot "BotService": { "BotName": "", "BotId": "", diff --git a/extensibility/mcp/README.md b/extensibility/mcp/README.md index 040a6745..f4a72961 100644 --- a/extensibility/mcp/README.md +++ b/extensibility/mcp/README.md @@ -15,3 +15,4 @@ MCP servers that provide tools and resources to Copilot Studio agents. |--------|-------------| | [pass-resources-as-inputs/](./pass-resources-as-inputs/) | Pass MCP resources as agent inputs | | [search-species-resources-typescript/](./search-species-resources-typescript/) | Species search MCP server in TypeScript | +| [dynamic-mcp-routing-typescript/](./dynamic-mcp-routing-typescript/) | Dynamic routing to multiple MCP server instances via a Power Platform connector | diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/.gitignore b/extensibility/mcp/dynamic-mcp-routing-typescript/.gitignore new file mode 100644 index 00000000..34f3a7ab --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +build/ +package-lock.json +connector/settings.json diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/README.md b/extensibility/mcp/dynamic-mcp-routing-typescript/README.md new file mode 100644 index 00000000..b089599f --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/README.md @@ -0,0 +1,209 @@ +--- +title: Dynamic MCP Routing +parent: MCP +grand_parent: Extensibility +nav_order: 3 +--- + +# Dynamic MCP Routing + +A Power Platform connector that routes MCP Streamable HTTP traffic to one of several MCP server instances, selected at configuration time via a dropdown. Demonstrates how to use a catalog service, `x-ms-dynamic-values`, `x-ms-agentic-protocol`, and a `script.csx` URL rewriter to give a single connector access to multiple independent MCP servers. + +## Architecture + +```mermaid +flowchart TD + CS["Copilot Studio
(MCP client)"] + CONN["Power Platform Connector
x-ms-agentic-protocol: mcp-streamable-1.0"] + SCRIPT["script.csx
Rewrites URL based on
selected instance"] + CAT["Catalog Server
GET /instances"] + MCP["MCP Server
(single process, path-based routing)"] + C["/instances/contoso/mcp"] + F["/instances/fabrikam/mcp"] + N["/instances/northwind/mcp"] + + CS -->|"MCP protocol"| CONN + CONN -->|"ListInstances"| CAT + CONN -->|"InvokeMCP"| SCRIPT + SCRIPT -->|"rewritten URL"| MCP + MCP --> C + MCP --> F + MCP --> N +``` + +**Three components:** + +1. **Catalog server** (`src/catalog/`) — REST endpoint returning a list of MCP server instances with their endpoint URLs. The connector's `ListInstances` operation hits this to populate the instance dropdown. + +2. **MCP server** (`src/mcp-server/`) — Single Express server hosting multiple independent MCP servers at `/instances/:id/mcp`. Each instance advertises its own tools (`list_projects`, `get_project_details`) with instance-specific data. Stateless: a fresh `Server` + `StreamableHTTPServerTransport` is created per request. + +3. **Power Platform connector** (`connector/`) — Swagger definition with two operations: + - `ListInstances` (internal, for dropdown) — calls the catalog + - `InvokeMCP` — annotated with `x-ms-agentic-protocol: mcp-streamable-1.0`, with an `instanceUrl` parameter populated via `x-ms-dynamic-values` + - `script.csx` rewrites the `InvokeMCP` request URL from the catalog host to the selected instance's MCP endpoint + +## How It Works + +### 1. Instance Discovery + +The connector's `InvokeMCP` parameter uses `x-ms-dynamic-values` to call `ListInstances`, which returns instances with their `mcpUrl`: + +```json +[ + { "id": "contoso", "name": "Contoso", "mcpUrl": "https://host/instances/contoso/mcp" }, + { "id": "fabrikam", "name": "Fabrikam", "mcpUrl": "https://host/instances/fabrikam/mcp" } +] +``` + +The agent builder picks an instance from the dropdown when adding the connector action. + +### 2. URL Rewriting + +The swagger `host` points at the catalog server. When Copilot Studio calls `InvokeMCP`, `script.csx` intercepts the request, reads the `instanceUrl` query parameter, and rewrites the full URL (scheme, host, port, path) to the selected instance's MCP endpoint: + +```csharp +var targetUri = new Uri(instanceUrl); +var builder = new UriBuilder(Context.Request.RequestUri) +{ + Scheme = targetUri.Scheme, + Host = targetUri.Host, + Port = targetUri.Port, + Path = targetUri.AbsolutePath +}; +``` + +### 3. MCP Protocol + +Each instance endpoint is a fully independent MCP server. Copilot Studio handles the MCP protocol (`initialize`, `tools/list`, `tools/call`) natively. The mock data includes three fictional organizations with project portfolio data. + +## Sample Structure + +``` +dynamic-mcp-routing-typescript/ +├── src/ +│ ├── catalog/ +│ │ └── index.ts # Catalog REST server +│ └── mcp-server/ +│ ├── index.ts # Multi-instance MCP server +│ └── data.ts # Mock instances, projects, details +├── connector/ +│ ├── apiDefinition.swagger.json # Swagger with x-ms-agentic-protocol +│ ├── apiProperties.json # No connection parameters +│ └── script.csx # URL rewriter for dynamic routing +├── scripts/ +│ ├── deploy.sh # One-step deploy for macOS / Linux +│ └── deploy.ps1 # One-step deploy for Windows +├── package.json +├── tsconfig.json +└── README.md +``` + +## Quick Start + +### Prerequisites + +- [Node.js 18+](https://nodejs.org/) +- [Dev Tunnels CLI](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started) +- [paconn CLI](https://learn.microsoft.com/connectors/custom-connectors/paconn-cli) (`pip install paconn`) +- A [Power Platform environment](https://admin.powerplatform.microsoft.com/) with Copilot Studio access + +### 1. Install and Build + +```bash +npm install +npm run build +``` + +### 2. Start Servers + +In two terminals: + +```bash +# Terminal 1 — Catalog server (port 3000) +npm run start:catalog + +# Terminal 2 — MCP server (port 3001) +npm run start:mcp +``` + +### 3. Create Dev Tunnels + +**Using the CLI:** + +```bash +devtunnel host -p 3000 -p 3001 --allow-anonymous +``` + +This outputs two URLs like: + +``` +https://abc123-3000.euw.devtunnels.ms (catalog) +https://abc123-3001.euw.devtunnels.ms (MCP) +``` + +Restart the catalog server with the MCP tunnel URL: + +```bash +MCP_SERVER_BASE=https://abc123-3001.euw.devtunnels.ms npm run start:catalog +``` + +### 4. Deploy the Connector + +Update `connector/apiDefinition.swagger.json` — set `host` to your catalog tunnel hostname (e.g. `abc123-3000.euw.devtunnels.ms`), then: + +```bash +python3 -m paconn login +python3 -m paconn create \ + -e YOUR_ENVIRONMENT_ID \ + -d connector/apiDefinition.swagger.json \ + -p connector/apiProperties.json \ + -x connector/script.csx +``` + +Or use the one-step deploy script that handles login, servers, tunnel, and connector deployment: + +**Bash (macOS / Linux):** + +```bash +./scripts/deploy.sh YOUR_ENVIRONMENT_ID [TENANT_ID] +``` + +**PowerShell (Windows):** + +```powershell +.\scripts\deploy.ps1 -EnvironmentId YOUR_ENVIRONMENT_ID [-TenantId TENANT_ID] +``` + +### 5. Configure Copilot Studio + +1. Open your agent in [Copilot Studio](https://copilotstudio.microsoft.com/) +2. Go to **Tools** > **Add tool** > filter by **Model Context Protocol** +3. Search for "Dynamic MCP Connector" and add it +4. Under **Inputs**, select an instance from the **Instance** dropdown (e.g. "Contoso", "Fabrikam", "Northwind") +5. The **Tools** section will populate with the MCP tools for the selected instance — tools won't appear until you pick an instance +6. Click **Save** + +## Example Queries + +Once the connector is added to an agent: + +- "What projects are available?" — calls `list_projects` +- "Show me the details for the ERP rollout" — calls `get_project_details` with `projectId: "erp-rollout"` +- "What are the risks on the supply chain project?" — calls `get_project_details` for Fabrikam + +## Development + +**Add a new instance:** Add an entry to the `instances` array in `src/mcp-server/data.ts` and `src/catalog/index.ts`, along with its projects and details. + +**Add a new tool:** Register additional tools in the `createServer` function in `src/mcp-server/index.ts` using `ListToolsRequestSchema` and `CallToolRequestSchema` handlers. + +**Remove dynamic routing:** If you only need a single MCP server, simplify by removing the catalog server and `script.csx`, and point the swagger `host` directly at the MCP server. + +## Resources + +- [Model Context Protocol](https://modelcontextprotocol.io/) +- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) +- [MCP in Copilot Studio](https://learn.microsoft.com/microsoft-copilot-studio/mcp-overview) +- [Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/) +- [Custom Connector CLI (paconn)](https://learn.microsoft.com/connectors/custom-connectors/paconn-cli) +- [`x-ms-agentic-protocol`](https://learn.microsoft.com/connectors/custom-connectors/mcp-overview) diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/connector/apiDefinition.swagger.json b/extensibility/mcp/dynamic-mcp-routing-typescript/connector/apiDefinition.swagger.json new file mode 100644 index 00000000..ca563fc0 --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/connector/apiDefinition.swagger.json @@ -0,0 +1,90 @@ +{ + "swagger": "2.0", + "info": { + "title": "Dynamic MCP Connector", + "description": "Select an MCP server instance from the catalog, then interact via MCP Streamable HTTP.", + "version": "1.0.0" + }, + "host": "CATALOG_HOST_PLACEHOLDER", + "basePath": "/", + "schemes": ["https"], + "consumes": ["application/json"], + "produces": ["application/json"], + "paths": { + "/instances": { + "get": { + "operationId": "ListInstances", + "summary": "List Instances", + "description": "Returns the MCP server instances available in the catalog.", + "x-ms-visibility": "internal", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Instance" + } + } + } + } + } + }, + "/mcp": { + "post": { + "operationId": "InvokeMCP", + "summary": "Invoke MCP Server", + "description": "Interact with the selected MCP server instance via Streamable HTTP.", + "x-ms-agentic-protocol": "mcp-streamable-1.0", + "parameters": [ + { + "name": "instanceUrl", + "in": "query", + "required": true, + "type": "string", + "x-ms-summary": "Instance", + "description": "Select an MCP server instance.", + "x-ms-dynamic-values": { + "operationId": "ListInstances", + "value-path": "mcpUrl", + "value-title": "name" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "definitions": { + "Instance": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Instance identifier", + "x-ms-summary": "Instance ID" + }, + "name": { + "type": "string", + "description": "Display name of the instance", + "x-ms-summary": "Name" + }, + "description": { + "type": "string", + "description": "Description of the instance", + "x-ms-summary": "Description" + }, + "mcpUrl": { + "type": "string", + "description": "MCP endpoint URL for this instance", + "x-ms-summary": "MCP URL" + } + } + } + } +} diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/connector/apiProperties.json b/extensibility/mcp/dynamic-mcp-routing-typescript/connector/apiProperties.json new file mode 100644 index 00000000..09c18914 --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/connector/apiProperties.json @@ -0,0 +1,5 @@ +{ + "properties": { + "connectionParameters": {} + } +} diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/connector/script.csx b/extensibility/mcp/dynamic-mcp-routing-typescript/connector/script.csx new file mode 100644 index 00000000..51c75f1d --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/connector/script.csx @@ -0,0 +1,39 @@ +using System.Net; +using System.Web; + +public class Script : ScriptBase +{ + public override async Task ExecuteAsync() + { + // Only rewrite for InvokeMCP — ListInstances goes to the catalog host unchanged. + if (Context.OperationId == "InvokeMCP") + { + // instanceUrl contains the full MCP endpoint URL for the selected instance, + // populated from the ListInstances dropdown (value-path: "mcpUrl"). + var query = HttpUtility.ParseQueryString(Context.Request.RequestUri.Query); + var instanceUrl = query["instanceUrl"]; + + if (!string.IsNullOrEmpty(instanceUrl)) + { + var targetUri = new Uri(instanceUrl); + + // Rewrite the entire request URL to the instance's MCP endpoint + var builder = new UriBuilder(Context.Request.RequestUri) + { + Scheme = targetUri.Scheme, + Host = targetUri.Host, + Port = targetUri.Port, + Path = targetUri.AbsolutePath + }; + + // Remove instanceUrl from query string — the MCP server doesn't need it + query.Remove("instanceUrl"); + builder.Query = query.ToString(); + + Context.Request.RequestUri = builder.Uri; + } + } + + return await this.Context.SendAsync(this.Context.Request, this.CancellationToken); + } +} diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/package.json b/extensibility/mcp/dynamic-mcp-routing-typescript/package.json new file mode 100644 index 00000000..7a8131f8 --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/package.json @@ -0,0 +1,28 @@ +{ + "name": "dynamic-mcp-routing", + "version": "1.0.0", + "description": "Dynamic MCP routing — a catalog server and multi-instance MCP server with a Power Platform connector that routes to the selected instance", + "type": "module", + "main": "build/mcp-server/index.js", + "scripts": { + "build": "tsc", + "start:catalog": "node build/catalog/index.js", + "start:mcp": "node build/mcp-server/index.js", + "start": "npm run start:mcp", + "dev:catalog": "npx tsx src/catalog/index.ts", + "dev:mcp": "npx tsx src/mcp-server/index.ts", + "deploy": "bash scripts/deploy.sh" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.20.2", + "express": "^4.18.2", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.24.6" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.0.0", + "tsx": "^4.19.0", + "typescript": "^5.3.0" + } +} diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.ps1 b/extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.ps1 new file mode 100644 index 00000000..a6abbc3d --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.ps1 @@ -0,0 +1,217 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Start servers, create a dev tunnel, and deploy/update the Power Platform connector. + +.PARAMETER EnvironmentId + Power Platform environment ID (required). + +.PARAMETER TenantId + Azure AD tenant ID (optional). Forces re-login if current token is for a different tenant. + +.EXAMPLE + .\scripts\deploy.ps1 -EnvironmentId "6cc0c98e-3fe6-e0d5-8eba-ba51c9da1d13" + .\scripts\deploy.ps1 -EnvironmentId "6cc0c98e-..." -TenantId "8a235459-..." +#> +param( + [Parameter(Mandatory=$true)] + [string]$EnvironmentId, + + [Parameter(Mandatory=$false)] + [string]$TenantId = "" +) + +$ErrorActionPreference = "Stop" +$ProjectDir = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$CatalogPort = 3000 +$McpPort = 3001 +$SettingsFile = Join-Path $ProjectDir "connector\settings.json" +$SwaggerFile = Join-Path $ProjectDir "connector\apiDefinition.swagger.json" +$PropsFile = Join-Path $ProjectDir "connector\apiProperties.json" +$ScriptFile = Join-Path $ProjectDir "connector\script.csx" +$PaconnTokenFile = Join-Path $env:USERPROFILE ".paconn\accessTokens.json" + +$Processes = @() + +function Cleanup { + Write-Host "`nShutting down..." + foreach ($p in $script:Processes) { + if ($p -and !$p.HasExited) { + Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue + } + } + Write-Host "Done." +} + +trap { Cleanup; break } + +# --- 0. Ensure paconn is logged in --- +function Ensure-Login { + Write-Host "==> Checking paconn login..." + $needLogin = $false + + if (-not (Test-Path $PaconnTokenFile)) { + Write-Host " No token file found." + $needLogin = $true + } else { + $token = Get-Content $PaconnTokenFile | ConvertFrom-Json + + # Check expiry + $expiresOn = [double]$token.expires_on + $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + if ($expiresOn -lt $now) { + Write-Host " Token expired." + $needLogin = $true + } + + # Check tenant + if ($TenantId -and -not $needLogin) { + if ($token.tenant_id -ne $TenantId) { + Write-Host " Logged into tenant $($token.tenant_id), need $TenantId." + $needLogin = $true + } + } + } + + if ($needLogin) { + Write-Host " Logging in..." + if ($TenantId) { + python -m paconn login -t $TenantId + } else { + python -m paconn login + } + Write-Host " Login complete." + } else { + Write-Host " Logged in as $($token.user_id)" + } +} + +Ensure-Login + +# --- 1. Build & start servers --- +Write-Host "==> Building..." +Set-Location $ProjectDir +npm run build + +Write-Host "==> Starting catalog server (port $CatalogPort)..." +$env:PORT = $CatalogPort +$catalogProc = Start-Process -FilePath "node" -ArgumentList "build/catalog/index.js" ` + -NoNewWindow -PassThru -RedirectStandardOutput "$env:TEMP\catalog-out.log" +$Processes += $catalogProc + +Write-Host "==> Starting MCP server (port $McpPort)..." +$env:PORT = $McpPort +$mcpProc = Start-Process -FilePath "node" -ArgumentList "build/mcp-server/index.js" ` + -NoNewWindow -PassThru -RedirectStandardOutput "$env:TEMP\mcp-out.log" +$Processes += $mcpProc +Remove-Item Env:\PORT + +Write-Host " Waiting for servers..." +for ($i = 0; $i -lt 15; $i++) { + try { + $null = Invoke-RestMethod -Uri "http://localhost:$CatalogPort/instances" -TimeoutSec 2 + Write-Host " Both servers ready." + break + } catch { + Start-Sleep -Seconds 1 + } +} + +# --- 2. Start devtunnel --- +Write-Host "==> Starting devtunnel for ports $CatalogPort and $McpPort..." +$tunnelLog = "$env:TEMP\devtunnel-output.log" +$tunnelProc = Start-Process -FilePath "devtunnel" ` + -ArgumentList "host -p $CatalogPort -p $McpPort --allow-anonymous" ` + -NoNewWindow -PassThru -RedirectStandardOutput $tunnelLog +$Processes += $tunnelProc + +Write-Host " Waiting for tunnel..." +$catalogHost = "" +$mcpHost = "" +for ($i = 0; $i -lt 30; $i++) { + if (Test-Path $tunnelLog) { + $log = Get-Content $tunnelLog -Raw + if ($log -match "Ready to accept") { + if ($log -match "([a-z0-9]+-$CatalogPort\.[a-z]+\.devtunnels\.ms)") { + $catalogHost = $Matches[1] + } + if ($log -match "([a-z0-9]+-$McpPort\.[a-z]+\.devtunnels\.ms)") { + $mcpHost = $Matches[1] + } + break + } + } + Start-Sleep -Seconds 1 +} + +if (-not $catalogHost -or -not $mcpHost) { + Write-Error "ERROR: Could not extract tunnel URLs. Check $tunnelLog" + Cleanup + exit 1 +} + +Write-Host " Tunnel ready!" +Write-Host " Catalog: https://$catalogHost" +Write-Host " MCP: https://$mcpHost" + +# --- 3. Restart catalog with tunnel URL --- +Stop-Process -Id $catalogProc.Id -Force -ErrorAction SilentlyContinue +Start-Sleep -Seconds 1 +Write-Host "==> Restarting catalog with MCP_SERVER_BASE=https://$mcpHost..." +$env:MCP_SERVER_BASE = "https://$mcpHost" +$env:PORT = $CatalogPort +$catalogProc = Start-Process -FilePath "node" -ArgumentList "build/catalog/index.js" ` + -NoNewWindow -PassThru -RedirectStandardOutput "$env:TEMP\catalog-out2.log" +$Processes += $catalogProc +Remove-Item Env:\PORT +Remove-Item Env:\MCP_SERVER_BASE +Start-Sleep -Seconds 3 + +Write-Host " Verifying catalog..." +$instances = Invoke-RestMethod -Uri "https://$catalogHost/instances" +Write-Host " Got $($instances.Count) instances" + +# --- 4. Update swagger host --- +Write-Host "==> Updating swagger host to $catalogHost..." +$swagger = Get-Content $SwaggerFile | ConvertFrom-Json +$swagger.host = $catalogHost +$swagger | ConvertTo-Json -Depth 20 | Set-Content $SwaggerFile -Encoding UTF8 +Write-Host " Updated." + +# --- 5. Deploy or update connector --- +if (Test-Path $SettingsFile) { + $settings = Get-Content $SettingsFile | ConvertFrom-Json + $connectorId = $settings.connectorId + Write-Host "==> Updating existing connector: $connectorId" + python -m paconn update ` + -e $EnvironmentId ` + -c $connectorId ` + -d $SwaggerFile ` + -p $PropsFile ` + -x $ScriptFile +} else { + Write-Host "==> Creating new connector..." + python -m paconn create ` + -e $EnvironmentId ` + -d $SwaggerFile ` + -p $PropsFile ` + -x $ScriptFile ` + -w + Write-Host " Connector created." +} + +Write-Host "" +Write-Host "=========================================" +Write-Host " Deployment complete!" +Write-Host " Catalog: https://$catalogHost" +Write-Host " MCP: https://$mcpHost" +Write-Host " Environment: $EnvironmentId" +Write-Host "=========================================" +Write-Host "" +Write-Host "Press Ctrl+C to stop servers and tunnel." + +try { + while ($true) { Start-Sleep -Seconds 60 } +} finally { + Cleanup +} diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.sh b/extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.sh new file mode 100755 index 00000000..516f2b61 --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/scripts/deploy.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +set -uo pipefail + +# --- Configuration --- +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CATALOG_PORT=3000 +MCP_PORT=3001 +ENV_ID="${1:?Usage: deploy.sh [tenant-id]}" +TENANT_ID="${2:-}" +SETTINGS_FILE="$PROJECT_DIR/connector/settings.json" +SWAGGER_FILE="$PROJECT_DIR/connector/apiDefinition.swagger.json" +PROPS_FILE="$PROJECT_DIR/connector/apiProperties.json" +SCRIPT_FILE="$PROJECT_DIR/connector/script.csx" +PACONN_TOKEN_FILE="$HOME/.paconn/accessTokens.json" + +# --- Output helpers --- +BOLD="\033[1m" +DIM="\033[2m" +GREEN="\033[32m" +YELLOW="\033[33m" +RED="\033[31m" +CYAN="\033[36m" +RESET="\033[0m" + +step() { echo -e "\n${BOLD}${CYAN}[$1]${RESET} ${BOLD}$2${RESET}"; } +info() { echo -e " ${DIM}$1${RESET}"; } +ok() { echo -e " ${GREEN}✓${RESET} $1"; } +warn() { echo -e " ${YELLOW}⚠${RESET} $1"; } +fail() { echo -e " ${RED}✗${RESET} $1"; } + +# --- Cleanup --- +cleanup() { + echo "" + step "•" "Shutting down..." + [ -n "${CATALOG_PID:-}" ] && kill "$CATALOG_PID" 2>/dev/null + [ -n "${MCP_PID:-}" ] && kill "$MCP_PID" 2>/dev/null + [ -n "${TUNNEL_PID:-}" ] && kill "$TUNNEL_PID" 2>/dev/null + wait 2>/dev/null + ok "All processes stopped." +} +trap cleanup EXIT + +# ============================================================ +# Step 1: Authentication +# ============================================================ +step "1/5" "Authenticating with Power Platform" + +need_login=false +if [ ! -f "$PACONN_TOKEN_FILE" ]; then + info "No token file found." + need_login=true +else + expires_on=$(python3 -c "import json; print(json.load(open('$PACONN_TOKEN_FILE')).get('expires_on','0'))" 2>/dev/null || echo "0") + now=$(python3 -c "import time; print(time.time())") + if python3 -c "exit(0 if float('$expires_on') < float('$now') else 1)" 2>/dev/null; then + info "Token expired." + need_login=true + fi + + if [ -n "$TENANT_ID" ] && [ "$need_login" = false ]; then + current_tenant=$(python3 -c "import json; print(json.load(open('$PACONN_TOKEN_FILE')).get('tenant_id',''))" 2>/dev/null || echo "") + if [ "$current_tenant" != "$TENANT_ID" ]; then + info "Logged into tenant $current_tenant, need $TENANT_ID." + need_login=true + fi + fi +fi + +if [ "$need_login" = true ]; then + warn "Login required — follow the device code prompt below:" + if [ -n "$TENANT_ID" ]; then + python3 -m paconn login -t "$TENANT_ID" + else + python3 -m paconn login + fi + ok "Login complete." +else + user_id=$(python3 -c "import json; print(json.load(open('$PACONN_TOKEN_FILE')).get('user_id','unknown'))" 2>/dev/null) + ok "Logged in as $user_id" +fi + +# ============================================================ +# Step 2: Start servers +# ============================================================ +step "2/5" "Starting servers" + +info "Building..." +cd "$PROJECT_DIR" +npm run build > /dev/null 2>&1 + +info "Catalog server on port $CATALOG_PORT..." +PORT=$CATALOG_PORT node build/catalog/index.js > /tmp/catalog-server.log 2>&1 & +CATALOG_PID=$! + +info "MCP server on port $MCP_PORT..." +PORT=$MCP_PORT node build/mcp-server/index.js > /tmp/mcp-server.log 2>&1 & +MCP_PID=$! + +info "Waiting for servers to respond..." +SERVERS_READY=false +for i in $(seq 1 20); do + catalog_ok=false + mcp_ok=false + curl -sf http://localhost:$CATALOG_PORT/instances > /dev/null 2>&1 && catalog_ok=true + curl -sf -o /dev/null -w '' http://localhost:$MCP_PORT/instances/contoso/mcp 2>/dev/null && mcp_ok=true + # Also accept connection refused → not ready yet; 404/405 → server is up + curl -s -o /dev/null -w "%{http_code}" http://localhost:$MCP_PORT/ 2>/dev/null | grep -qE "^[2-5]" && mcp_ok=true + if [ "$catalog_ok" = true ] && [ "$mcp_ok" = true ]; then + SERVERS_READY=true + break + fi + sleep 1 +done +if [ "$SERVERS_READY" = true ]; then + ok "Catalog server ready on :$CATALOG_PORT" + ok "MCP server ready on :$MCP_PORT" +else + fail "Servers did not start in time." + info "Catalog log: /tmp/catalog-server.log" + info "MCP log: /tmp/mcp-server.log" + exit 1 +fi + +# ============================================================ +# Step 3: Dev tunnel +# ============================================================ +step "3/5" "Creating dev tunnel" + +info "Exposing ports $CATALOG_PORT and $MCP_PORT..." +devtunnel host -p "$CATALOG_PORT" -p "$MCP_PORT" --allow-anonymous > /tmp/devtunnel-output.log 2>&1 & +TUNNEL_PID=$! + +CATALOG_HOST="" +MCP_HOST="" +for i in $(seq 1 30); do + if grep -q "Ready to accept" /tmp/devtunnel-output.log 2>/dev/null; then + CATALOG_HOST=$(grep -oE "[a-z0-9]+-${CATALOG_PORT}\.[a-z]+\.devtunnels\.ms" /tmp/devtunnel-output.log | head -1) + MCP_HOST=$(grep -oE "[a-z0-9]+-${MCP_PORT}\.[a-z]+\.devtunnels\.ms" /tmp/devtunnel-output.log | head -1) + break + fi + if [ "$i" -eq 30 ]; then + fail "Tunnel did not start in time." + info "Log: /tmp/devtunnel-output.log" + exit 1 + fi + sleep 1 +done + +ok "Catalog: https://$CATALOG_HOST" +ok "MCP: https://$MCP_HOST" + +# Restart catalog with public MCP URL +info "Restarting catalog with public MCP URLs..." +kill "$CATALOG_PID" 2>/dev/null +wait "$CATALOG_PID" 2>/dev/null || true +cd "$PROJECT_DIR" +MCP_SERVER_BASE="https://$MCP_HOST" PORT=$CATALOG_PORT node build/catalog/index.js > /tmp/catalog-server.log 2>&1 & +CATALOG_PID=$! +sleep 3 + +# Verify +INSTANCE_COUNT=$(curl -s "https://$CATALOG_HOST/instances" 2>/dev/null | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "0") +ok "Catalog verified — $INSTANCE_COUNT instances with public MCP URLs" + +# ============================================================ +# Step 4: Update swagger +# ============================================================ +step "4/5" "Updating connector definition" + +python3 -c " +import json +with open('$SWAGGER_FILE', 'r') as f: + swagger = json.load(f) +swagger['host'] = '$CATALOG_HOST' +with open('$SWAGGER_FILE', 'w') as f: + json.dump(swagger, f, indent=2) +" +ok "Swagger host → $CATALOG_HOST" + +# ============================================================ +# Step 5: Deploy connector +# ============================================================ +step "5/5" "Deploying connector to environment $ENV_ID" + +if [ -f "$SETTINGS_FILE" ]; then + CONNECTOR_ID=$(python3 -c "import json; print(json.load(open('$SETTINGS_FILE'))['connectorId'])") + info "Found existing connector: $CONNECTOR_ID" + info "Updating..." + python3 -m paconn update \ + -e "$ENV_ID" \ + -c "$CONNECTOR_ID" \ + -d "$SWAGGER_FILE" \ + -p "$PROPS_FILE" \ + -x "$SCRIPT_FILE" + ok "Connector updated." +else + info "No settings.json found — creating new connector..." + + CREATE_OUTPUT=$(python3 -m paconn create \ + -e "$ENV_ID" \ + -d "$SWAGGER_FILE" \ + -p "$PROPS_FILE" \ + -x "$SCRIPT_FILE" \ + -w 2>&1) || true + + if echo "$CREATE_OUTPUT" | grep -qi "DisplayNameIsInUse\|already exists"; then + SUFFIX=$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 4) + NEW_TITLE="Dynamic MCP Connector $SUFFIX" + warn "Name already taken. Retrying as '$NEW_TITLE'..." + python3 -c " +import json +with open('$SWAGGER_FILE', 'r') as f: + swagger = json.load(f) +swagger['info']['title'] = '$NEW_TITLE' +with open('$SWAGGER_FILE', 'w') as f: + json.dump(swagger, f, indent=2) +" + python3 -m paconn create \ + -e "$ENV_ID" \ + -d "$SWAGGER_FILE" \ + -p "$PROPS_FILE" \ + -x "$SCRIPT_FILE" \ + -w + ok "Connector '$NEW_TITLE' created." + elif echo "$CREATE_OUTPUT" | grep -qi "created successfully"; then + ok "Connector created." + else + echo "$CREATE_OUTPUT" + fail "Connector creation failed. See output above." + exit 1 + fi +fi + +# ============================================================ +# Done +# ============================================================ +echo "" +echo -e "${BOLD}${GREEN}=========================================${RESET}" +echo -e "${BOLD} Deployment complete!${RESET}" +echo -e "${GREEN}=========================================${RESET}" +echo -e " Catalog: ${CYAN}https://$CATALOG_HOST${RESET}" +echo -e " MCP Server: ${CYAN}https://$MCP_HOST${RESET}" +echo -e " Environment: ${DIM}$ENV_ID${RESET}" +echo -e "${GREEN}=========================================${RESET}" +echo "" +echo -e "${DIM}Press Ctrl+C to stop servers and tunnel.${RESET}" +wait diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/src/catalog/index.ts b/extensibility/mcp/dynamic-mcp-routing-typescript/src/catalog/index.ts new file mode 100644 index 00000000..380ae1a8 --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/src/catalog/index.ts @@ -0,0 +1,42 @@ +import express, { Request, Response } from "express"; + +const PORT = parseInt(process.env.PORT || "3000"); +const MCP_SERVER_BASE = process.env.MCP_SERVER_BASE || "http://localhost:3001"; + +const instances = [ + { + id: "contoso", + name: "Contoso", + description: "Contoso Ltd — Global ERP transformation programme", + }, + { + id: "fabrikam", + name: "Fabrikam", + description: "Fabrikam Inc — Supply chain modernisation", + }, + { + id: "northwind", + name: "Northwind", + description: "Northwind Traders — Finance & HR digital transformation", + }, +]; + +const app = express(); +app.use(express.json()); + +// GET /instances — returns catalog with MCP endpoint URLs +app.get("/instances", (_req: Request, res: Response) => { + res.json( + instances.map((i) => ({ + id: i.id, + name: i.name, + description: i.description, + mcpUrl: `${MCP_SERVER_BASE}/instances/${i.id}/mcp`, + })) + ); +}); + +app.listen(PORT, () => { + console.log(`Catalog server running on http://localhost:${PORT}`); + console.log(`MCP server base: ${MCP_SERVER_BASE}`); +}); diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/src/mcp-server/data.ts b/extensibility/mcp/dynamic-mcp-routing-typescript/src/mcp-server/data.ts new file mode 100644 index 00000000..f9c174a2 --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/src/mcp-server/data.ts @@ -0,0 +1,116 @@ +export interface Instance { + id: string; + name: string; + description: string; +} + +export interface Project { + id: string; + name: string; + description: string; +} + +export const instances: Instance[] = [ + { + id: "contoso", + name: "Contoso", + description: "Contoso Ltd — Global ERP transformation programme", + }, + { + id: "fabrikam", + name: "Fabrikam", + description: "Fabrikam Inc — Supply chain modernisation", + }, + { + id: "northwind", + name: "Northwind", + description: "Northwind Traders — Finance & HR digital transformation", + }, +]; + +export const projects: Record = { + contoso: [ + { id: "erp-rollout", name: "ERP Rollout", description: "SAP S/4HANA migration across 12 regions" }, + { id: "data-platform", name: "Data Platform", description: "Enterprise data lake and analytics platform build" }, + ], + fabrikam: [ + { id: "supply-chain", name: "Supply Chain Optimisation", description: "End-to-end supply chain visibility and automation" }, + { id: "warehouse-automation", name: "Warehouse Automation", description: "Robotics and IoT integration for 8 distribution centres" }, + { id: "vendor-portal", name: "Vendor Portal", description: "Self-service vendor onboarding and management portal" }, + ], + northwind: [ + { id: "hr-transformation", name: "HR Transformation", description: "Workday implementation and change management" }, + { id: "finance-modernisation", name: "Finance Modernisation", description: "Cloud-based finance platform with real-time reporting" }, + ], +}; + +export const projectDetails: Record> = { + contoso: { + "erp-rollout": { + name: "ERP Rollout", + status: "In Progress — Phase 3 of 5", + completion: "58%", + regions: { total: 12, completed: 7, inProgress: 2, pending: 3 }, + nextMilestone: "APAC wave (Q3)", + risks: [ + "Data migration quality in Singapore entity", + "Change adoption scores below target in Japan", + ], + budget: { allocated: "$48M", spent: "$29M", forecast: "$46M" }, + }, + "data-platform": { + name: "Data Platform", + status: "In Progress", + completion: "80%", + pipelines: { live: ["SAP", "Salesforce", "ServiceNow"], pending: ["Workday", "Jira"] }, + analyticsLayer: "Databricks — UAT with 3 business units", + blocker: "PII classification for GDPR compliance pending legal sign-off", + budget: { allocated: "$12M", spent: "$9.2M", forecast: "$11.5M" }, + }, + }, + fabrikam: { + "supply-chain": { + name: "Supply Chain Optimisation", + status: "Live — partial rollout", + productLines: { total: 6, live: 4 }, + forecastAccuracy: { before: "72%", after: "89%" }, + logistics: { partners: ["DHL", "FedEx"], integrationStatus: "Final testing" }, + openIssue: "Real-time tracking API latency exceeds SLA for ocean freight", + }, + "warehouse-automation": { + name: "Warehouse Automation", + status: "In Progress", + distributionCentres: { total: 8, automated: 3, nextUp: "Chicago DC (6 weeks)" }, + results: { throughputIncrease: "40% at Dallas site" }, + iotSensors: "Deployment on track", + }, + "vendor-portal": { + name: "Vendor Portal", + status: "Live", + vendors: { total: 340, onboarded: 120 }, + onboardingTime: { before: "14 days", after: "3 days" }, + upcoming: "Compliance document upload — next sprint", + }, + }, + northwind: { + "hr-transformation": { + name: "HR Transformation", + status: "Partially Live", + platform: "Workday", + liveModules: ["Core HCM", "Payroll"], + pendingModules: ["Talent", "Learning"], + employees: 8500, + adoptionScore: { current: "76%", target: "80%" }, + risk: "Payroll parallel run discrepancies in UK entity — needs resolution before month-end", + }, + "finance-modernisation": { + name: "Finance Modernisation", + status: "Partially Live", + platform: "Oracle Fusion", + liveModules: ["General Ledger", "Accounts Payable"], + nextQuarter: ["Accounts Receivable", "Fixed Assets"], + reporting: "Real-time dashboards in pilot with CFO office", + goal: "Reduce month-end close by 3 days via reconciliation automation", + }, + }, +}; diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/src/mcp-server/index.ts b/extensibility/mcp/dynamic-mcp-routing-typescript/src/mcp-server/index.ts new file mode 100644 index 00000000..0f7f82de --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/src/mcp-server/index.ts @@ -0,0 +1,157 @@ +import express, { Request, Response } from "express"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { z } from "zod"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { instances, projects, projectDetails } from "./data.js"; + +const PORT = parseInt(process.env.PORT || "3001"); + +// --- Tool schemas --- + +const ListProjectsSchema = z.object({}); + +const GetProjectDetailsSchema = z.object({ + projectId: z.string().describe("Project identifier"), +}); + +// --- Factory: creates a configured MCP Server for a given instance --- + +function createServer(instanceId: string): Server { + const instance = instances.find((i) => i.id === instanceId)!; + const instanceProjects = projects[instanceId]; + + const server = new Server( + { + name: `${instance.name} MCP Server`, + version: "1.0.0", + }, + { + capabilities: { tools: {} }, + } + ); + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: "list_projects", + description: `List all projects in the ${instance.name} instance.`, + inputSchema: zodToJsonSchema(ListProjectsSchema), + }, + { + name: "get_project_details", + description: + `Get details for a project in the ${instance.name} instance. ` + + `Available projects: ${instanceProjects.map((p) => `${p.id} (${p.name})`).join(", ")}`, + inputSchema: zodToJsonSchema(GetProjectDetailsSchema), + }, + ], + })); + + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + if (name === "list_projects") { + return { + content: [ + { type: "text", text: JSON.stringify(instanceProjects, null, 2) }, + ], + }; + } + + if (name === "get_project_details") { + const { projectId } = GetProjectDetailsSchema.parse(args); + const details = projectDetails[instanceId]?.[projectId]; + + if (!details) { + return { + content: [ + { + type: "text", + text: `Project '${projectId}' not found in ${instance.name}. ` + + `Available: ${instanceProjects.map((p) => p.id).join(", ")}`, + }, + ], + isError: true, + }; + } + + return { + content: [{ type: "text", text: JSON.stringify(details, null, 2) }], + }; + } + + throw new Error(`Unknown tool: ${name}`); + }); + + return server; +} + +// --- Express app --- + +const app = express(); +app.use(express.json()); + +// POST /instances/:instanceId/mcp — stateless: new transport per request, server per instance +app.post("/instances/:instanceId/mcp", async (req: Request, res: Response) => { + const { instanceId } = req.params; + if (!projects[instanceId]) { + res.status(404).json({ error: `Instance '${instanceId}' not found` }); + return; + } + + const server = createServer(instanceId); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + + res.on("close", () => { + transport.close(); + }); + + await server.connect(transport); + + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error(`Error handling MCP request for ${instanceId}:`, error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); + } + } +}); + +// GET & DELETE — 405 +app.get("/instances/:instanceId/mcp", (_req: Request, res: Response) => { + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { code: -32000, message: "Method not allowed." }, + id: null, + })); +}); + +app.delete("/instances/:instanceId/mcp", (_req: Request, res: Response) => { + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { code: -32000, message: "Method not allowed." }, + id: null, + })); +}); + +// --- Start --- +app.listen(PORT, () => { + console.log(`MCP server running on http://localhost:${PORT}`); + console.log(`\nMCP endpoints:`); + for (const inst of instances) { + console.log(` POST /instances/${inst.id}/mcp`.padEnd(48) + `— ${inst.name}`); + } +}); diff --git a/extensibility/mcp/dynamic-mcp-routing-typescript/tsconfig.json b/extensibility/mcp/dynamic-mcp-routing-typescript/tsconfig.json new file mode 100644 index 00000000..988dbfa6 --- /dev/null +++ b/extensibility/mcp/dynamic-mcp-routing-typescript/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "build"] +} diff --git a/extensibility/mcp/order-management-enhanced-tc/README.md b/extensibility/mcp/order-management-enhanced-tc/README.md new file mode 100644 index 00000000..4baa9fb2 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/README.md @@ -0,0 +1,160 @@ +--- +title: Order Management with Enhanced Task Completion +parent: MCP +grand_parent: Extensibility +nav_order: 3 +--- + +# Order Management with Enhanced Task Completion + +{: .warning } +> **Experimental feature.** Enhanced Task Completion is an experimental capability in Copilot Studio. See the [official documentation](https://github.com/microsoft/Agents/blob/main/docs/enhanced-task-completion.md) for current status and limitations. + +An end-to-end sample demonstrating Copilot Studio agents with [**Enhanced Task Completion**](https://github.com/microsoft/Agents/blob/main/docs/enhanced-task-completion.md) calling MCP servers for e-commerce order management and warehouse fulfillment, with a **Gradio chat UI** that renders tool calls, reasoning, and file attachments inline. + +## What is Enhanced Task Completion? + +Enhanced Task Completion shifts Copilot Studio from a "plan-then-execute" model to an adaptive, conversational approach. Instead of selecting all tools upfront, the agent: + +- **Reasons before acting** — asks clarifying questions and gathers context before calling tools +- **Orchestrates tools dynamically** — recognizes dependencies between tool outputs, parallelizes independent calls, and adjusts strategy based on intermediate results +- **Interleaves conversation and actions** — fluidly mixes questions, tool calls, and responses across multiple turns +- **Recovers from failures** — retries or finds alternative approaches when tool calls fail + +This sample demonstrates all of these capabilities through a realistic e-commerce customer service scenario where the agent chains 9 tools across two MCP servers and a connected agent to answer complex multi-part questions. + +![Gradio Chat UI](./assets/gradio-ui.png) + +## What's Included + +### Orders Agent (Copilot Studio) + +The primary agent with Enhanced Task Completion enabled. Handles customer inquiries by dynamically chaining tools from the Order Management MCP server. When a question involves inventory or fulfillment, it delegates to the Warehouse Agent as a connected agent. + +### Warehouse Agent (Copilot Studio) + +A connected agent invoked by the Orders Agent for warehouse and fulfillment queries. Calls tools from the Warehouse MCP server to check stock levels, track fulfillment pipeline stages, find alternative products, and look up restock dates. + +### Order Management MCP Server (5 tools) + +Node.js [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) server with interdependent tools for e-commerce order operations: + +| Tool | Input | Purpose | +|---|---|---| +| `search_orders` | Customer name/email/order# | Entry point — find orders | +| `get_order` | order_id | Full order details + line items | +| `get_shipment` | order_id | Tracking info (shipped/delivered only) | +| `request_return` | order_id, item_skus[], reason | Initiate a return | +| `get_return_status` | return_id | Check return progress | + +### Warehouse MCP Server (4 tools) + +Node.js Streamable HTTP server with interdependent tools for warehouse and fulfillment: + +| Tool | Input | Purpose | +|---|---|---| +| `check_stock` | SKU | Inventory levels + warehouse location | +| `get_fulfillment_status` | order_id | Pipeline stage (received → shipped) | +| `find_alternatives` | SKU | Similar products in stock | +| `get_restock_date` | SKU | Next inbound shipment date | + +### Gradio Chat UI + +Python frontend that connects to the Orders Agent via the [Microsoft Agents SDK](https://github.com/microsoft/Agents-for-python) and renders the full Enhanced Task Completion activity protocol inline: + +- **Reasoning steps** — agent thinking displayed as collapsible accordions +- **Tool calls** — grouped with parameters, duration, and results +- **Intermediate messages** — agent narration between tool call batches +- **File upload/download** — CSV/text files sent as base64 attachments, agent-generated files offered for download +- **MSAL auth** — interactive login with persisted token cache (sign in once) + +### Custom Connectors + +Power Platform connector definitions (Swagger + apiProperties) that expose each MCP server as an action in Copilot Studio. The connectors use the `x-ms-agentic-protocol: mcp-streamable-1.0` extension to enable native MCP tool discovery. + +## Prerequisites + +- Node.js 18+ +- Python 3.12+ +- [Dev Tunnels CLI](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started) (`devtunnel`) +- A Power Platform environment with Copilot Studio +- An Entra ID app registration with `CopilotStudio.Copilots.Invoke` permission + +## Quick Start + +### 1. Install dependencies + +```bash +node scripts/setup.mjs +``` + +### 2. Import agents (first time only) + +Import `agents/solution/OrderManagementMCPDemo.zip` into your environment via **make.powerapps.com > Solutions > Import**. After import, create connections for each MCP connector from the **Custom connectors** page (no auth — just click **Create**). See [Importing the Agent Solutions](./agents/IMPORT) for details. + +### 3. Start MCP servers + tunnels + +```bash +node scripts/start.mjs +``` + +This starts both MCP servers and creates anonymous dev tunnels. Note the tunnel URLs printed: + +``` +Order Management MCP endpoint: https://xxxxx-3000.uks1.devtunnels.ms/mcp +Warehouse MCP endpoint: https://xxxxx-3001.uks1.devtunnels.ms/mcp +``` + +### 4. Update connector URLs + +Each time you restart (tunnels get new URLs), update the custom connector hosts: + +1. Go to **make.powerapps.com** > **Custom connectors** +2. Find **"orders mcp"** > click **Edit** > update the **Host** field with the order tunnel host (e.g., `xxxxx-3000.uks1.devtunnels.ms`) > click **Update connector** +3. Find **"warehouse server 3"** > click **Edit** > update the **Host** field with the warehouse tunnel host (e.g., `xxxxx-3001.uks1.devtunnels.ms`) > click **Update connector** + +No need to republish the agents — the connectors are referenced dynamically. + +### 5. Start the chat UI + +Configure the `.env` file with your agent details (requires an Entra ID App Registration). See [Chat UI Setup](./SETUP) for step-by-step instructions. + +```bash +cp chat-ui/.env.sample chat-ui/.env +# Edit chat-ui/.env — see SETUP.md for details +node scripts/start-ui.mjs +``` + +Open http://localhost:7860 and try one of these: + +**Basic order lookup:** +> Hi, I'm Sarah Mitchell. I ordered some Sony headphones recently but they arrived with a crackling sound in the left ear. I'd like to return them. + +**Cross-server (orders + warehouse):** +> I'm James Rivera. My Nintendo Switch order hasn't shipped yet. When can I expect it? If it's not available, what are my options? + +**File upload — populate a CSV:** +> Upload `chat-ui/data/demo-orders.csv` and ask: "Fill in all the empty columns for each order and return the completed CSV." + +## Architecture + +```mermaid +graph TB + User([fa:fa-user User]) -->|chat| GradioUI + + subgraph Local Machine + GradioUI["Gradio Chat UI
Port 7860
Reasoning, tool calls,
file upload/download
"] + OrderMCP["Order Management
MCP Server
5 tools · Port 3000"] + WarehouseMCP["Warehouse
MCP Server
4 tools · Port 3001"] + end + + subgraph Copilot Studio + OrdersAgent["Orders Agent
Enhanced Task Completion"] + WarehouseAgent["Warehouse Agent
connected agent"] + OrdersAgent -->|invokes| WarehouseAgent + end + + GradioUI -->|"Agents SDK
(streaming)"| OrdersAgent + OrdersAgent -->|"MCP Action
(via connector)"| OrderMCP + WarehouseAgent -->|"MCP Action
(via connector)"| WarehouseMCP +``` diff --git a/extensibility/mcp/order-management-enhanced-tc/SETUP.md b/extensibility/mcp/order-management-enhanced-tc/SETUP.md new file mode 100644 index 00000000..4858ba2a --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/SETUP.md @@ -0,0 +1,39 @@ +--- +title: Chat UI Setup +parent: Order Management with Enhanced Task Completion +grand_parent: MCP +nav_exclude: true +--- + +# Chat UI Setup + +The Gradio chat UI authenticates against Copilot Studio using MSAL interactive login. You need an Entra ID App Registration and a `.env` file with your agent details. + +## App Registration + +1. Go to **portal.azure.com** > **App registrations** > **New registration** +2. Name: e.g., "MCP Demo Chat Client" +3. Supported account types: **Single tenant** +4. Redirect URI: **Public client/native** > `http://localhost` +5. After creation, go to **API permissions** > **Add a permission** > **APIs my organization uses** +6. Search for **CopilotStudio** > select **CopilotStudio.Copilots.Invoke** (delegated) +7. Click **Grant admin consent** +8. Copy the **Application (client) ID** — this is your `AGENTAPPID` below + +## Configure `.env` + +Copy `chat-ui/.env.sample` to `chat-ui/.env` and fill in: + +```env +COPILOTSTUDIOAGENT__ENVIRONMENTID= +COPILOTSTUDIOAGENT__SCHEMANAME= +COPILOTSTUDIOAGENT__TENANTID= +COPILOTSTUDIOAGENT__AGENTAPPID= +``` + +| Variable | Where to find it | +|---|---| +| `ENVIRONMENTID` | The GUID in your Power Platform URL, or **Settings** > **Session details** in Copilot Studio | +| `SCHEMANAME` | Copilot Studio > open the Orders Agent > **Settings** > **Advanced** > **Schema name** | +| `TENANTID` | Your Entra ID tenant ID (Azure Portal > **Microsoft Entra ID** > **Overview**) | +| `AGENTAPPID` | The Application (client) ID from the App Registration above | diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/IMPORT.md b/extensibility/mcp/order-management-enhanced-tc/agents/IMPORT.md new file mode 100644 index 00000000..bf5623d9 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/IMPORT.md @@ -0,0 +1,38 @@ +--- +title: Importing the Agent Solutions +parent: Order Management with Enhanced Task Completion +grand_parent: MCP +nav_exclude: true +--- + +# Importing the Agent Solutions + +## Prerequisites + +- A Power Platform environment with Copilot Studio +- Admin or Maker role in the target environment + +## Steps + +### 1. Import the solution + +The solution zip contains both agents, their custom connectors, and connection references — all in one package. + +1. Go to [make.powerapps.com](https://make.powerapps.com) +2. Select your target environment +3. Navigate to **Solutions** > **Import solution** +4. Upload `solution/OrderManagementMCPDemo.zip` +5. Click **Next** through the details page +6. If the import wizard shows a **Connections** page, click **New connection** for each connector (no auth needed — just click **Create**), then select the connections you just created +7. Click **Import** + +### 2. Create connections + +After import, go to **Custom connectors** in the left nav. For each MCP connector (**orders mcp** and **warehouse server 3**), click **Create connection**. The MCP connectors have no authentication — just click **Create** with no credentials. + +{: .note } +> In a clean environment the import wizard may skip the Connections step entirely. Creating connections from the Custom connectors page ensures the agents can reach the MCP servers. + +### 3. Publish agents + +In Copilot Studio, open each agent and click **Publish** to make the latest version live. diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/solution/OrderManagementMCPDemo.zip b/extensibility/mcp/order-management-enhanced-tc/agents/solution/OrderManagementMCPDemo.zip new file mode 100644 index 00000000..e3496a51 Binary files /dev/null and b/extensibility/mcp/order-management-enhanced-tc/agents/solution/OrderManagementMCPDemo.zip differ diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Assets/botcomponent_connectionreferenceset.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Assets/botcomponent_connectionreferenceset.xml new file mode 100644 index 00000000..b0c24108 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Assets/botcomponent_connectionreferenceset.xml @@ -0,0 +1,8 @@ + + + 1 + + + 1 + + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_connectionparameters.json b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_connectionparameters.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_connectionparameters.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_iconblob.Png b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_iconblob.Png new file mode 100644 index 00000000..f417fb88 Binary files /dev/null and b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_iconblob.Png differ diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_openapidefinition.json b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_openapidefinition.json new file mode 100644 index 00000000..212f1790 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_openapidefinition.json @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"title":"orders mcp","description":"my orders mcp server connector","version":"1.0.0"},"host":"x1kv1q93-3000.uks1.devtunnels.ms","basePath":"/","schemes":["https"],"paths":{"/mcp":{"post":{"responses":{"200":{"description":"Immediate Response"}},"x-ms-agentic-protocol":"mcp-streamable-1.0","operationId":"InvokeServer","summary":"orders mcp","description":"my orders mcp server connector"}}},"securityDefinitions":{},"security":[]} \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_policytemplateinstances.json b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_policytemplateinstances.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Forders-20mcp_policytemplateinstances.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_connectionparameters.json b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_connectionparameters.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_connectionparameters.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_iconblob.Png b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_iconblob.Png new file mode 100644 index 00000000..f417fb88 Binary files /dev/null and b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_iconblob.Png differ diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_openapidefinition.json b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_openapidefinition.json new file mode 100644 index 00000000..a92b8077 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_openapidefinition.json @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"title":"warehouse server 3","description":"my warehouse server 3 connector","version":"1.0.0"},"host":"73gh7mpk-3001.uks1.devtunnels.ms","basePath":"/","schemes":["https"],"paths":{"/mcp":{"post":{"responses":{"200":{"description":"Immediate Response"}},"x-ms-agentic-protocol":"mcp-streamable-1.0","operationId":"InvokeServer","summary":"warehouse server 3","description":"my warehouse server 3 connector"}}},"securityDefinitions":{},"security":[]} \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_policytemplateinstances.json b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_policytemplateinstances.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/Connector/new_5Fwarehouse-20server-203_policytemplateinstances.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/[Content_Types].xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/[Content_Types].xml new file mode 100644 index 00000000..5ac167a1 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/[Content_Types].xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.InvokeConnectedAgentTaskAction.Warehouseagent/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.InvokeConnectedAgentTaskAction.Warehouseagent/botcomponent.xml new file mode 100644 index 00000000..258b6eb7 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.InvokeConnectedAgentTaskAction.Warehouseagent/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + warehouse agent + 0 + Warehouse agent + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.InvokeConnectedAgentTaskAction.Warehouseagent/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.InvokeConnectedAgentTaskAction.Warehouseagent/data new file mode 100644 index 00000000..82298e1b --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.InvokeConnectedAgentTaskAction.Warehouseagent/data @@ -0,0 +1,8 @@ +kind: TaskDialog +modelDisplayName: Warehouse agent +modelDescription: warehouse agent used for inventory queries +action: + kind: InvokeConnectedAgentTaskAction + botSchemaName: cr26e_Warehouseagent + historyType: + kind: ConversationHistory \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.InvokeConnectedAgentTaskAction.Warehouseagent/dependencies.json b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.InvokeConnectedAgentTaskAction.Warehouseagent/dependencies.json new file mode 100644 index 00000000..80663960 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.InvokeConnectedAgentTaskAction.Warehouseagent/dependencies.json @@ -0,0 +1 @@ +[{"type":"bot","schemaName":"cr26e_Warehouseagent"}] \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.gpt.default/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.gpt.default/botcomponent.xml new file mode 100644 index 00000000..f65e9e30 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.gpt.default/botcomponent.xml @@ -0,0 +1,10 @@ + + 15 + 0 + Orders Agent + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.gpt.default/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.gpt.default/data new file mode 100644 index 00000000..2ac0fa5b --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.gpt.default/data @@ -0,0 +1,12 @@ +kind: GptComponentMetadata +instructions: +gptCapabilities: + webBrowsing: true + codeInterpreter: true + +aISettings: + model: + modelNameHint: Sonnet46 + + extensionData: + lastUsedCustomModel: {} \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ConversationStart/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ConversationStart/botcomponent.xml new file mode 100644 index 00000000..59b7170a --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ConversationStart/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This system topic triggers when the agent receives an Activity indicating the beginning of a new conversation. If you do not want the agent to initiate the conversation, disable this topic. + 0 + Conversation Start + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ConversationStart/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ConversationStart/data new file mode 100644 index 00000000..dcee754c --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ConversationStart/data @@ -0,0 +1,12 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnConversationStart + id: main + actions: + - kind: SendActivity + id: sendMessage_M0LuhV + activity: + text: + - Hello, I'm {System.Bot.Name}. How can I help? + speak: + - Hello and thank you for calling {System.Bot.Name}, powered by generative AI. How may I help you today? \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.EndofConversation/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.EndofConversation/botcomponent.xml new file mode 100644 index 00000000..57917355 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.EndofConversation/botcomponent.xml @@ -0,0 +1,12 @@ + + 9 + This system topic is only triggered by a redirect action, +and guides the user through rating their conversation with the agent. + 0 + End of Conversation + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.EndofConversation/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.EndofConversation/data new file mode 100644 index 00000000..84b5f6b7 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.EndofConversation/data @@ -0,0 +1,75 @@ +kind: AdaptiveDialog +startBehavior: CancelOtherTopics +beginDialog: + kind: OnSystemRedirect + id: main + actions: + - kind: Question + id: 41d42054-d4cb-4e90-b922-2b16b37fe379 + conversationOutcome: ResolvedImplied + alwaysPrompt: true + variable: init:Topic.SurveyResponse + prompt: Did that answer your question? + entity: BooleanPrebuiltEntity + + - kind: ConditionGroup + id: condition-0 + conditions: + - id: condition-0-item-0 + condition: =Topic.SurveyResponse = true + actions: + - kind: CSATQuestion + id: csat_1 + conversationOutcome: ResolvedConfirmed + + - kind: SendActivity + id: sendMessage_8r29O0 + activity: Thanks for your feedback. + + - kind: Question + id: question_1 + alwaysPrompt: true + variable: init:Topic.Continue + prompt: Can I help with anything else? + entity: BooleanPrebuiltEntity + + - kind: ConditionGroup + id: condition-1 + conditions: + - id: condition-1-item-0 + condition: =Topic.Continue = true + actions: + - kind: SendActivity + id: sendMessage_4eOE6h + activity: Go ahead. I'm listening. + + elseActions: + - kind: SendActivity + id: yHBz55 + activity: Ok, goodbye. + + - kind: EndConversation + id: jh1GMT + + elseActions: + - kind: Question + id: PM68ot + alwaysPrompt: true + variable: init:Topic.TryAgain + prompt: Sorry I wasn't able to help better. Would you like to try again? + entity: BooleanPrebuiltEntity + + - kind: ConditionGroup + id: KNxYBf + conditions: + - id: DPveFP + condition: =Topic.TryAgain = false + actions: + - kind: BeginDialog + id: cngqi4 + dialog: cr26e_OrdersAgent.topic.Escalate + + elseActions: + - kind: SendActivity + id: GrVHEW + activity: Go ahead. I'm listening. \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Escalate/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Escalate/botcomponent.xml new file mode 100644 index 00000000..a47150d5 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Escalate/botcomponent.xml @@ -0,0 +1,13 @@ + + 9 + This system topic is triggered when the user indicates they would like to speak to a representative. +You can configure how the agent will handle human hand-off scenarios in the agent settings.. +If your agent does not handle escalations, this topic should be disabled. + 0 + Escalate + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Escalate/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Escalate/data new file mode 100644 index 00000000..5e3cab08 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Escalate/data @@ -0,0 +1,60 @@ +kind: AdaptiveDialog +startBehavior: CancelOtherTopics +beginDialog: + kind: OnEscalate + id: main + intent: + displayName: Escalate + includeInOnSelectIntent: false + triggerQueries: + - Talk to agent + - Talk to a person + - Talk to someone + - Call back + - Call customer service + - Call me please + - Call support + - Call technical support + - Can an agent call me + - Can I call + - Can I get in touch with someone else + - Can I get real agent support + - Can I get transferred to a person to call + - Can I have a call in number Or can I be called + - Can I have a representative call me + - Can I schedule a call + - Can I speak to a representative + - Can I talk to a human + - Can I talk to a human assistant + - Can someone call me + - Chat with a human + - Chat with a representative + - Chat with agent + - Chat with someone please + - Connect me to a live agent + - Connect me to a person + - Could some one contact me by phone + - Customer agent + - Customer representative + - Customer service + - I need a manager to contact me + - I need customer service + - I need help from a person + - I need to speak with a live argent + - I need to talk to a specialist please + - I want to talk to customer service + - I want to proceed with live support + - I want to speak with a consultant + - I want to speak with a live tech + - I would like to speak with an associate + - I would like to talk to a technician + - Talk with tech support member + + actions: + - kind: SendActivity + id: sendMessage_s39DCt + conversationOutcome: Escalated + activity: |- + Escalating to a representative is not currently configured for this agent, however this is where the agent could provide information about how to get in touch with someone another way. + + Is there anything else I can help you with? \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Fallback/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Fallback/botcomponent.xml new file mode 100644 index 00000000..d0b5dc3a --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Fallback/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This system topic triggers when the user's utterance does not match any existing topics. + 0 + Fallback + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Fallback/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Fallback/data new file mode 100644 index 00000000..0c269cca --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Fallback/data @@ -0,0 +1,19 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnUnknownIntent + id: main + actions: + - kind: ConditionGroup + id: conditionGroup_LktzXw + conditions: + - id: conditionItem_tlGIVo + condition: =System.FallbackCount < 3 + actions: + - kind: SendActivity + id: sendMessage_QZreqo + activity: I'm sorry, I'm not sure how to help with that. Can you try rephrasing? + + elseActions: + - kind: BeginDialog + id: 5aXj5M + dialog: cr26e_OrdersAgent.topic.Escalate \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Goodbye/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Goodbye/botcomponent.xml new file mode 100644 index 00000000..8999e68a --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Goodbye/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This topic triggers when the user says goodbye. By default, it does not end the conversation. If you would like to end the conversation when the user says goodbye, you can add an "End of Conversation" action to this topic, or redirect to the "End of Conversation" system topic. + 0 + Goodbye + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Goodbye/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Goodbye/data new file mode 100644 index 00000000..82aef5ef --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Goodbye/data @@ -0,0 +1,39 @@ +kind: AdaptiveDialog +startBehavior: CancelOtherTopics +beginDialog: + kind: OnRecognizedIntent + id: main + intent: + displayName: Goodbye + includeInOnSelectIntent: false + triggerQueries: + - Bye + - Bye for now + - Bye now + - Good bye + - No thank you. Goodbye. + - See you later + + actions: + - kind: Question + id: question_zf2HhP + variable: Topic.EndConversation + prompt: Would you like to end our conversation? + entity: BooleanPrebuiltEntity + + - kind: ConditionGroup + id: condition_DGc1Wy + conditions: + - id: condition_DGc1Wy-item-0 + condition: =Topic.EndConversation = true + actions: + - kind: BeginDialog + id: dn94DC + dialog: cr26e_OrdersAgent.topic.EndofConversation + + - id: condition_DGc1Wy-item-1 + condition: =Topic.EndConversation = false + actions: + - kind: SendActivity + id: sendMessage_LdLhmf + activity: Go ahead. I'm listening. \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Greeting/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Greeting/botcomponent.xml new file mode 100644 index 00000000..9202bf88 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Greeting/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This topic is triggered when the user greets the agent. + 0 + Greeting + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Greeting/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Greeting/data new file mode 100644 index 00000000..ce02e8fb --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Greeting/data @@ -0,0 +1,25 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnRecognizedIntent + id: main + intent: + displayName: Greeting + includeInOnSelectIntent: false + triggerQueries: + - Good afternoon + - Good morning + - Hello + - Hey + - Hi + + actions: + - kind: SendActivity + id: sendMessage_abmysR + activity: + text: + - Hello, how can I help you today? + speak: + - Hello, how can I help? + + - kind: CancelAllDialogs + id: cancelAllDialogs_01At22 \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.MultipleTopicsMatched/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.MultipleTopicsMatched/botcomponent.xml new file mode 100644 index 00000000..d726b5c8 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.MultipleTopicsMatched/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This system topic triggers when the agent matches multiple Topics with the incoming message and needs to clarify which one should be triggered. + 0 + Multiple Topics Matched + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.MultipleTopicsMatched/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.MultipleTopicsMatched/data new file mode 100644 index 00000000..454562a0 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.MultipleTopicsMatched/data @@ -0,0 +1,43 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnSelectIntent + id: main + triggerBehavior: Always + actions: + - kind: SetVariable + id: setVariable_M6434i + variable: init:Topic.IntentOptions + value: =System.Recognizer.IntentOptions + + - kind: SetTextVariable + id: setTextVariable_0 + variable: Topic.NoneOfTheseDisplayName + value: None of these + + - kind: EditTable + id: sendMessage_g5Ls09 + changeType: Add + itemsVariable: Topic.IntentOptions + value: "={ DisplayName: Topic.NoneOfTheseDisplayName, TopicId: \"NoTopic\", TriggerId: \"NoTrigger\", Score: 1.0 }" + + - kind: Question + id: question_zf2HhP + interruptionPolicy: + allowInterruption: false + + alwaysPrompt: true + variable: System.Recognizer.SelectedIntent + prompt: "To clarify, did you mean:" + entity: + kind: DynamicClosedListEntity + items: =Topic.IntentOptions + + - kind: ConditionGroup + id: conditionGroup_60PuXb + conditions: + - id: conditionItem_rs7GgM + condition: =System.Recognizer.SelectedIntent.TopicId = "NoTopic" + actions: + - kind: ReplaceDialog + id: YZXRDb + dialog: cr26e_OrdersAgent.topic.Fallback \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.OnError/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.OnError/botcomponent.xml new file mode 100644 index 00000000..94cd4cc2 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.OnError/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This system topic triggers when the agent encounters an error. When using the test chat pane, the full error description is displayed. + 0 + On Error + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.OnError/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.OnError/data new file mode 100644 index 00000000..29bec599 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.OnError/data @@ -0,0 +1,45 @@ +kind: AdaptiveDialog +startBehavior: UseLatestPublishedContentAndCancelOtherTopics +beginDialog: + kind: OnError + id: main + actions: + - kind: SetVariable + id: setVariable_timestamp + variable: init:Topic.CurrentTime + value: =Text(Now(), DateTimeFormat.UTC) + + - kind: ConditionGroup + id: condition_1 + conditions: + - id: bL4wmY + condition: =System.Conversation.InTestMode = true + actions: + - kind: SendActivity + id: sendMessage_XJBYMo + activity: |- + Error Message: {System.Error.Message} + Error Code: {System.Error.Code} + Conversation Id: {System.Conversation.Id} + Time (UTC): {Topic.CurrentTime} + + elseActions: + - kind: SendActivity + id: sendMessage_dZ0gaF + activity: + text: + - |- + An error has occurred. + Error code: {System.Error.Code} + Conversation Id: {System.Conversation.Id} + Time (UTC): {Topic.CurrentTime}. + speak: + - An error has occurred, please try again. + + - kind: LogCustomTelemetryEvent + id: 9KwEAn + eventName: OnErrorLog + properties: "={ErrorMessage: System.Error.Message, ErrorCode: System.Error.Code, TimeUTC: Topic.CurrentTime, ConversationId: System.Conversation.Id}" + + - kind: CancelAllDialogs + id: NW7NyY \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ResetConversation/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ResetConversation/botcomponent.xml new file mode 100644 index 00000000..873ab8f9 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ResetConversation/botcomponent.xml @@ -0,0 +1,10 @@ + + 9 + 0 + Reset Conversation + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ResetConversation/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ResetConversation/data new file mode 100644 index 00000000..3d427e1d --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ResetConversation/data @@ -0,0 +1,16 @@ +kind: AdaptiveDialog +startBehavior: UseLatestPublishedContentAndCancelOtherTopics +beginDialog: + kind: OnSystemRedirect + id: main + actions: + - kind: SendActivity + id: sendMessage_OPsT1O + activity: What can I help you with? + + - kind: ClearAllVariables + id: clearAllVariables_73bTFR + variables: ConversationScopedVariables + + - kind: CancelAllDialogs + id: cancelAllDialogs_12Gt21 \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Search/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Search/botcomponent.xml new file mode 100644 index 00000000..1db4983a --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Search/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + Create generative answers from knowledge sources. + 0 + Conversational boosting + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Search/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Search/data new file mode 100644 index 00000000..9081f12f --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Search/data @@ -0,0 +1,20 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnUnknownIntent + id: main + priority: -1 + actions: + - kind: SearchAndSummarizeContent + id: search-content + variable: Topic.Answer + userInput: =System.Activity.Text + + - kind: ConditionGroup + id: has-answer-conditions + conditions: + - id: has-answer + condition: =!IsBlank(Topic.Answer) + actions: + - kind: EndDialog + id: end-topic + clearTopicQueue: true \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Signin/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Signin/botcomponent.xml new file mode 100644 index 00000000..9bf4e472 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Signin/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This system topic triggers when the agent needs to sign in the user or require the user to sign in + 0 + Sign in + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Signin/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Signin/data new file mode 100644 index 00000000..c4067bc3 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.Signin/data @@ -0,0 +1,19 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnSignIn + id: main + actions: + - kind: ConditionGroup + id: conditionGroup_ypjGKL + conditions: + - id: conditionItem_7XYIIR + condition: =System.SignInReason = SignInReason.SignInRequired + actions: + - kind: SendActivity + id: sendMessage_1jHUNO + activity: Hello! To be able to help you, I'll need you to sign in. + + - kind: OAuthInput + id: gOjhZA + title: Login + text: To continue, please login \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.StartOver/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.StartOver/botcomponent.xml new file mode 100644 index 00000000..44cd4617 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.StartOver/botcomponent.xml @@ -0,0 +1,10 @@ + + 9 + 0 + Start Over + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.StartOver/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.StartOver/data new file mode 100644 index 00000000..d01d036a --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.StartOver/data @@ -0,0 +1,35 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnRecognizedIntent + id: main + intent: + displayName: Start Over + includeInOnSelectIntent: false + triggerQueries: + - let's begin again + - start over + - start again + - restart + + actions: + - kind: Question + id: question_zguoVV + alwaysPrompt: false + variable: init:Topic.Confirm + prompt: Are you sure you want to restart the conversation? + entity: BooleanPrebuiltEntity + + - kind: ConditionGroup + id: conditionGroup_lvx2zV + conditions: + - id: conditionItem_sVQtHa + condition: =Topic.Confirm = true + actions: + - kind: BeginDialog + id: 0YKYsy + dialog: cr26e_OrdersAgent.topic.ResetConversation + + elseActions: + - kind: SendActivity + id: sendMessage_lk2CyQ + activity: Ok. Let's carry on. \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ThankYou/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ThankYou/botcomponent.xml new file mode 100644 index 00000000..787f72be --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ThankYou/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This topic triggers when the user says thank you. + 0 + Thank you + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ThankYou/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ThankYou/data new file mode 100644 index 00000000..9b816ed3 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ThankYou/data @@ -0,0 +1,17 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnRecognizedIntent + id: main + intent: + displayName: Thank you + includeInOnSelectIntent: false + triggerQueries: + - thanks + - thank you + - thanks so much + - ty + + actions: + - kind: SendActivity + id: sendMessage_9iz6v7 + activity: You're welcome. \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ordersmcp/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ordersmcp/botcomponent.xml new file mode 100644 index 00000000..dcee6f91 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ordersmcp/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + my orders mcp server connector + 0 + orders mcp + + cr26e_OrdersAgent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ordersmcp/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ordersmcp/data new file mode 100644 index 00000000..0716f77a --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_OrdersAgent.topic.ordersmcp/data @@ -0,0 +1,12 @@ +kind: TaskDialog +modelDisplayName: orders mcp +modelDescription: my orders mcp server connector +action: + kind: InvokeExternalAgentTaskAction + connectionReference: cr26e_OrdersAgent.shared_new-5forders-20mcp-5fee08b8354fad177a.6be7a421f5184b9ebb376965cf6cd8b8 + connectionProperties: + mode: Maker + + operationDetails: + kind: ModelContextProtocolMetadata + operationId: InvokeServer \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.gpt.default/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.gpt.default/botcomponent.xml new file mode 100644 index 00000000..8e6178e2 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.gpt.default/botcomponent.xml @@ -0,0 +1,10 @@ + + 15 + 0 + Warehouse agent + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.gpt.default/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.gpt.default/data new file mode 100644 index 00000000..2ac0fa5b --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.gpt.default/data @@ -0,0 +1,12 @@ +kind: GptComponentMetadata +instructions: +gptCapabilities: + webBrowsing: true + codeInterpreter: true + +aISettings: + model: + modelNameHint: Sonnet46 + + extensionData: + lastUsedCustomModel: {} \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ConversationStart/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ConversationStart/botcomponent.xml new file mode 100644 index 00000000..70dc9412 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ConversationStart/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This system topic triggers when the agent receives an Activity indicating the beginning of a new conversation. If you do not want the agent to initiate the conversation, disable this topic. + 0 + Conversation Start + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ConversationStart/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ConversationStart/data new file mode 100644 index 00000000..dcee754c --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ConversationStart/data @@ -0,0 +1,12 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnConversationStart + id: main + actions: + - kind: SendActivity + id: sendMessage_M0LuhV + activity: + text: + - Hello, I'm {System.Bot.Name}. How can I help? + speak: + - Hello and thank you for calling {System.Bot.Name}, powered by generative AI. How may I help you today? \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.EndofConversation/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.EndofConversation/botcomponent.xml new file mode 100644 index 00000000..def25ec9 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.EndofConversation/botcomponent.xml @@ -0,0 +1,12 @@ + + 9 + This system topic is only triggered by a redirect action, +and guides the user through rating their conversation with the agent. + 0 + End of Conversation + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.EndofConversation/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.EndofConversation/data new file mode 100644 index 00000000..570c0448 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.EndofConversation/data @@ -0,0 +1,75 @@ +kind: AdaptiveDialog +startBehavior: CancelOtherTopics +beginDialog: + kind: OnSystemRedirect + id: main + actions: + - kind: Question + id: 41d42054-d4cb-4e90-b922-2b16b37fe379 + conversationOutcome: ResolvedImplied + alwaysPrompt: true + variable: init:Topic.SurveyResponse + prompt: Did that answer your question? + entity: BooleanPrebuiltEntity + + - kind: ConditionGroup + id: condition-0 + conditions: + - id: condition-0-item-0 + condition: =Topic.SurveyResponse = true + actions: + - kind: CSATQuestion + id: csat_1 + conversationOutcome: ResolvedConfirmed + + - kind: SendActivity + id: sendMessage_8r29O0 + activity: Thanks for your feedback. + + - kind: Question + id: question_1 + alwaysPrompt: true + variable: init:Topic.Continue + prompt: Can I help with anything else? + entity: BooleanPrebuiltEntity + + - kind: ConditionGroup + id: condition-1 + conditions: + - id: condition-1-item-0 + condition: =Topic.Continue = true + actions: + - kind: SendActivity + id: sendMessage_4eOE6h + activity: Go ahead. I'm listening. + + elseActions: + - kind: SendActivity + id: yHBz55 + activity: Ok, goodbye. + + - kind: EndConversation + id: jh1GMT + + elseActions: + - kind: Question + id: PM68ot + alwaysPrompt: true + variable: init:Topic.TryAgain + prompt: Sorry I wasn't able to help better. Would you like to try again? + entity: BooleanPrebuiltEntity + + - kind: ConditionGroup + id: KNxYBf + conditions: + - id: DPveFP + condition: =Topic.TryAgain = false + actions: + - kind: BeginDialog + id: cngqi4 + dialog: cr26e_Warehouseagent.topic.Escalate + + elseActions: + - kind: SendActivity + id: GrVHEW + activity: Go ahead. I'm listening. \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Escalate/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Escalate/botcomponent.xml new file mode 100644 index 00000000..3333b332 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Escalate/botcomponent.xml @@ -0,0 +1,13 @@ + + 9 + This system topic is triggered when the user indicates they would like to speak to a representative. +You can configure how the agent will handle human hand-off scenarios in the agent settings.. +If your agent does not handle escalations, this topic should be disabled. + 0 + Escalate + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Escalate/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Escalate/data new file mode 100644 index 00000000..5e3cab08 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Escalate/data @@ -0,0 +1,60 @@ +kind: AdaptiveDialog +startBehavior: CancelOtherTopics +beginDialog: + kind: OnEscalate + id: main + intent: + displayName: Escalate + includeInOnSelectIntent: false + triggerQueries: + - Talk to agent + - Talk to a person + - Talk to someone + - Call back + - Call customer service + - Call me please + - Call support + - Call technical support + - Can an agent call me + - Can I call + - Can I get in touch with someone else + - Can I get real agent support + - Can I get transferred to a person to call + - Can I have a call in number Or can I be called + - Can I have a representative call me + - Can I schedule a call + - Can I speak to a representative + - Can I talk to a human + - Can I talk to a human assistant + - Can someone call me + - Chat with a human + - Chat with a representative + - Chat with agent + - Chat with someone please + - Connect me to a live agent + - Connect me to a person + - Could some one contact me by phone + - Customer agent + - Customer representative + - Customer service + - I need a manager to contact me + - I need customer service + - I need help from a person + - I need to speak with a live argent + - I need to talk to a specialist please + - I want to talk to customer service + - I want to proceed with live support + - I want to speak with a consultant + - I want to speak with a live tech + - I would like to speak with an associate + - I would like to talk to a technician + - Talk with tech support member + + actions: + - kind: SendActivity + id: sendMessage_s39DCt + conversationOutcome: Escalated + activity: |- + Escalating to a representative is not currently configured for this agent, however this is where the agent could provide information about how to get in touch with someone another way. + + Is there anything else I can help you with? \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Fallback/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Fallback/botcomponent.xml new file mode 100644 index 00000000..bc266aab --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Fallback/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This system topic triggers when the user's utterance does not match any existing topics. + 0 + Fallback + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Fallback/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Fallback/data new file mode 100644 index 00000000..23b73e69 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Fallback/data @@ -0,0 +1,19 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnUnknownIntent + id: main + actions: + - kind: ConditionGroup + id: conditionGroup_LktzXw + conditions: + - id: conditionItem_tlGIVo + condition: =System.FallbackCount < 3 + actions: + - kind: SendActivity + id: sendMessage_QZreqo + activity: I'm sorry, I'm not sure how to help with that. Can you try rephrasing? + + elseActions: + - kind: BeginDialog + id: 5aXj5M + dialog: cr26e_Warehouseagent.topic.Escalate \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Goodbye/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Goodbye/botcomponent.xml new file mode 100644 index 00000000..b14488c8 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Goodbye/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This topic triggers when the user says goodbye. By default, it does not end the conversation. If you would like to end the conversation when the user says goodbye, you can add an "End of Conversation" action to this topic, or redirect to the "End of Conversation" system topic. + 0 + Goodbye + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Goodbye/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Goodbye/data new file mode 100644 index 00000000..c26593a1 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Goodbye/data @@ -0,0 +1,39 @@ +kind: AdaptiveDialog +startBehavior: CancelOtherTopics +beginDialog: + kind: OnRecognizedIntent + id: main + intent: + displayName: Goodbye + includeInOnSelectIntent: false + triggerQueries: + - Bye + - Bye for now + - Bye now + - Good bye + - No thank you. Goodbye. + - See you later + + actions: + - kind: Question + id: question_zf2HhP + variable: Topic.EndConversation + prompt: Would you like to end our conversation? + entity: BooleanPrebuiltEntity + + - kind: ConditionGroup + id: condition_DGc1Wy + conditions: + - id: condition_DGc1Wy-item-0 + condition: =Topic.EndConversation = true + actions: + - kind: BeginDialog + id: dn94DC + dialog: cr26e_Warehouseagent.topic.EndofConversation + + - id: condition_DGc1Wy-item-1 + condition: =Topic.EndConversation = false + actions: + - kind: SendActivity + id: sendMessage_LdLhmf + activity: Go ahead. I'm listening. \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Greeting/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Greeting/botcomponent.xml new file mode 100644 index 00000000..01381e20 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Greeting/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This topic is triggered when the user greets the agent. + 0 + Greeting + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Greeting/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Greeting/data new file mode 100644 index 00000000..ce02e8fb --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Greeting/data @@ -0,0 +1,25 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnRecognizedIntent + id: main + intent: + displayName: Greeting + includeInOnSelectIntent: false + triggerQueries: + - Good afternoon + - Good morning + - Hello + - Hey + - Hi + + actions: + - kind: SendActivity + id: sendMessage_abmysR + activity: + text: + - Hello, how can I help you today? + speak: + - Hello, how can I help? + + - kind: CancelAllDialogs + id: cancelAllDialogs_01At22 \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.MultipleTopicsMatched/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.MultipleTopicsMatched/botcomponent.xml new file mode 100644 index 00000000..235b79ca --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.MultipleTopicsMatched/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This system topic triggers when the agent matches multiple Topics with the incoming message and needs to clarify which one should be triggered. + 0 + Multiple Topics Matched + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.MultipleTopicsMatched/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.MultipleTopicsMatched/data new file mode 100644 index 00000000..4b7bd875 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.MultipleTopicsMatched/data @@ -0,0 +1,43 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnSelectIntent + id: main + triggerBehavior: Always + actions: + - kind: SetVariable + id: setVariable_M6434i + variable: init:Topic.IntentOptions + value: =System.Recognizer.IntentOptions + + - kind: SetTextVariable + id: setTextVariable_0 + variable: Topic.NoneOfTheseDisplayName + value: None of these + + - kind: EditTable + id: sendMessage_g5Ls09 + changeType: Add + itemsVariable: Topic.IntentOptions + value: "={ DisplayName: Topic.NoneOfTheseDisplayName, TopicId: \"NoTopic\", TriggerId: \"NoTrigger\", Score: 1.0 }" + + - kind: Question + id: question_zf2HhP + interruptionPolicy: + allowInterruption: false + + alwaysPrompt: true + variable: System.Recognizer.SelectedIntent + prompt: "To clarify, did you mean:" + entity: + kind: DynamicClosedListEntity + items: =Topic.IntentOptions + + - kind: ConditionGroup + id: conditionGroup_60PuXb + conditions: + - id: conditionItem_rs7GgM + condition: =System.Recognizer.SelectedIntent.TopicId = "NoTopic" + actions: + - kind: ReplaceDialog + id: YZXRDb + dialog: cr26e_Warehouseagent.topic.Fallback \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.OnError/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.OnError/botcomponent.xml new file mode 100644 index 00000000..64b9014b --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.OnError/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This system topic triggers when the agent encounters an error. When using the test chat pane, the full error description is displayed. + 0 + On Error + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.OnError/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.OnError/data new file mode 100644 index 00000000..29bec599 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.OnError/data @@ -0,0 +1,45 @@ +kind: AdaptiveDialog +startBehavior: UseLatestPublishedContentAndCancelOtherTopics +beginDialog: + kind: OnError + id: main + actions: + - kind: SetVariable + id: setVariable_timestamp + variable: init:Topic.CurrentTime + value: =Text(Now(), DateTimeFormat.UTC) + + - kind: ConditionGroup + id: condition_1 + conditions: + - id: bL4wmY + condition: =System.Conversation.InTestMode = true + actions: + - kind: SendActivity + id: sendMessage_XJBYMo + activity: |- + Error Message: {System.Error.Message} + Error Code: {System.Error.Code} + Conversation Id: {System.Conversation.Id} + Time (UTC): {Topic.CurrentTime} + + elseActions: + - kind: SendActivity + id: sendMessage_dZ0gaF + activity: + text: + - |- + An error has occurred. + Error code: {System.Error.Code} + Conversation Id: {System.Conversation.Id} + Time (UTC): {Topic.CurrentTime}. + speak: + - An error has occurred, please try again. + + - kind: LogCustomTelemetryEvent + id: 9KwEAn + eventName: OnErrorLog + properties: "={ErrorMessage: System.Error.Message, ErrorCode: System.Error.Code, TimeUTC: Topic.CurrentTime, ConversationId: System.Conversation.Id}" + + - kind: CancelAllDialogs + id: NW7NyY \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ResetConversation/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ResetConversation/botcomponent.xml new file mode 100644 index 00000000..da6b2e66 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ResetConversation/botcomponent.xml @@ -0,0 +1,10 @@ + + 9 + 0 + Reset Conversation + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ResetConversation/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ResetConversation/data new file mode 100644 index 00000000..3d427e1d --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ResetConversation/data @@ -0,0 +1,16 @@ +kind: AdaptiveDialog +startBehavior: UseLatestPublishedContentAndCancelOtherTopics +beginDialog: + kind: OnSystemRedirect + id: main + actions: + - kind: SendActivity + id: sendMessage_OPsT1O + activity: What can I help you with? + + - kind: ClearAllVariables + id: clearAllVariables_73bTFR + variables: ConversationScopedVariables + + - kind: CancelAllDialogs + id: cancelAllDialogs_12Gt21 \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Search/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Search/botcomponent.xml new file mode 100644 index 00000000..030ecfa7 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Search/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + Create generative answers from knowledge sources. + 0 + Conversational boosting + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Search/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Search/data new file mode 100644 index 00000000..9081f12f --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Search/data @@ -0,0 +1,20 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnUnknownIntent + id: main + priority: -1 + actions: + - kind: SearchAndSummarizeContent + id: search-content + variable: Topic.Answer + userInput: =System.Activity.Text + + - kind: ConditionGroup + id: has-answer-conditions + conditions: + - id: has-answer + condition: =!IsBlank(Topic.Answer) + actions: + - kind: EndDialog + id: end-topic + clearTopicQueue: true \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Signin/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Signin/botcomponent.xml new file mode 100644 index 00000000..5f930678 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Signin/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This system topic triggers when the agent needs to sign in the user or require the user to sign in + 0 + Sign in + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Signin/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Signin/data new file mode 100644 index 00000000..c4067bc3 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.Signin/data @@ -0,0 +1,19 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnSignIn + id: main + actions: + - kind: ConditionGroup + id: conditionGroup_ypjGKL + conditions: + - id: conditionItem_7XYIIR + condition: =System.SignInReason = SignInReason.SignInRequired + actions: + - kind: SendActivity + id: sendMessage_1jHUNO + activity: Hello! To be able to help you, I'll need you to sign in. + + - kind: OAuthInput + id: gOjhZA + title: Login + text: To continue, please login \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.StartOver/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.StartOver/botcomponent.xml new file mode 100644 index 00000000..799944c2 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.StartOver/botcomponent.xml @@ -0,0 +1,10 @@ + + 9 + 0 + Start Over + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.StartOver/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.StartOver/data new file mode 100644 index 00000000..68f6bf43 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.StartOver/data @@ -0,0 +1,35 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnRecognizedIntent + id: main + intent: + displayName: Start Over + includeInOnSelectIntent: false + triggerQueries: + - let's begin again + - start over + - start again + - restart + + actions: + - kind: Question + id: question_zguoVV + alwaysPrompt: false + variable: init:Topic.Confirm + prompt: Are you sure you want to restart the conversation? + entity: BooleanPrebuiltEntity + + - kind: ConditionGroup + id: conditionGroup_lvx2zV + conditions: + - id: conditionItem_sVQtHa + condition: =Topic.Confirm = true + actions: + - kind: BeginDialog + id: 0YKYsy + dialog: cr26e_Warehouseagent.topic.ResetConversation + + elseActions: + - kind: SendActivity + id: sendMessage_lk2CyQ + activity: Ok. Let's carry on. \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ThankYou/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ThankYou/botcomponent.xml new file mode 100644 index 00000000..cda157c8 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ThankYou/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + This topic triggers when the user says thank you. + 0 + Thank you + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ThankYou/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ThankYou/data new file mode 100644 index 00000000..9b816ed3 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.ThankYou/data @@ -0,0 +1,17 @@ +kind: AdaptiveDialog +beginDialog: + kind: OnRecognizedIntent + id: main + intent: + displayName: Thank you + includeInOnSelectIntent: false + triggerQueries: + - thanks + - thank you + - thanks so much + - ty + + actions: + - kind: SendActivity + id: sendMessage_9iz6v7 + activity: You're welcome. \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.warehouseserver3/botcomponent.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.warehouseserver3/botcomponent.xml new file mode 100644 index 00000000..d907ef6c --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.warehouseserver3/botcomponent.xml @@ -0,0 +1,11 @@ + + 9 + my warehouse server 3 connector + 0 + warehouse server 3 + + cr26e_Warehouseagent + + 0 + 1 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.warehouseserver3/data b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.warehouseserver3/data new file mode 100644 index 00000000..d21075e5 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/botcomponents/cr26e_Warehouseagent.topic.warehouseserver3/data @@ -0,0 +1,12 @@ +kind: TaskDialog +modelDisplayName: warehouse server 3 +modelDescription: my warehouse server 3 connector +action: + kind: InvokeExternalAgentTaskAction + connectionReference: cr26e_Warehouseagent.shared_new-5fwarehouse-20server-203-5fee08b8354fad177a.726ca1c1522e492082b8e68667e2267e + connectionProperties: + mode: Invoker + + operationDetails: + kind: ModelContextProtocolMetadata + operationId: InvokeServer \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_OrdersAgent/bot.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_OrdersAgent/bot.xml new file mode 100644 index 00000000..dc6d1f08 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_OrdersAgent/bot.xml @@ -0,0 +1,11 @@ + + 2 + 1 + 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 + 0 + 1033 + Orders Agent + 0 + + 4 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_OrdersAgent/configuration.json b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_OrdersAgent/configuration.json new file mode 100644 index 00000000..f651a8df --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_OrdersAgent/configuration.json @@ -0,0 +1 @@ +{"categories":[],"channels":[{"id":null,"channelId":"msteams","channelSpecifier":null,"displayName":null},{"id":null,"channelId":"Microsoft365Copilot","channelSpecifier":null,"displayName":null}],"settings":{"GenerativeActionsEnabled":true,"SmartTaskCompletionEnabled":true},"publishOnCreate":false,"publishOnImport":true,"isLightweightBot":false,"$kind":"BotConfiguration","isAgentConnectable":true,"gPTSettings":{"$kind":"GPTSettings","defaultSchemaName":"cr26e_OrdersAgent.gpt.default"},"aISettings":{"$kind":"AISettings","useModelKnowledge":true,"isFileAnalysisEnabled":true,"isSemanticSearchEnabled":true,"contentModeration":"Low","optInUseLatestModels":false},"recognizer":{"$kind":"CLIAgentRecognizer"}} \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_Warehouseagent/bot.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_Warehouseagent/bot.xml new file mode 100644 index 00000000..cb425b52 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_Warehouseagent/bot.xml @@ -0,0 +1,11 @@ + + 2 + 1 + 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 + 0 + 1033 + Warehouse agent + 0 + + 4 + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_Warehouseagent/configuration.json b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_Warehouseagent/configuration.json new file mode 100644 index 00000000..98dcf3eb --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/bots/cr26e_Warehouseagent/configuration.json @@ -0,0 +1,23 @@ +{ + "$kind": "BotConfiguration", + "settings": { + "GenerativeActionsEnabled": true, + "SmartTaskCompletionEnabled": true + }, + "isAgentConnectable": true, + "gPTSettings": { + "$kind": "GPTSettings", + "defaultSchemaName": "cr26e_Warehouseagent.gpt.default" + }, + "aISettings": { + "$kind": "AISettings", + "useModelKnowledge": true, + "isFileAnalysisEnabled": true, + "isSemanticSearchEnabled": true, + "contentModeration": "Low", + "optInUseLatestModels": false + }, + "recognizer": { + "$kind": "CLIAgentRecognizer" + } +} \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/customizations.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/customizations.xml new file mode 100644 index 00000000..2dc0a793 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/customizations.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + e03cf4ce-5b7f-4ba4-b5b7-c90c80bbca98 + my orders mcp server connector + orders mcp + #007ee5 + new_5Forders-20mcp + 1 + /Connector/new_5Forders-20mcp_openapidefinition.json + /Connector/new_5Forders-20mcp_connectionparameters.json + /Connector/new_5Forders-20mcp_policytemplateinstances.json + /Connector/new_5Forders-20mcp_iconblob.Png + + + ff27b2b6-790d-4782-8725-f63dfefa30d3 + my warehouse server 3 connector + warehouse server 3 + + new_5Fwarehouse-20server-203 + 1 + /Connector/new_5Fwarehouse-20server-203_openapidefinition.json + /Connector/new_5Fwarehouse-20server-203_connectionparameters.json + /Connector/new_5Fwarehouse-20server-203_policytemplateinstances.json + /Connector/new_5Fwarehouse-20server-203_iconblob.Png + + + + + cr26e_OrdersAgent.shared_new-5forders-20mcp-5fee08b8354fad177a.6be7a421f5184b9ebb376965cf6cd8b8 + /providers/Microsoft.PowerApps/apis/shared_new-5forders-20mcp-5fee08b8354fad177a + + e03cf4ce-5b7f-4ba4-b5b7-c90c80bbca98 + + 0 + 0 + 0 + 1 + + + cr26e_Warehouseagent.shared_new-5fwarehouse-20server-203-5fee08b8354fad177a.726ca1c1522e492082b8e68667e2267e + /providers/Microsoft.PowerApps/apis/shared_new-5fwarehouse-20server-203-5fee08b8354fad177a + + ff27b2b6-790d-4782-8725-f63dfefa30d3 + + 0 + 0 + 0 + 1 + + + + 1033 + + \ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/solution.xml b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/solution.xml new file mode 100644 index 00000000..b310d965 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/agents/sourcecode/OrderManagementMCPDemo/solution.xml @@ -0,0 +1,87 @@ + + + OrderManagementMCPDemo + + + + + 1.0.0.1 + 0 + + DefaultPublisherorg5d9d4b6b + + + + + + + + + new + 10000 + +
+ 1 + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ 2 + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + +
+
\ No newline at end of file diff --git a/extensibility/mcp/order-management-enhanced-tc/assets/gradio-demo.png b/extensibility/mcp/order-management-enhanced-tc/assets/gradio-demo.png new file mode 100644 index 00000000..3303aa57 Binary files /dev/null and b/extensibility/mcp/order-management-enhanced-tc/assets/gradio-demo.png differ diff --git a/extensibility/mcp/order-management-enhanced-tc/assets/gradio-ui.png b/extensibility/mcp/order-management-enhanced-tc/assets/gradio-ui.png new file mode 100644 index 00000000..9042da81 Binary files /dev/null and b/extensibility/mcp/order-management-enhanced-tc/assets/gradio-ui.png differ diff --git a/extensibility/mcp/order-management-enhanced-tc/chat-ui/.env.sample b/extensibility/mcp/order-management-enhanced-tc/chat-ui/.env.sample new file mode 100644 index 00000000..616c4e0b --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/chat-ui/.env.sample @@ -0,0 +1,4 @@ +COPILOTSTUDIOAGENT__ENVIRONMENTID=your-environment-id +COPILOTSTUDIOAGENT__SCHEMANAME=your-agent-schema-name +COPILOTSTUDIOAGENT__TENANTID=your-tenant-id +COPILOTSTUDIOAGENT__AGENTAPPID=your-app-client-id diff --git a/extensibility/mcp/order-management-enhanced-tc/chat-ui/.gitignore b/extensibility/mcp/order-management-enhanced-tc/chat-ui/.gitignore new file mode 100644 index 00000000..434a9634 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/chat-ui/.gitignore @@ -0,0 +1,5 @@ +.env +.venv/ +__pycache__/ +.token_cache.json +activities.jsonl diff --git a/extensibility/mcp/order-management-enhanced-tc/chat-ui/app.py b/extensibility/mcp/order-management-enhanced-tc/chat-ui/app.py new file mode 100644 index 00000000..68650e80 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/chat-ui/app.py @@ -0,0 +1,709 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +""" +Gradio chat frontend for Copilot Studio agents. +Renders reasoning, tool calls, and messages inline with collapsible accordions. + +Usage: + pip install -r requirements.txt + python app.py +""" + +import asyncio +import json +import os +import re +from pathlib import Path + +import gradio as gr +from dotenv import load_dotenv +from msal import PublicClientApplication, TokenCache + +from microsoft_agents.activity import ActivityTypes +from microsoft_agents.copilotstudio.client import ( + ConnectionSettings, + CopilotClient, +) + +load_dotenv() + +LOG_FILE = Path(__file__).parent / "activities.jsonl" +TOKEN_CACHE_FILE = Path(__file__).parent / ".token_cache.json" + + +# --------------------------------------------------------------------------- +# Persisted MSAL token cache +# --------------------------------------------------------------------------- + +class LocalTokenCache(TokenCache): + def __init__(self, path: str): + super().__init__() + self._path = path + if os.path.exists(self._path): + with self._lock: + with open(self._path, "r") as f: + self._cache = json.load(f) + + def add(self, event, **kwargs): + super().add(event, **kwargs) + self._persist() + + def modify(self, credential_type, old_entry, new_key_value_pairs=None): + super().modify(credential_type, old_entry, new_key_value_pairs) + self._persist() + + def _persist(self): + with self._lock: + with open(self._path, "w") as f: + json.dump(self._cache, f) + + +# --------------------------------------------------------------------------- +# Auth — runs once at startup, cached for subsequent runs +# --------------------------------------------------------------------------- + +_cache = LocalTokenCache(str(TOKEN_CACHE_FILE)) +_pca = PublicClientApplication( + client_id=os.environ["COPILOTSTUDIOAGENT__AGENTAPPID"], + authority=f"https://login.microsoftonline.com/{os.environ['COPILOTSTUDIOAGENT__TENANTID']}", + token_cache=_cache, +) + + +def acquire_token() -> str: + scopes = ["https://api.powerplatform.com/.default"] + accounts = _pca.get_accounts() + if accounts: + result = _pca.acquire_token_silent(scopes, account=accounts[0]) + if result and "access_token" in result: + return result["access_token"] + result = _pca.acquire_token_interactive(scopes=scopes) + if "access_token" in result: + return result["access_token"] + raise RuntimeError(result.get("error_description", "Auth failed")) + + +_token = acquire_token() +print(f"Authenticated. Token cached at {TOKEN_CACHE_FILE}") + + +def create_client() -> CopilotClient: + global _token + # Refresh silently if possible + accounts = _pca.get_accounts() + if accounts: + result = _pca.acquire_token_silent( + ["https://api.powerplatform.com/.default"], account=accounts[0] + ) + if result and "access_token" in result: + _token = result["access_token"] + + settings = ConnectionSettings( + environment_id=os.environ["COPILOTSTUDIOAGENT__ENVIRONMENTID"], + agent_identifier=os.environ["COPILOTSTUDIOAGENT__SCHEMANAME"], + cloud=None, + copilot_agent_type=None, + custom_power_platform_cloud=None, + ) + return CopilotClient(settings, _token) + + +# --------------------------------------------------------------------------- +# Activity parsing helpers +# --------------------------------------------------------------------------- + +def log_activity(raw: dict, direction: str = "recv"): + with open(LOG_FILE, "a") as f: + f.write(json.dumps({"dir": direction, **raw}, default=str) + "\n") + + +def activity_to_dict(activity) -> dict: + d = {"type": activity.type} + for attr in ["text", "value_type", "value", "name", "entities", "channel_data", + "attachments", "attachment_layout", "suggested_actions"]: + val = getattr(activity, attr, None) + if val is not None: + if attr == "entities" and val: + d[attr] = [str(e) for e in val] + elif attr == "attachments" and val: + d[attr] = [ + {"contentType": a.content_type, "contentUrl": (a.content_url or "")[:100], + "name": a.name, "hasContent": a.content is not None} + for a in val + ] + elif attr == "suggested_actions" and val: + d[attr] = [a.title for a in val.actions] if val.actions else [] + else: + d[attr] = val + if hasattr(activity, "conversation") and activity.conversation: + d["conversation_id"] = activity.conversation.id + return d + + +def parse_tool_call(entity_str: str) -> dict | None: + if "type='toolCall'" not in entity_str: + return None + result = {} + for key in ["tool_call_id", "tool_name", "tool_display_name", "status", "duration_ms"]: + m = re.search(rf"{key}='([^']*)'", entity_str) + if m: + result[key] = m.group(1) + m = re.search(r"filled_parameters=\{([^}]*)\}", entity_str) + if m: + try: + result["parameters"] = json.loads("{" + m.group(1).replace("'", '"') + "}") + except json.JSONDecodeError: + result["parameters_raw"] = m.group(1) + m = re.search(r"result='(.+?)'\s*$", entity_str) + if not m: + m = re.search(r"result='(.+?)'(?:,\s*type=)", entity_str) + if m: + try: + parsed = json.loads(m.group(1)) + if isinstance(parsed, dict) and "content" in parsed: + for c in parsed["content"]: + if c.get("type") == "text": + try: + result["result"] = json.loads(c["text"]) + except (json.JSONDecodeError, TypeError): + result["result"] = c["text"] + break + else: + result["result"] = parsed + except json.JSONDecodeError: + result["result_raw"] = m.group(1)[:300] + return result + + +# Fields to strip from tool results (internal IDs, not useful for end users) +_HIDDEN_KEYS = {"conversation_id", "id", "botId", "bot_id", "agent_id", "is_error"} + + +def _format_tool_result(result) -> str: + """Format tool result for display — show data but hide internal IDs.""" + if isinstance(result, dict): + cleaned = {k: v for k, v in result.items() if k not in _HIDDEN_KEYS} + if not cleaned: + return "✅ Done" + return json.dumps(cleaned, indent=2) + elif isinstance(result, list): + cleaned = [] + for item in result: + if isinstance(item, dict): + cleaned.append({k: v for k, v in item.items() if k not in _HIDDEN_KEYS}) + else: + cleaned.append(item) + return json.dumps(cleaned, indent=2) + elif isinstance(result, str): + return result + return "✅ Done" + + +def parse_thought(entity_str: str) -> str | None: + if "type='thought'" not in entity_str: + return None + m = re.search(r"text=['\"](.+?)['\"](?:\s+reasoned_for_seconds|\s*$)", entity_str) + return m.group(1) if m else None + + +# --------------------------------------------------------------------------- +# Conversation state (per-process, single user demo) +# --------------------------------------------------------------------------- + +_client: CopilotClient | None = None +_conversation_id: str | None = None + + +def ensure_client() -> CopilotClient: + global _client + if _client is None: + _client = create_client() + return _client + + +# --------------------------------------------------------------------------- +# Chat handler — yields gr.ChatMessage list progressively +# --------------------------------------------------------------------------- + +_tool_id_counter = 0 + + +import base64 +import mimetypes +import tempfile + +from microsoft_agents.activity import Activity +from microsoft_agents.activity.attachment import Attachment + + +def _build_attachments(files: list) -> list[Attachment]: + """Build Bot Framework Attachments from uploaded files as base64 data URLs.""" + attachments = [] + for f in files: + file_path = f if isinstance(f, str) else f.get("path", f.get("name", "")) + if not file_path: + continue + path = Path(file_path) + content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream" + data = path.read_bytes() + b64 = base64.b64encode(data).decode("ascii") + attachments.append(Attachment( + content_type=content_type, + content_url=f"data:{content_type};base64,{b64}", + name=path.name, + )) + return attachments + + +async def chat_async(user_message, history: list): + global _conversation_id, _tool_id_counter + + # Handle multimodal input (text + files) + if isinstance(user_message, dict): + text = user_message.get("text", "") + files = user_message.get("files", []) + else: + text = str(user_message) + files = [] + + LOG_FILE.write_text("") if not _conversation_id else None + log_activity({"type": "user_message", "text": text[:200]}, "send") + + client = ensure_client() + + # Start conversation on first message + if _conversation_id is None: + async for activity in client.start_conversation(True): + if hasattr(activity, "conversation") and activity.conversation: + _conversation_id = activity.conversation.id + log_activity(activity_to_dict(activity), "start") + + messages = list(history) + + # Build attachments from uploaded files + attachments = _build_attachments(files) if files else [] + has_attachments = len(attachments) > 0 + + # Track tool groups: when we see tool calls between messages, + # group them under one parent + tool_group_id: str | None = None + tool_count = 0 + + # Send with attachments if files were uploaded, otherwise plain text + if has_attachments: + outgoing = Activity( + type="message", + text=text, + attachments=attachments, + conversation={"id": _conversation_id} if _conversation_id else None, + ) + activity_stream = client.ask_question_with_activity(outgoing) + else: + activity_stream = client.ask_question(text, _conversation_id) + + async for activity in activity_stream: + if not _conversation_id and hasattr(activity, "conversation") and activity.conversation: + _conversation_id = activity.conversation.id + + log_activity(activity_to_dict(activity)) + + cd = getattr(activity, "channel_data", None) or {} + stream_type = cd.get("streamType", "") + entities = getattr(activity, "entities", None) or [] + + if activity.type == ActivityTypes.typing: + for e in entities: + e_str = str(e) + + # Reasoning + thought = parse_thought(e_str) + if thought: + messages.append(gr.ChatMessage( + role="assistant", + content=thought, + metadata={"title": "💭 Reasoning", "status": "done"}, + )) + yield messages + + # Tool calls + tc = parse_tool_call(e_str) + if tc: + tool_id = tc.get("tool_call_id", "") + status = tc.get("status", "") + tool_name = tc.get("tool_display_name") or tc.get("tool_name", "tool") + + if status == "started": + # Create a tool group parent if this is the first tool in a batch + if tool_group_id is None: + _tool_id_counter += 1 + tool_group_id = f"tool_group_{_tool_id_counter}" + tool_count = 0 + + tool_count += 1 + params = tc.get("parameters", tc.get("parameters_raw", "")) + # Show clean parameter summary + if isinstance(params, dict): + param_parts = [f"{k}={v}" for k, v in params.items()] + content = ", ".join(param_parts) + else: + content = str(params) if params else "" + + messages.append(gr.ChatMessage( + role="assistant", + content=content, + metadata={ + "title": f"🔧 {tool_name}", + "id": tool_id, + "parent_id": tool_group_id, + "status": "pending", + }, + )) + yield messages + + elif status == "completed": + # Find and update the tool message + for msg in messages: + if (isinstance(msg, gr.ChatMessage) + and msg.metadata + and msg.metadata.get("id") == tool_id): + duration = tc.get("duration_ms", "") + result = tc.get("result", tc.get("result_raw", "")) + msg.content = _format_tool_result(result) + msg.metadata["status"] = "done" + if duration: + msg.metadata["duration"] = float(duration) / 1000 + break + yield messages + + elif activity.type == ActivityTypes.message: + # Check for file attachments + activity_attachments = getattr(activity, "attachments", None) or [] + for att in activity_attachments: + content_url = getattr(att, "content_url", "") or "" + att_name = getattr(att, "name", "file") or "file" + if content_url.startswith("data:"): + # Decode base64 data URL and save as temp file + try: + header, b64data = content_url.split(",", 1) + file_bytes = base64.b64decode(b64data) + temp_dir = tempfile.gettempdir() + temp_path = Path(temp_dir) / att_name + temp_path.write_bytes(file_bytes) + messages.append(gr.ChatMessage( + role="assistant", + content=gr.FileData(path=str(temp_path), mime_type=getattr(att, "content_type", "")), + )) + yield messages + except Exception as e: + messages.append(gr.ChatMessage( + role="assistant", + content=f"📎 {att_name} (download failed: {e})", + )) + yield messages + + if stream_type == "final" and activity.text: + # Close the current tool group + tool_group_id = None + tool_count = 0 + + messages.append(gr.ChatMessage( + role="assistant", + content=activity.text, + )) + yield messages + + elif activity.type == ActivityTypes.end_of_conversation: + break + + +def chat(user_message: str, history: list): + """Sync wrapper for the async chat handler.""" + loop = asyncio.new_event_loop() + gen = chat_async(user_message, history) + + try: + while True: + result = loop.run_until_complete(gen.__anext__()) + yield result + except StopAsyncIteration: + pass + finally: + loop.close() + + +# --------------------------------------------------------------------------- +# Theme & CSS +# --------------------------------------------------------------------------- + +theme = gr.themes.Base( + primary_hue=gr.themes.Color( + c50="#f0f9f6", c100="#d5f0e8", c200="#a8e0cf", c300="#6ec9b0", + c400="#3bab8e", c500="#1e8c6e", c600="#187058", c700="#145845", + c800="#104437", c900="#0c332a", c950="#06201a", + ), + secondary_hue=gr.themes.Color( + c50="#fef7ee", c100="#fdedd3", c200="#f9d7a5", c300="#f4bb6d", + c400="#ef9a33", c500="#e8801b", c600="#cf6612", c700="#ab4e12", + c800="#893f16", c900="#713615", c950="#3d1a09", + ), + neutral_hue=gr.themes.Color( + c50="#f8f9fa", c100="#f1f3f5", c200="#e5e7eb", c300="#d1d5db", + c400="#9ca3af", c500="#6b7280", c600="#4b5563", c700="#374151", + c800="#1f2937", c900="#111827", c950="#030712", + ), + font=[gr.themes.GoogleFont("Plus Jakarta Sans"), "system-ui", "sans-serif"], + font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace"], + radius_size=gr.themes.sizes.radius_lg, + spacing_size=gr.themes.sizes.spacing_md, +).set( + # Overall page + body_background_fill="#f8f9fa", + body_background_fill_dark="#111827", + + # Blocks + block_background_fill="white", + block_background_fill_dark="#1f2937", + block_border_width="0px", + block_shadow="0 1px 3px 0 rgba(0,0,0,0.06), 0 1px 2px -1px rgba(0,0,0,0.04)", + block_shadow_dark="0 1px 3px 0 rgba(0,0,0,0.4)", + + # Buttons + button_primary_background_fill="*primary_500", + button_primary_background_fill_hover="*primary_600", + button_primary_text_color="white", + button_primary_shadow="0 1px 2px 0 rgba(30,140,110,0.2)", + button_secondary_background_fill="white", + button_secondary_background_fill_hover="*neutral_50", + button_secondary_border_color="*neutral_200", + button_secondary_text_color="*neutral_700", + + # Inputs + input_background_fill="white", + input_background_fill_dark="#1f2937", + input_border_color="*neutral_200", + input_border_color_dark="*neutral_700", + input_border_color_focus="*primary_400", + input_shadow="none", + input_shadow_focus="0 0 0 3px rgba(30,140,110,0.1)", + + # Labels & text + block_label_text_color="*neutral_500", + block_title_text_color="*neutral_800", + block_title_text_color_dark="*neutral_200", +) + +custom_css = """ +/* ── Page background with subtle grid ── */ +.gradio-container { + background: + linear-gradient(rgba(248,249,250,0.97), rgba(248,249,250,0.97)), + linear-gradient(90deg, #e5e7eb 1px, transparent 1px), + linear-gradient(#e5e7eb 1px, transparent 1px) !important; + background-size: 100% 100%, 48px 48px, 48px 48px !important; +} + +/* ── Chat area ── */ +.chatbot { + border: none !important; + box-shadow: none !important; + background: transparent !important; +} + +/* ── User messages ── */ +.message-row.user-row .message-bubble { + background: #111827 !important; + color: #f1f3f5 !important; + border-radius: 20px 20px 4px 20px !important; + box-shadow: 0 2px 12px rgba(17, 24, 39, 0.12) !important; + font-size: 0.92em !important; + line-height: 1.6 !important; +} + +/* ── Bot messages ── */ +.message-row.bot-row .message-bubble { + background: white !important; + border: 1px solid #e5e7eb !important; + border-radius: 20px 20px 20px 4px !important; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04) !important; + line-height: 1.65 !important; +} + +/* ── Tool call accordions ── */ +.message-row .accordion { + border: 1px solid #e5e7eb !important; + border-left: 3px solid #3bab8e !important; + border-radius: 2px 10px 10px 2px !important; + background: #f8f9fa !important; + overflow: hidden; + transition: border-color 0.2s ease !important; +} + +.message-row .accordion:hover { + border-left-color: #1e8c6e !important; +} + +.message-row .accordion .label-wrap { + padding: 10px 14px !important; + font-size: 0.88em !important; + font-weight: 600 !important; + letter-spacing: 0.01em !important; +} + +/* ── Code blocks in tool results ── */ +.message-row pre { + border-radius: 8px !important; + font-size: 0.8em !important; + border: 1px solid #e5e7eb !important; + background: #f8f9fa !important; +} + +/* ── Header area ── */ +.header-bar { + background: linear-gradient(135deg, #111827 0%, #1f2937 50%, #145845 100%) !important; + border-radius: 16px !important; + padding: 28px 32px !important; + margin-bottom: 8px !important; + border: 1px solid rgba(255,255,255,0.06) !important; + box-shadow: 0 4px 24px rgba(17, 24, 39, 0.12), 0 1px 3px rgba(17, 24, 39, 0.08) !important; +} + +.header-bar h1 { + font-size: 1.5em !important; + font-weight: 700 !important; + letter-spacing: -0.03em !important; + color: #f1f3f5 !important; + margin: 0 0 6px 0 !important; + line-height: 1.2 !important; +} + +.header-bar p { + color: #9ca3af !important; + font-size: 0.9em !important; + margin: 0 !important; + line-height: 1.5 !important; + font-weight: 400 !important; +} + +.header-bar .badge { + display: inline-block; + background: rgba(62, 171, 142, 0.15); + color: #6ec9b0; + font-size: 0.72em; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 3px 10px; + border-radius: 20px; + border: 1px solid rgba(62, 171, 142, 0.2); + margin-bottom: 10px; +} + +/* ── Example buttons ── */ +.example-btn { + border-radius: 12px !important; + font-size: 0.84em !important; + padding: 10px 16px !important; + border: 1px solid #e5e7eb !important; + background: white !important; + transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) !important; + line-height: 1.5 !important; + box-shadow: 0 1px 2px rgba(0,0,0,0.03) !important; +} + +.example-btn:hover { + border-color: #3bab8e !important; + color: #1e8c6e !important; + background: #f0f9f6 !important; + box-shadow: 0 2px 8px rgba(30,140,110,0.08) !important; + transform: translateY(-1px) !important; +} + +/* ── Input area ── */ +.textbox textarea { + border-radius: 14px !important; + font-size: 0.92em !important; + padding: 12px 16px !important; +} + +/* ── Markdown rendering in bot messages ── */ +.message-row.bot-row .message-bubble h3 { + font-size: 1em !important; + font-weight: 700 !important; + margin-top: 16px !important; + margin-bottom: 6px !important; + letter-spacing: -0.01em !important; +} + +.message-row.bot-row .message-bubble ul, +.message-row.bot-row .message-bubble ol { + padding-left: 1.2em !important; + margin: 6px 0 !important; +} + +.message-row.bot-row .message-bubble li { + margin: 4px 0 !important; + line-height: 1.55 !important; +} + +.message-row.bot-row .message-bubble blockquote { + border-left: 3px solid #d1d5db !important; + margin: 10px 0 !important; + padding: 6px 14px !important; + background: #f8f9fa !important; + border-radius: 0 8px 8px 0 !important; + font-size: 0.92em !important; +} + +.message-row.bot-row .message-bubble hr { + border-color: #e5e7eb !important; + margin: 14px 0 !important; +} + +/* ── Scrollbar ── */ +::-webkit-scrollbar { + width: 5px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: #9ca3af; +} + +/* ── Footer ── */ +footer { + opacity: 0.4; + font-size: 0.85em !important; +} +""" + +# --------------------------------------------------------------------------- +# Gradio UI +# --------------------------------------------------------------------------- + +with gr.Blocks(title="Copilot Studio Agent Chat") as demo: + gr.HTML(""" +
+
Enhanced Task Completion
+

Copilot Studio Agent Chat

+

Agents with Enhanced Task Completion reason dynamically, chain tools across MCP servers, and process files — all visible inline below.

+
+ """) + gr.ChatInterface( + fn=chat, + multimodal=True, + examples=[ + "Hi, I'm Sarah Mitchell. I ordered some Sony headphones recently but they arrived with a crackling sound in the left ear. I'd like to return them. Also, can you check where my other order is — the Kindle I ordered last week?", + "I'm Emily Chen. I returned a damaged Blu-ray disc a couple weeks ago — can you check if my refund has been processed yet, and also tell me where my ergonomic chair delivery is right now?", + "I'm James Rivera. I have two pending orders — can you give me a full status update on both? I want to know exactly where each one is in the process, when they'll ship, and if anything is out of stock, what alternatives do I have?", + "I've uploaded a CSV with order IDs. For each order, fill in all the empty columns and return the completed CSV.", + ], + ) + +if __name__ == "__main__": + demo.launch(theme=theme, css=custom_css) diff --git a/extensibility/mcp/order-management-enhanced-tc/chat-ui/data/demo-orders.csv b/extensibility/mcp/order-management-enhanced-tc/chat-ui/data/demo-orders.csv new file mode 100644 index 00000000..9026e3b1 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/chat-ui/data/demo-orders.csv @@ -0,0 +1,7 @@ +order_id,customer,status,shipment_carrier,tracking_number,estimated_delivery,warehouse_stage,in_stock,restock_date +ORD-10421,Sarah Mitchell,,,,,,, +ORD-10422,Sarah Mitchell,,,,,,, +ORD-10455,James Rivera,,,,,,, +ORD-10460,James Rivera,,,,,,, +ORD-10470,Emily Chen,,,,,,, +ORD-10389,Emily Chen,,,,,,, diff --git a/extensibility/mcp/order-management-enhanced-tc/chat-ui/requirements.txt b/extensibility/mcp/order-management-enhanced-tc/chat-ui/requirements.txt new file mode 100644 index 00000000..ebaff1d4 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/chat-ui/requirements.txt @@ -0,0 +1,6 @@ +gradio>=5.0.0 +microsoft-agents-copilotstudio-client>=0.0.2 +microsoft-agents-activity>=0.0.2 +aiohttp>=3.9.0 +msal>=1.28.0 +python-dotenv>=1.0.0 diff --git a/extensibility/mcp/order-management-enhanced-tc/connectors/order-management/apiDefinition.swagger.json b/extensibility/mcp/order-management-enhanced-tc/connectors/order-management/apiDefinition.swagger.json new file mode 100644 index 00000000..199f6ffc --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/connectors/order-management/apiDefinition.swagger.json @@ -0,0 +1,28 @@ +{ + "swagger": "2.0", + "info": { + "title": "Order Management MCP", + "description": "MCP server for e-commerce order management. Tools: search_orders, get_order, get_shipment, request_return, get_return_status.", + "version": "1.0.0" + }, + "host": "TUNNEL_HOST_PLACEHOLDER", + "basePath": "/", + "schemes": ["https"], + "consumes": ["application/json"], + "produces": ["application/json"], + "paths": { + "/mcp": { + "post": { + "operationId": "InvokeMCP", + "summary": "Invoke Order Management MCP Server", + "description": "Interact with the Order Management MCP server via Streamable HTTP.", + "x-ms-agentic-protocol": "mcp-streamable-1.0", + "parameters": [], + "responses": { + "200": { "description": "Success" } + } + } + } + }, + "definitions": {} +} diff --git a/extensibility/mcp/order-management-enhanced-tc/connectors/order-management/apiProperties.json b/extensibility/mcp/order-management-enhanced-tc/connectors/order-management/apiProperties.json new file mode 100644 index 00000000..09c18914 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/connectors/order-management/apiProperties.json @@ -0,0 +1,5 @@ +{ + "properties": { + "connectionParameters": {} + } +} diff --git a/extensibility/mcp/order-management-enhanced-tc/connectors/warehouse/apiDefinition.swagger.json b/extensibility/mcp/order-management-enhanced-tc/connectors/warehouse/apiDefinition.swagger.json new file mode 100644 index 00000000..59ad3672 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/connectors/warehouse/apiDefinition.swagger.json @@ -0,0 +1,28 @@ +{ + "swagger": "2.0", + "info": { + "title": "Warehouse Fulfillment MCP", + "description": "MCP server for warehouse inventory and fulfillment. Tools: check_stock, get_fulfillment_status, find_alternatives, get_restock_date.", + "version": "1.0.0" + }, + "host": "TUNNEL_HOST_PLACEHOLDER", + "basePath": "/", + "schemes": ["https"], + "consumes": ["application/json"], + "produces": ["application/json"], + "paths": { + "/mcp": { + "post": { + "operationId": "InvokeMCP", + "summary": "Invoke Warehouse MCP Server", + "description": "Interact with the Warehouse Fulfillment MCP server via Streamable HTTP.", + "x-ms-agentic-protocol": "mcp-streamable-1.0", + "parameters": [], + "responses": { + "200": { "description": "Success" } + } + } + } + }, + "definitions": {} +} diff --git a/extensibility/mcp/order-management-enhanced-tc/connectors/warehouse/apiProperties.json b/extensibility/mcp/order-management-enhanced-tc/connectors/warehouse/apiProperties.json new file mode 100644 index 00000000..09c18914 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/connectors/warehouse/apiProperties.json @@ -0,0 +1,5 @@ +{ + "properties": { + "connectionParameters": {} + } +} diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/.gitignore b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/.gitignore new file mode 100644 index 00000000..b9470778 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/package.json b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/package.json new file mode 100644 index 00000000..c0688df2 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/package.json @@ -0,0 +1,22 @@ +{ + "name": "order-management-mcp", + "version": "1.0.0", + "type": "module", + "main": "dist/app.js", + "scripts": { + "build": "tsc", + "start": "node dist/start.js", + "dev": "tsx src/start.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "express": "^4.21.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/data/orders.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/data/orders.ts new file mode 100644 index 00000000..519e20e0 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/data/orders.ts @@ -0,0 +1,106 @@ +import { Order } from "../types.js"; + +export const orders: Order[] = [ + { + id: "ORD-10421", + customer: { id: "C-001", name: "Sarah Mitchell", email: "sarah.mitchell@example.com" }, + date: "2026-04-10", + status: "shipped", + items: [ + { sku: "SKU-WH1000", name: "Sony WH-1000XM5 Headphones", quantity: 1, unitPrice: 349.99 }, + { sku: "SKU-USBC3", name: "USB-C Charging Cable (3-pack)", quantity: 1, unitPrice: 14.99 }, + ], + shippingAddress: "742 Evergreen Terrace, Springfield, IL 62704", + paymentMethod: "Visa ending in 4242", + total: 364.98, + }, + { + id: "ORD-10422", + customer: { id: "C-001", name: "Sarah Mitchell", email: "sarah.mitchell@example.com" }, + date: "2026-04-15", + status: "processing", + items: [ + { sku: "SKU-KINDLE", name: "Kindle Paperwhite (16GB)", quantity: 1, unitPrice: 149.99 }, + ], + shippingAddress: "742 Evergreen Terrace, Springfield, IL 62704", + paymentMethod: "Visa ending in 4242", + total: 149.99, + }, + { + id: "ORD-10318", + customer: { id: "C-001", name: "Sarah Mitchell", email: "sarah.mitchell@example.com" }, + date: "2026-03-22", + status: "delivered", + items: [ + { sku: "SKU-AIRPOD", name: "AirPods Pro (2nd Gen)", quantity: 1, unitPrice: 249.00 }, + { sku: "SKU-CASE01", name: "Silicone AirPods Case - Midnight", quantity: 1, unitPrice: 19.99 }, + ], + shippingAddress: "742 Evergreen Terrace, Springfield, IL 62704", + paymentMethod: "Visa ending in 4242", + total: 268.99, + }, + { + id: "ORD-10455", + customer: { id: "C-002", name: "James Rivera", email: "j.rivera@example.com" }, + date: "2026-04-12", + status: "shipped", + items: [ + { sku: "SKU-HOODIE", name: "Nike Tech Fleece Hoodie - Black (L)", quantity: 1, unitPrice: 120.00 }, + { sku: "SKU-JOGGER", name: "Nike Sportswear Joggers - Grey (L)", quantity: 1, unitPrice: 85.00 }, + ], + shippingAddress: "88 Sunset Blvd, Apt 4B, Los Angeles, CA 90028", + paymentMethod: "Mastercard ending in 8811", + total: 205.00, + }, + { + id: "ORD-10460", + customer: { id: "C-002", name: "James Rivera", email: "j.rivera@example.com" }, + date: "2026-04-18", + status: "processing", + items: [ + { sku: "SKU-SWITCH", name: "Nintendo Switch OLED", quantity: 1, unitPrice: 349.99 }, + { sku: "SKU-ZELDA", name: "The Legend of Zelda: Tears of the Kingdom", quantity: 1, unitPrice: 59.99 }, + ], + shippingAddress: "88 Sunset Blvd, Apt 4B, Los Angeles, CA 90028", + paymentMethod: "Mastercard ending in 8811", + total: 409.98, + }, + { + id: "ORD-10389", + customer: { id: "C-003", name: "Emily Chen", email: "emily.chen@example.com" }, + date: "2026-03-28", + status: "delivered", + items: [ + { sku: "SKU-DUNE2", name: "Dune: Part Two (4K Blu-ray)", quantity: 1, unitPrice: 29.99 }, + { sku: "SKU-FOUNDATION", name: "Foundation (Isaac Asimov) - Paperback", quantity: 1, unitPrice: 12.99 }, + { sku: "SKU-3BODY", name: "The Three-Body Problem - Paperback", quantity: 1, unitPrice: 14.99 }, + ], + shippingAddress: "1200 Main Street, Unit 7C, Seattle, WA 98101", + paymentMethod: "Amex ending in 3003", + total: 57.97, + }, + { + id: "ORD-10470", + customer: { id: "C-003", name: "Emily Chen", email: "emily.chen@example.com" }, + date: "2026-04-14", + status: "shipped", + items: [ + { sku: "SKU-ERGOCHAIR", name: "ErgoChair Pro - Matte Black", quantity: 1, unitPrice: 499.00 }, + ], + shippingAddress: "1200 Main Street, Unit 7C, Seattle, WA 98101", + paymentMethod: "Amex ending in 3003", + total: 499.00, + }, + { + id: "ORD-10401", + customer: { id: "C-002", name: "James Rivera", email: "j.rivera@example.com" }, + date: "2026-04-01", + status: "cancelled", + items: [ + { sku: "SKU-IPAD", name: "iPad Air (M2, 256GB)", quantity: 1, unitPrice: 699.00 }, + ], + shippingAddress: "88 Sunset Blvd, Apt 4B, Los Angeles, CA 90028", + paymentMethod: "Mastercard ending in 8811", + total: 699.00, + }, +]; diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/data/returns.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/data/returns.ts new file mode 100644 index 00000000..9cfb769f --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/data/returns.ts @@ -0,0 +1,65 @@ +import { ReturnRequest } from "../types.js"; + +export const returns: ReturnRequest[] = [ + { + id: "RA-50012", + orderId: "ORD-10318", + itemSkus: ["SKU-CASE01"], + reason: "Wrong color received", + status: "refund_processed", + createdAt: "2026-03-30", + refundAmount: 19.99, + returnLabelUrl: "https://returns.example.com/labels/RA-50012.pdf", + timeline: [ + { date: "2026-03-30", event: "Return requested" }, + { date: "2026-03-31", event: "Return label sent" }, + { date: "2026-04-03", event: "Item shipped by customer" }, + { date: "2026-04-07", event: "Item received at warehouse" }, + { date: "2026-04-09", event: "Refund of $19.99 processed to Visa ending in 4242" }, + ], + }, + { + id: "RA-50018", + orderId: "ORD-10389", + itemSkus: ["SKU-DUNE2"], + reason: "Disc scratched on arrival", + status: "received", + createdAt: "2026-04-03", + refundAmount: 29.99, + returnLabelUrl: "https://returns.example.com/labels/RA-50018.pdf", + timeline: [ + { date: "2026-04-03", event: "Return requested" }, + { date: "2026-04-04", event: "Return label sent" }, + { date: "2026-04-08", event: "Item shipped by customer" }, + { date: "2026-04-14", event: "Item received at warehouse — inspection pending" }, + ], + }, +]; + +let nextReturnId = 50019; + +export function createReturn( + orderId: string, + itemSkus: string[], + reason: string, + refundAmount: number, + paymentMethod: string, +): ReturnRequest { + const id = `RA-${nextReturnId++}`; + const today = new Date().toISOString().slice(0, 10); + const ret: ReturnRequest = { + id, + orderId, + itemSkus, + reason, + status: "requested", + createdAt: today, + refundAmount, + returnLabelUrl: `https://returns.example.com/labels/${id}.pdf`, + timeline: [ + { date: today, event: "Return requested" }, + ], + }; + returns.push(ret); + return ret; +} diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/data/shipments.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/data/shipments.ts new file mode 100644 index 00000000..c8e0d141 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/data/shipments.ts @@ -0,0 +1,70 @@ +import { Shipment } from "../types.js"; + +export const shipments: Shipment[] = [ + { + orderId: "ORD-10421", + carrier: "FedEx", + trackingNumber: "FX-794644790132", + trackingUrl: "https://www.fedex.com/fedextrack/?trknbr=FX-794644790132", + estimatedDelivery: "2026-04-22", + events: [ + { timestamp: "2026-04-17T14:30:00Z", location: "Springfield, IL", status: "Out for delivery" }, + { timestamp: "2026-04-16T08:15:00Z", location: "Chicago, IL", status: "In transit - arrived at local facility" }, + { timestamp: "2026-04-15T06:00:00Z", location: "Indianapolis, IN", status: "In transit" }, + { timestamp: "2026-04-14T18:45:00Z", location: "Memphis, TN", status: "Departed FedEx hub" }, + { timestamp: "2026-04-13T10:00:00Z", location: "Memphis, TN", status: "Picked up" }, + ], + }, + { + orderId: "ORD-10455", + carrier: "UPS", + trackingNumber: "1Z999AA10123456784", + trackingUrl: "https://www.ups.com/track?tracknum=1Z999AA10123456784", + estimatedDelivery: "2026-04-21", + events: [ + { timestamp: "2026-04-16T11:20:00Z", location: "Phoenix, AZ", status: "In transit" }, + { timestamp: "2026-04-15T09:00:00Z", location: "Dallas, TX", status: "Departed facility" }, + { timestamp: "2026-04-14T16:30:00Z", location: "Dallas, TX", status: "Picked up" }, + ], + }, + { + orderId: "ORD-10318", + carrier: "USPS", + trackingNumber: "9400111899223100287654", + trackingUrl: "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223100287654", + estimatedDelivery: "2026-03-28", + events: [ + { timestamp: "2026-03-28T10:15:00Z", location: "Springfield, IL", status: "Delivered - left at front door" }, + { timestamp: "2026-03-28T07:00:00Z", location: "Springfield, IL", status: "Out for delivery" }, + { timestamp: "2026-03-27T14:00:00Z", location: "Springfield, IL", status: "Arrived at local post office" }, + { timestamp: "2026-03-26T08:00:00Z", location: "St. Louis, MO", status: "In transit to destination" }, + { timestamp: "2026-03-25T12:00:00Z", location: "Memphis, TN", status: "Accepted at USPS facility" }, + ], + }, + { + orderId: "ORD-10389", + carrier: "USPS", + trackingNumber: "9400111899223100299876", + trackingUrl: "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223100299876", + estimatedDelivery: "2026-04-02", + events: [ + { timestamp: "2026-04-01T11:30:00Z", location: "Seattle, WA", status: "Delivered - handed to resident" }, + { timestamp: "2026-04-01T07:45:00Z", location: "Seattle, WA", status: "Out for delivery" }, + { timestamp: "2026-03-31T16:00:00Z", location: "Seattle, WA", status: "Arrived at local post office" }, + { timestamp: "2026-03-30T09:00:00Z", location: "Portland, OR", status: "In transit" }, + { timestamp: "2026-03-29T14:00:00Z", location: "San Francisco, CA", status: "Accepted at USPS facility" }, + ], + }, + { + orderId: "ORD-10470", + carrier: "FedEx Freight", + trackingNumber: "FXF-330198745621", + trackingUrl: "https://www.fedex.com/fedextrack/?trknbr=FXF-330198745621", + estimatedDelivery: "2026-04-23", + events: [ + { timestamp: "2026-04-18T10:00:00Z", location: "Portland, OR", status: "In transit - scheduled delivery appointment" }, + { timestamp: "2026-04-17T06:30:00Z", location: "Reno, NV", status: "In transit" }, + { timestamp: "2026-04-16T14:00:00Z", location: "Los Angeles, CA", status: "Picked up from warehouse" }, + ], + }, +]; diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/server.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/server.ts new file mode 100644 index 00000000..8bdf9a35 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/server.ts @@ -0,0 +1,339 @@ +import express, { Request, Response } from "express"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { z } from "zod"; +import { orders } from "./data/orders.js"; +import { shipments } from "./data/shipments.js"; +import { returns, createReturn } from "./data/returns.js"; + +// --------------------------------------------------------------------------- +// MCP Server — 5 interdependent tools for order management +// --------------------------------------------------------------------------- + +function createMcpServer(): McpServer { + const server = new McpServer({ + name: "order-management", + version: "1.0.0", + }); + + // 1. search_orders — entry point: find orders by customer name, email, or order number + server.tool( + "search_orders", + "Search for orders by customer name, email address, or order number. " + + "Returns a summary list. Use the order_id from results with get_order for full details.", + { + query: z.string().describe( + "Search term: customer name (e.g. 'Sarah'), email (e.g. 'sarah.mitchell@example.com'), " + + "or order number (e.g. 'ORD-10421')" + ), + }, + async ({ query }) => { + const q = query.toLowerCase(); + const matches = orders.filter( + (o) => + o.id.toLowerCase().includes(q) || + o.customer.name.toLowerCase().includes(q) || + o.customer.email.toLowerCase().includes(q) + ); + + if (matches.length === 0) { + return { + content: [{ type: "text", text: `No orders found matching "${query}".` }], + }; + } + + const summaries = matches.map((o) => ({ + order_id: o.id, + date: o.date, + status: o.status, + total: `$${o.total.toFixed(2)}`, + customer: o.customer.name, + item_count: o.items.length, + })); + + return { + content: [{ + type: "text", + text: JSON.stringify(summaries, null, 2), + }], + }; + } + ); + + // 2. get_order — drill into full order details + server.tool( + "get_order", + "Get full details for a specific order including line items, shipping address, and payment info. " + + "Use an order_id returned by search_orders.", + { + order_id: z.string().describe("Order ID (e.g. 'ORD-10421') from search_orders results"), + }, + async ({ order_id }) => { + const order = orders.find((o) => o.id === order_id); + if (!order) { + return { + content: [{ type: "text", text: `Order "${order_id}" not found.` }], + isError: true, + }; + } + + const detail = { + order_id: order.id, + date: order.date, + status: order.status, + customer: order.customer, + items: order.items.map((i) => ({ + sku: i.sku, + name: i.name, + quantity: i.quantity, + unit_price: `$${i.unitPrice.toFixed(2)}`, + subtotal: `$${(i.unitPrice * i.quantity).toFixed(2)}`, + })), + shipping_address: order.shippingAddress, + payment_method: order.paymentMethod, + total: `$${order.total.toFixed(2)}`, + }; + + return { + content: [{ type: "text", text: JSON.stringify(detail, null, 2) }], + }; + } + ); + + // 3. get_shipment — tracking info for shipped/delivered orders + server.tool( + "get_shipment", + "Get shipment tracking details for an order. Only works for orders with status 'shipped' or 'delivered'. " + + "Use an order_id from get_order. Returns carrier, tracking number, URL, and event timeline.", + { + order_id: z.string().describe("Order ID (e.g. 'ORD-10421') — must be a shipped or delivered order"), + }, + async ({ order_id }) => { + const order = orders.find((o) => o.id === order_id); + if (!order) { + return { + content: [{ type: "text", text: `Order "${order_id}" not found.` }], + isError: true, + }; + } + + if (order.status !== "shipped" && order.status !== "delivered") { + return { + content: [{ + type: "text", + text: `Order "${order_id}" has status "${order.status}" — shipment tracking is only available for shipped or delivered orders.`, + }], + isError: true, + }; + } + + const shipment = shipments.find((s) => s.orderId === order_id); + if (!shipment) { + return { + content: [{ type: "text", text: `No shipment record found for order "${order_id}".` }], + isError: true, + }; + } + + return { + content: [{ + type: "text", + text: JSON.stringify({ + order_id: shipment.orderId, + carrier: shipment.carrier, + tracking_number: shipment.trackingNumber, + tracking_url: shipment.trackingUrl, + estimated_delivery: shipment.estimatedDelivery, + latest_status: shipment.events[0]?.status ?? "Unknown", + events: shipment.events, + }, null, 2), + }], + }; + } + ); + + // 4. request_return — initiate a return for specific items + server.tool( + "request_return", + "Initiate a return for specific items in an order. Requires order_id from get_order and the SKU(s) " + + "of items to return (found in get_order results). Returns a return authorization with label and instructions.", + { + order_id: z.string().describe("Order ID (e.g. 'ORD-10421')"), + item_skus: z.array(z.string()).describe( + "Array of item SKUs to return (e.g. ['SKU-WH1000']). Get SKUs from get_order results." + ), + reason: z.string().describe("Reason for return (e.g. 'defective', 'wrong size', 'changed mind')"), + }, + async ({ order_id, item_skus, reason }) => { + const order = orders.find((o) => o.id === order_id); + if (!order) { + return { + content: [{ type: "text", text: `Order "${order_id}" not found.` }], + isError: true, + }; + } + + if (order.status === "cancelled") { + return { + content: [{ type: "text", text: `Order "${order_id}" is cancelled and cannot be returned.` }], + isError: true, + }; + } + + if (order.status === "processing") { + return { + content: [{ + type: "text", + text: `Order "${order_id}" is still processing. Please wait until it ships or cancel the order instead.`, + }], + isError: true, + }; + } + + const matchedItems = order.items.filter((i) => item_skus.includes(i.sku)); + const unknownSkus = item_skus.filter((sku) => !order.items.some((i) => i.sku === sku)); + + if (matchedItems.length === 0) { + return { + content: [{ + type: "text", + text: `None of the provided SKUs (${item_skus.join(", ")}) match items in order "${order_id}". ` + + `Available SKUs: ${order.items.map((i) => i.sku).join(", ")}`, + }], + isError: true, + }; + } + + const refundAmount = matchedItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0); + const ret = createReturn(order_id, item_skus, reason, refundAmount, order.paymentMethod); + + const response: Record = { + return_id: ret.id, + order_id: ret.orderId, + status: ret.status, + items_returned: matchedItems.map((i) => ({ sku: i.sku, name: i.name })), + reason: ret.reason, + estimated_refund: `$${ret.refundAmount.toFixed(2)}`, + refund_to: order.paymentMethod, + return_label_url: ret.returnLabelUrl, + instructions: "Print the return label and attach it to the package. Drop off at any carrier location within 14 days.", + }; + + if (unknownSkus.length > 0) { + response.warnings = [`These SKUs were not found in the order and were skipped: ${unknownSkus.join(", ")}`]; + } + + return { + content: [{ type: "text", text: JSON.stringify(response, null, 2) }], + }; + } + ); + + // 5. get_return_status — check status of a return request + server.tool( + "get_return_status", + "Check the status of a return request. Use the return_id (RA number) from request_return results " + + "or ask the customer for their RA number. Returns status, timeline, and refund details.", + { + return_id: z.string().describe("Return authorization ID (e.g. 'RA-50012') from request_return results"), + }, + async ({ return_id }) => { + const ret = returns.find((r) => r.id === return_id); + if (!ret) { + return { + content: [{ type: "text", text: `Return "${return_id}" not found.` }], + isError: true, + }; + } + + return { + content: [{ + type: "text", + text: JSON.stringify({ + return_id: ret.id, + order_id: ret.orderId, + status: ret.status, + items: ret.itemSkus, + reason: ret.reason, + refund_amount: `$${ret.refundAmount.toFixed(2)}`, + return_label_url: ret.returnLabelUrl, + created: ret.createdAt, + timeline: ret.timeline, + }, null, 2), + }], + }; + } + ); + + return server; +} + +// --------------------------------------------------------------------------- +// Express app — stateless Streamable HTTP transport +// --------------------------------------------------------------------------- + +export function createServer(port: number): void { + const app = express(); + + app.use((_req, res, next) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, DELETE"); + res.setHeader("Access-Control-Allow-Headers", "*"); + if (_req.method === "OPTIONS") { + res.sendStatus(204); + return; + } + next(); + }); + + app.use(express.json()); + + // POST /mcp — stateless: new server + transport per request + app.post("/mcp", async (req: Request, res: Response) => { + const server = createMcpServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + + await server.connect(transport); + + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("Error handling MCP request:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); + } + } + }); + + // GET & DELETE /mcp — 405 + app.get("/mcp", (_req: Request, res: Response) => { + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { code: -32000, message: "Method not allowed." }, + id: null, + })); + }); + + app.delete("/mcp", (_req: Request, res: Response) => { + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { code: -32000, message: "Method not allowed." }, + id: null, + })); + }); + + app.get("/health", (_req: Request, res: Response) => { + res.json({ status: "ok", name: "order-management-mcp", version: "1.0.0" }); + }); + + app.listen(port, () => { + console.log(`Order Management MCP Server running on http://localhost:${port}/mcp`); + console.log(`Health check: http://localhost:${port}/health`); + }); +} diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/start.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/start.ts new file mode 100644 index 00000000..2f9b621c --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/start.ts @@ -0,0 +1,4 @@ +import { createServer } from "./server.js"; + +const PORT = parseInt(process.env.PORT || "3000"); +createServer(PORT); diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/types.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/types.ts new file mode 100644 index 00000000..1619eb42 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/src/types.ts @@ -0,0 +1,50 @@ +export interface Customer { + id: string; + name: string; + email: string; +} + +export interface LineItem { + sku: string; + name: string; + quantity: number; + unitPrice: number; +} + +export interface Order { + id: string; + customer: Customer; + date: string; + status: "processing" | "shipped" | "delivered" | "cancelled"; + items: LineItem[]; + shippingAddress: string; + paymentMethod: string; + total: number; +} + +export interface TrackingEvent { + timestamp: string; + location: string; + status: string; +} + +export interface Shipment { + orderId: string; + carrier: string; + trackingNumber: string; + trackingUrl: string; + estimatedDelivery: string; + events: TrackingEvent[]; +} + +export interface ReturnRequest { + id: string; + orderId: string; + itemSkus: string[]; + reason: string; + status: "requested" | "label_sent" | "in_transit" | "received" | "refund_processed"; + createdAt: string; + refundAmount: number; + returnLabelUrl: string; + timeline: { date: string; event: string }[]; +} diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/tsconfig.json b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/tsconfig.json new file mode 100644 index 00000000..2d2840e4 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/order-management/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "sourceMap": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/.gitignore b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/.gitignore new file mode 100644 index 00000000..b9470778 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/package.json b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/package.json new file mode 100644 index 00000000..adb14fe9 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/package.json @@ -0,0 +1,22 @@ +{ + "name": "warehouse-mcp", + "version": "1.0.0", + "type": "module", + "main": "dist/start.js", + "scripts": { + "build": "tsc", + "start": "node dist/start.js", + "dev": "tsx src/start.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0", + "express": "^4.21.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/data/fulfillment.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/data/fulfillment.ts new file mode 100644 index 00000000..df20f3d1 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/data/fulfillment.ts @@ -0,0 +1,102 @@ +import { FulfillmentRecord } from "../types.js"; + +export const fulfillment: FulfillmentRecord[] = [ + { + orderId: "ORD-10422", + warehouse: "Chicago-IL1", + assignedWorker: "Mike Torres", + currentStage: "received", + pipeline: [ + { stage: "received", timestamp: "2026-04-15T09:30:00Z" }, + ], + estimatedShipDate: "2026-04-23", + notes: "Kindle Paperwhite — awaiting restock. Item reserved from incoming shipment.", + }, + { + orderId: "ORD-10460", + warehouse: "Chicago-IL1", + assignedWorker: "Lisa Park", + currentStage: "picked", + pipeline: [ + { stage: "received", timestamp: "2026-04-18T10:00:00Z" }, + { stage: "picked", timestamp: "2026-04-19T14:20:00Z" }, + ], + estimatedShipDate: "2026-04-21", + notes: "Switch OLED on backorder — Zelda game picked, holding for bundle ship.", + }, + { + orderId: "ORD-10421", + warehouse: "Seattle-WA1", + assignedWorker: "Carlos Mendez", + currentStage: "handed_to_carrier", + pipeline: [ + { stage: "received", timestamp: "2026-04-11T08:00:00Z" }, + { stage: "picked", timestamp: "2026-04-11T11:30:00Z" }, + { stage: "packed", timestamp: "2026-04-12T09:15:00Z" }, + { stage: "labeled", timestamp: "2026-04-12T10:00:00Z" }, + { stage: "handed_to_carrier", timestamp: "2026-04-13T08:45:00Z" }, + ], + estimatedShipDate: "2026-04-13", + notes: "Shipped via FedEx. Two items in single box.", + }, + { + orderId: "ORD-10455", + warehouse: "Dallas-TX1", + assignedWorker: "Rachel Kim", + currentStage: "handed_to_carrier", + pipeline: [ + { stage: "received", timestamp: "2026-04-12T11:00:00Z" }, + { stage: "picked", timestamp: "2026-04-13T09:00:00Z" }, + { stage: "packed", timestamp: "2026-04-13T14:30:00Z" }, + { stage: "labeled", timestamp: "2026-04-14T08:00:00Z" }, + { stage: "handed_to_carrier", timestamp: "2026-04-14T15:00:00Z" }, + ], + estimatedShipDate: "2026-04-14", + notes: "Shipped via UPS. Hoodie + joggers in one package.", + }, + { + orderId: "ORD-10470", + warehouse: "Seattle-WA1", + assignedWorker: "Carlos Mendez", + currentStage: "handed_to_carrier", + pipeline: [ + { stage: "received", timestamp: "2026-04-14T10:00:00Z" }, + { stage: "picked", timestamp: "2026-04-15T08:00:00Z" }, + { stage: "packed", timestamp: "2026-04-15T13:00:00Z" }, + { stage: "labeled", timestamp: "2026-04-16T09:00:00Z" }, + { stage: "handed_to_carrier", timestamp: "2026-04-16T14:00:00Z" }, + ], + estimatedShipDate: "2026-04-16", + notes: "Oversized item — FedEx Freight. Delivery appointment required.", + }, + { + orderId: "ORD-10318", + warehouse: "Seattle-WA1", + assignedWorker: "Anna Bell", + currentStage: "handed_to_carrier", + pipeline: [ + { stage: "received", timestamp: "2026-03-22T09:00:00Z" }, + { stage: "picked", timestamp: "2026-03-23T10:00:00Z" }, + { stage: "packed", timestamp: "2026-03-24T08:30:00Z" }, + { stage: "labeled", timestamp: "2026-03-24T11:00:00Z" }, + { stage: "handed_to_carrier", timestamp: "2026-03-25T09:00:00Z" }, + ], + estimatedShipDate: "2026-03-25", + notes: "AirPods + case shipped USPS Priority.", + }, + { + orderId: "ORD-10389", + warehouse: "Chicago-IL1", + assignedWorker: "Mike Torres", + currentStage: "handed_to_carrier", + pipeline: [ + { stage: "received", timestamp: "2026-03-28T08:00:00Z" }, + { stage: "picked", timestamp: "2026-03-28T14:00:00Z" }, + { stage: "packed", timestamp: "2026-03-29T09:00:00Z" }, + { stage: "labeled", timestamp: "2026-03-29T10:30:00Z" }, + { stage: "handed_to_carrier", timestamp: "2026-03-29T14:00:00Z" }, + ], + estimatedShipDate: "2026-03-29", + notes: "Books + Blu-ray in padded mailer. USPS Priority.", + }, +]; diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/data/restock.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/data/restock.ts new file mode 100644 index 00000000..351a6efd --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/data/restock.ts @@ -0,0 +1,40 @@ +import { RestockInfo } from "../types.js"; + +export const restockSchedule: RestockInfo[] = [ + { + sku: "SKU-KINDLE", + name: "Kindle Paperwhite (16GB)", + currentQuantity: 0, + nextShipmentDate: "2026-04-22", + expectedQuantity: 50, + supplier: "Amazon Devices Distribution", + notes: "Delayed from original April 18 date. Supplier confirmed new ETA.", + }, + { + sku: "SKU-SWITCH", + name: "Nintendo Switch OLED", + currentQuantity: 0, + nextShipmentDate: "2026-04-28", + expectedQuantity: 30, + supplier: "Nintendo of America", + notes: "High demand — limited allocation. Next batch after this is mid-May.", + }, + { + sku: "SKU-WH1000", + name: "Sony WH-1000XM5 Headphones", + currentQuantity: 3, + nextShipmentDate: "2026-05-05", + expectedQuantity: 20, + supplier: "Sony Electronics Inc.", + notes: "Regular replenishment cycle. Current stock sufficient for 1-2 weeks.", + }, + { + sku: "SKU-ERGOCHAIR", + name: "ErgoChair Pro - Matte Black", + currentQuantity: 1, + nextShipmentDate: "2026-04-30", + expectedQuantity: 10, + supplier: "Autonomous Inc.", + notes: "Low stock alert triggered. Express shipment arranged.", + }, +]; diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/data/stock.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/data/stock.ts new file mode 100644 index 00000000..7ee22794 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/data/stock.ts @@ -0,0 +1,34 @@ +import { StockEntry } from "../types.js"; + +export const stock: StockEntry[] = [ + // Electronics + { sku: "SKU-WH1000", name: "Sony WH-1000XM5 Headphones", category: "electronics", quantity: 3, warehouse: "Seattle-WA1", aisle: "E-14", availableForShipping: true }, + { sku: "SKU-USBC3", name: "USB-C Charging Cable (3-pack)", category: "electronics", quantity: 142, warehouse: "Seattle-WA1", aisle: "A-02", availableForShipping: true }, + { sku: "SKU-KINDLE", name: "Kindle Paperwhite (16GB)", category: "electronics", quantity: 0, warehouse: "Chicago-IL1", aisle: "E-08", availableForShipping: false }, + { sku: "SKU-AIRPOD", name: "AirPods Pro (2nd Gen)", category: "electronics", quantity: 17, warehouse: "Seattle-WA1", aisle: "E-12", availableForShipping: true }, + { sku: "SKU-SWITCH", name: "Nintendo Switch OLED", category: "electronics", quantity: 0, warehouse: "Chicago-IL1", aisle: "E-22", availableForShipping: false }, + { sku: "SKU-ZELDA", name: "The Legend of Zelda: Tears of the Kingdom", category: "electronics", quantity: 24, warehouse: "Chicago-IL1", aisle: "G-05", availableForShipping: true }, + { sku: "SKU-IPAD", name: "iPad Air (M2, 256GB)", category: "electronics", quantity: 5, warehouse: "Seattle-WA1", aisle: "E-10", availableForShipping: true }, + + // Audio alternatives + { sku: "SKU-WH900", name: "Sony WH-1000XM4 Headphones (Previous Gen)", category: "electronics", quantity: 8, warehouse: "Seattle-WA1", aisle: "E-14", availableForShipping: true }, + { sku: "SKU-BOSE700", name: "Bose 700 Noise Cancelling Headphones", category: "electronics", quantity: 6, warehouse: "Seattle-WA1", aisle: "E-15", availableForShipping: true }, + { sku: "SKU-AIRMAX", name: "AirPods Max - Space Gray", category: "electronics", quantity: 2, warehouse: "Seattle-WA1", aisle: "E-13", availableForShipping: true }, + + // Clothing + { sku: "SKU-HOODIE", name: "Nike Tech Fleece Hoodie - Black (L)", category: "clothing", quantity: 4, warehouse: "Dallas-TX1", aisle: "C-07", availableForShipping: true }, + { sku: "SKU-HOODIE-XL", name: "Nike Tech Fleece Hoodie - Black (XL)", category: "clothing", quantity: 7, warehouse: "Dallas-TX1", aisle: "C-07", availableForShipping: true }, + { sku: "SKU-HOODIE-GRY", name: "Nike Tech Fleece Hoodie - Grey (L)", category: "clothing", quantity: 2, warehouse: "Dallas-TX1", aisle: "C-07", availableForShipping: true }, + { sku: "SKU-JOGGER", name: "Nike Sportswear Joggers - Grey (L)", category: "clothing", quantity: 11, warehouse: "Dallas-TX1", aisle: "C-09", availableForShipping: true }, + { sku: "SKU-JOGGER-XL", name: "Nike Sportswear Joggers - Grey (XL)", category: "clothing", quantity: 3, warehouse: "Dallas-TX1", aisle: "C-09", availableForShipping: true }, + + // Books & Media + { sku: "SKU-DUNE2", name: "Dune: Part Two (4K Blu-ray)", category: "media", quantity: 9, warehouse: "Chicago-IL1", aisle: "M-03", availableForShipping: true }, + { sku: "SKU-FOUNDATION", name: "Foundation (Isaac Asimov) - Paperback", category: "books", quantity: 22, warehouse: "Chicago-IL1", aisle: "B-11", availableForShipping: true }, + { sku: "SKU-3BODY", name: "The Three-Body Problem - Paperback", category: "books", quantity: 15, warehouse: "Chicago-IL1", aisle: "B-11", availableForShipping: true }, + + // Furniture + { sku: "SKU-ERGOCHAIR", name: "ErgoChair Pro - Matte Black", category: "furniture", quantity: 1, warehouse: "Seattle-WA1", aisle: "F-01", availableForShipping: true }, + { sku: "SKU-ERGOCHAIR-W", name: "ErgoChair Pro - White", category: "furniture", quantity: 3, warehouse: "Seattle-WA1", aisle: "F-01", availableForShipping: true }, + { sku: "SKU-DESKMAT", name: "Felt Desk Mat - Dark Grey (90x40cm)", category: "furniture", quantity: 30, warehouse: "Seattle-WA1", aisle: "F-04", availableForShipping: true }, +]; diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/server.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/server.ts new file mode 100644 index 00000000..fe0a5baa --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/server.ts @@ -0,0 +1,249 @@ +import express, { Request, Response } from "express"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { z } from "zod"; +import { stock } from "./data/stock.js"; +import { fulfillment } from "./data/fulfillment.js"; +import { restockSchedule } from "./data/restock.js"; + +// --------------------------------------------------------------------------- +// MCP Server — 4 interdependent tools for warehouse & fulfillment +// --------------------------------------------------------------------------- + +function createMcpServer(): McpServer { + const server = new McpServer({ + name: "warehouse-fulfillment", + version: "1.0.0", + }); + + // 1. check_stock — look up inventory for a SKU + server.tool( + "check_stock", + "Check warehouse inventory for a product by SKU. Returns quantity on hand, warehouse location, " + + "aisle, and whether the item is available for shipping. If quantity is 0, use get_restock_date for restock info.", + { + sku: z.string().describe("Product SKU (e.g. 'SKU-WH1000'). Use SKUs from order line items."), + }, + async ({ sku }) => { + const entry = stock.find((s) => s.sku === sku); + if (!entry) { + return { + content: [{ type: "text", text: `SKU "${sku}" not found in warehouse inventory.` }], + isError: true, + }; + } + + return { + content: [{ + type: "text", + text: JSON.stringify({ + sku: entry.sku, + name: entry.name, + category: entry.category, + quantity_on_hand: entry.quantity, + warehouse: entry.warehouse, + aisle: entry.aisle, + available_for_shipping: entry.availableForShipping, + status: entry.quantity === 0 ? "OUT_OF_STOCK" : entry.quantity <= 3 ? "LOW_STOCK" : "IN_STOCK", + }, null, 2), + }], + }; + } + ); + + // 2. get_fulfillment_status — where is an order in the warehouse pipeline + server.tool( + "get_fulfillment_status", + "Get the fulfillment pipeline status for an order. Shows which warehouse stage the order is at " + + "(received → picked → packed → labeled → handed_to_carrier), assigned worker, and estimated ship date. " + + "Use an order_id from the order management system.", + { + order_id: z.string().describe("Order ID (e.g. 'ORD-10422') from the order management system"), + }, + async ({ order_id }) => { + const record = fulfillment.find((f) => f.orderId === order_id); + if (!record) { + return { + content: [{ + type: "text", + text: `No fulfillment record found for order "${order_id}". The order may not have entered the warehouse yet.`, + }], + isError: true, + }; + } + + return { + content: [{ + type: "text", + text: JSON.stringify({ + order_id: record.orderId, + warehouse: record.warehouse, + assigned_worker: record.assignedWorker, + current_stage: record.currentStage, + estimated_ship_date: record.estimatedShipDate, + notes: record.notes, + pipeline: record.pipeline.map((s) => ({ + stage: s.stage, + completed_at: s.timestamp, + })), + }, null, 2), + }], + }; + } + ); + + // 3. find_alternatives — similar products in same category + server.tool( + "find_alternatives", + "Find alternative products similar to a given SKU. Returns items in the same category that are in stock. " + + "Useful when a customer wants a different size, color, or comparable product, or when an item is out of stock.", + { + sku: z.string().describe("Product SKU to find alternatives for (e.g. 'SKU-HOODIE')"), + }, + async ({ sku }) => { + const original = stock.find((s) => s.sku === sku); + if (!original) { + return { + content: [{ type: "text", text: `SKU "${sku}" not found in inventory.` }], + isError: true, + }; + } + + // Find items in same category, excluding the original, that are in stock + const alternatives = stock.filter( + (s) => s.category === original.category && s.sku !== sku && s.quantity > 0 + ); + + if (alternatives.length === 0) { + return { + content: [{ + type: "text", + text: `No alternatives found for "${original.name}" in the ${original.category} category.`, + }], + }; + } + + return { + content: [{ + type: "text", + text: JSON.stringify({ + original: { sku: original.sku, name: original.name, category: original.category }, + alternatives: alternatives.map((a) => ({ + sku: a.sku, + name: a.name, + quantity_available: a.quantity, + warehouse: a.warehouse, + available_for_shipping: a.availableForShipping, + })), + }, null, 2), + }], + }; + } + ); + + // 4. get_restock_date — when will an item be back in stock + server.tool( + "get_restock_date", + "Get the next restock date and supplier info for a product. Use this when check_stock shows an item " + + "is out of stock or low stock. Returns expected delivery date, quantity, and supplier details.", + { + sku: z.string().describe("Product SKU (e.g. 'SKU-KINDLE'). Typically used after check_stock shows low/no stock."), + }, + async ({ sku }) => { + const info = restockSchedule.find((r) => r.sku === sku); + if (!info) { + return { + content: [{ + type: "text", + text: `No restock schedule found for SKU "${sku}". The item may be regularly stocked or discontinued.`, + }], + }; + } + + return { + content: [{ + type: "text", + text: JSON.stringify({ + sku: info.sku, + name: info.name, + current_quantity: info.currentQuantity, + next_shipment_date: info.nextShipmentDate, + expected_quantity: info.expectedQuantity, + supplier: info.supplier, + notes: info.notes, + }, null, 2), + }], + }; + } + ); + + return server; +} + +// --------------------------------------------------------------------------- +// Express app — stateless Streamable HTTP transport +// --------------------------------------------------------------------------- + +export function createServer(port: number): void { + const app = express(); + + app.use((_req, res, next) => { + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, DELETE"); + res.setHeader("Access-Control-Allow-Headers", "*"); + if (_req.method === "OPTIONS") { + res.sendStatus(204); + return; + } + next(); + }); + + app.use(express.json()); + + app.post("/mcp", async (req: Request, res: Response) => { + const server = createMcpServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + + await server.connect(transport); + + try { + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("Error handling MCP request:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); + } + } + }); + + app.get("/mcp", (_req: Request, res: Response) => { + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { code: -32000, message: "Method not allowed." }, + id: null, + })); + }); + + app.delete("/mcp", (_req: Request, res: Response) => { + res.writeHead(405).end(JSON.stringify({ + jsonrpc: "2.0", + error: { code: -32000, message: "Method not allowed." }, + id: null, + })); + }); + + app.get("/health", (_req: Request, res: Response) => { + res.json({ status: "ok", name: "warehouse-mcp", version: "1.0.0" }); + }); + + app.listen(port, () => { + console.log(`Warehouse MCP Server running on http://localhost:${port}/mcp`); + console.log(`Health check: http://localhost:${port}/health`); + }); +} diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/start.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/start.ts new file mode 100644 index 00000000..de8e39c9 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/start.ts @@ -0,0 +1,4 @@ +import { createServer } from "./server.js"; + +const PORT = parseInt(process.env.PORT || "3001"); +createServer(PORT); diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/types.ts b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/types.ts new file mode 100644 index 00000000..5c931b8e --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/src/types.ts @@ -0,0 +1,43 @@ +export interface StockEntry { + sku: string; + name: string; + category: string; + quantity: number; + warehouse: string; + aisle: string; + availableForShipping: boolean; +} + +export interface FulfillmentStage { + stage: "received" | "picked" | "packed" | "labeled" | "handed_to_carrier"; + timestamp: string; +} + +export interface FulfillmentRecord { + orderId: string; + warehouse: string; + assignedWorker: string; + currentStage: string; + pipeline: FulfillmentStage[]; + estimatedShipDate: string; + notes: string; +} + +export interface Alternative { + sku: string; + name: string; + price: number; + quantity: number; + warehouse: string; + availableForShipping: boolean; +} + +export interface RestockInfo { + sku: string; + name: string; + currentQuantity: number; + nextShipmentDate: string; + expectedQuantity: number; + supplier: string; + notes: string; +} diff --git a/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/tsconfig.json b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/tsconfig.json new file mode 100644 index 00000000..2d2840e4 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/mcp-servers/warehouse/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "declaration": true, + "sourceMap": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/extensibility/mcp/order-management-enhanced-tc/scripts/deploy-connectors.mjs b/extensibility/mcp/order-management-enhanced-tc/scripts/deploy-connectors.mjs new file mode 100644 index 00000000..de60c4cc --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/scripts/deploy-connectors.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node + +/** + * Cross-platform script to deploy MCP connectors to a Power Platform environment. + * + * Usage: node scripts/deploy-connectors.mjs + * + * Prerequisites: + * pip install paconn + * paconn login (or: python3 -m paconn login) + * + * Example: + * node scripts/deploy-connectors.mjs 6cc0c98e-3fe6-e0d5-8eba-ba51c9da1d13 \ + * https://abc123-3000.uks1.devtunnels.ms \ + * https://def456-3001.uks1.devtunnels.ms + */ + +import { execSync } from "child_process"; +import { readFileSync, writeFileSync, mkdtempSync, cpSync } from "fs"; +import { resolve, dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { tmpdir } from "os"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, ".."); + +const [,, envId, orderUrl, warehouseUrl] = process.argv; + +if (!envId || !orderUrl || !warehouseUrl) { + console.error("Usage: node scripts/deploy-connectors.mjs "); + console.error(""); + console.error("Example:"); + console.error(" node scripts/deploy-connectors.mjs 6cc0c98e-... https://abc-3000.devtunnels.ms https://def-3001.devtunnels.ms"); + process.exit(1); +} + +const paconn = process.platform === "win32" ? "paconn" : "python3 -m paconn"; + +function deployConnector(name, connectorDir, tunnelUrl) { + console.log(`\n=== Deploying ${name} connector ===`); + + // Create a temp copy with the tunnel URL patched in + const tmpDir = mkdtempSync(join(tmpdir(), `connector-${name}-`)); + cpSync(connectorDir, tmpDir, { recursive: true }); + + // Patch the swagger host + const swaggerPath = join(tmpDir, "apiDefinition.swagger.json"); + let swagger = readFileSync(swaggerPath, "utf-8"); + + // Extract host from tunnel URL (strip scheme and trailing slash) + const host = tunnelUrl.replace(/^https?:\/\//, "").replace(/\/$/, ""); + swagger = swagger.replace("TUNNEL_HOST_PLACEHOLDER", host); + writeFileSync(swaggerPath, swagger); + + console.log(` Tunnel URL: ${tunnelUrl}`); + console.log(` Swagger host set to: ${host}`); + + try { + execSync( + `${paconn} create --api-def "${join(tmpDir, "apiDefinition.swagger.json")}" --api-prop "${join(tmpDir, "apiProperties.json")}" --env "${envId}"`, + { stdio: "inherit" } + ); + console.log(` ${name} connector deployed successfully.`); + } catch (e) { + console.error(` Failed to deploy ${name} connector. You may need to run 'paconn login' first.`); + console.error(` Or use 'paconn update' if the connector already exists.`); + } +} + +deployConnector("Order Management", resolve(ROOT, "connectors/order-management"), orderUrl); +deployConnector("Warehouse", resolve(ROOT, "connectors/warehouse"), warehouseUrl); + +console.log("\n=== Done ==="); +console.log("Next: Import agent solutions in Copilot Studio (see agents/IMPORT.md)"); diff --git a/extensibility/mcp/order-management-enhanced-tc/scripts/setup.mjs b/extensibility/mcp/order-management-enhanced-tc/scripts/setup.mjs new file mode 100644 index 00000000..94600954 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/scripts/setup.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node + +/** + * Cross-platform setup script. + * Installs and builds both MCP servers + Python chat UI dependencies. + */ + +import { execSync } from "child_process"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, ".."); + +function run(cmd, cwd) { + console.log(`\n> ${cmd} (in ${cwd})`); + execSync(cmd, { cwd, stdio: "inherit" }); +} + +console.log("=== Setting up Order Management MCP Server ==="); +run("npm install", resolve(ROOT, "mcp-servers/order-management")); +run("npm run build", resolve(ROOT, "mcp-servers/order-management")); + +console.log("\n=== Setting up Warehouse MCP Server ==="); +run("npm install", resolve(ROOT, "mcp-servers/warehouse")); +run("npm run build", resolve(ROOT, "mcp-servers/warehouse")); + +console.log("\n=== Setting up Chat UI ==="); +const chatDir = resolve(ROOT, "chat-ui"); +const python = process.platform === "win32" ? "python" : "python3"; +const venvBin = process.platform === "win32" ? "Scripts" : "bin"; +const pip = resolve(chatDir, ".venv", venvBin, "pip"); + +try { + run(`${python} -m venv .venv`, chatDir); + run(`${pip} install -r requirements.txt`, chatDir); +} catch (e) { + console.error("Python setup failed. Ensure Python 3.12+ is installed."); + console.error(e.message); +} + +console.log("\n=== Setup complete ==="); +console.log("Next steps:"); +console.log(" 1. Start servers: node scripts/start.mjs"); +console.log(" 2. Deploy connectors: node scripts/deploy-connectors.mjs "); +console.log(" 3. Import agents in Copilot Studio (see agents/IMPORT.md)"); +console.log(" 4. Start UI: node scripts/start-ui.mjs"); diff --git a/extensibility/mcp/order-management-enhanced-tc/scripts/start-ui.mjs b/extensibility/mcp/order-management-enhanced-tc/scripts/start-ui.mjs new file mode 100644 index 00000000..c19ba003 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/scripts/start-ui.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env node + +/** + * Cross-platform script to start the Gradio chat UI. + * Sets up venv and installs dependencies if needed. + * + * Usage: node scripts/start-ui.mjs + */ + +import { execSync, spawn } from "child_process"; +import { existsSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const chatDir = resolve(__dirname, "..", "chat-ui"); +const python = process.platform === "win32" ? "python" : "python3"; +const venvDir = resolve(chatDir, ".venv"); +const venvBin = process.platform === "win32" ? "Scripts" : "bin"; +const venvPython = resolve(venvDir, venvBin, process.platform === "win32" ? "python.exe" : "python"); + +// Setup venv if needed +if (!existsSync(venvDir)) { + console.log("Creating Python virtual environment..."); + execSync(`${python} -m venv .venv`, { cwd: chatDir, stdio: "inherit" }); + console.log("Installing dependencies..."); + execSync(`${venvPython} -m pip install -r requirements.txt`, { cwd: chatDir, stdio: "inherit" }); +} + +// Check for .env +if (!existsSync(resolve(chatDir, ".env"))) { + console.error("Missing chat-ui/.env — copy .env.sample and fill in your agent details."); + console.error("See agents/IMPORT.md for the values you need."); + process.exit(1); +} + +console.log("Starting Gradio chat UI...\n"); +const app = spawn(venvPython, ["app.py"], { cwd: chatDir, stdio: "inherit" }); + +app.on("close", (code) => process.exit(code ?? 0)); +process.on("SIGINT", () => { app.kill(); process.exit(0); }); +process.on("SIGTERM", () => { app.kill(); process.exit(0); }); diff --git a/extensibility/mcp/order-management-enhanced-tc/scripts/start.mjs b/extensibility/mcp/order-management-enhanced-tc/scripts/start.mjs new file mode 100644 index 00000000..2c5f2564 --- /dev/null +++ b/extensibility/mcp/order-management-enhanced-tc/scripts/start.mjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +/** + * Cross-platform script to start both MCP servers + devtunnels. + * Prints tunnel URLs to update in Copilot Studio MCP actions. + * + * Usage: node scripts/start.mjs + */ + +import { spawn } from "child_process"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, ".."); + +const servers = [ + { name: "Order Management", dir: "mcp-servers/order-management", port: 3000, tunnelUrl: null }, + { name: "Warehouse", dir: "mcp-servers/warehouse", port: 3001, tunnelUrl: null }, +]; + +const children = []; + +function startServer(server) { + const cwd = resolve(ROOT, server.dir); + const proc = spawn("node", ["dist/start.js"], { + cwd, + env: { ...process.env, PORT: String(server.port) }, + stdio: ["ignore", "pipe", "pipe"], + }); + proc.stdout.on("data", (d) => process.stdout.write(`[${server.name}] ${d}`)); + proc.stderr.on("data", (d) => process.stderr.write(`[${server.name}] ${d}`)); + children.push(proc); +} + +function startTunnel(server) { + const proc = spawn("devtunnel", ["host", "-p", String(server.port), "-a"], { + stdio: ["ignore", "pipe", "pipe"], + }); + + proc.stdout.on("data", (data) => { + const text = data.toString(); + if (text.includes("Connect via browser")) { + const urls = text.match(/https:\/\/[^\s,]+/g); + if (urls) { + const clean = urls.find((u) => u.includes(`-${server.port}.`)) ?? urls[0]; + server.tunnelUrl = clean.replace(/\/$/, ""); + console.log(`\n ${server.name} MCP endpoint: ${server.tunnelUrl}/mcp\n`); + if (servers.every((s) => s.tunnelUrl)) { + console.log("All tunnels ready. Update these URLs in Copilot Studio:\n"); + for (const s of servers) { + console.log(` ${s.name}: ${s.tunnelUrl}/mcp`); + } + console.log("\nPress Ctrl+C to stop all servers.\n"); + } + } + } + }); + proc.stderr.on("data", (d) => process.stderr.write(d)); + children.push(proc); +} + +function cleanup() { + for (const child of children) { + try { child.kill(); } catch {} + } + process.exit(0); +} + +process.on("SIGINT", cleanup); +process.on("SIGTERM", cleanup); + +console.log("Starting MCP servers and devtunnels...\n"); + +for (const server of servers) { + startServer(server); + startTunnel(server); +} diff --git a/testing/README.md b/testing/README.md index 81c70827..fca32960 100644 --- a/testing/README.md +++ b/testing/README.md @@ -13,5 +13,6 @@ Test frameworks and samples for Copilot Studio agents. | Folder | Description | |--------|-------------| +| [evaluation/](./evaluation/) | Automated evaluation using the Copilot Studio Evaluation API | | [functional/](./functional/) | Functional testing with pytest and response analysis | | [load/](./load/) | Load testing with JMeter | diff --git a/testing/evaluation/EvalGateADO/README.md b/testing/evaluation/EvalGateADO/README.md new file mode 100644 index 00000000..1778d6cf --- /dev/null +++ b/testing/evaluation/EvalGateADO/README.md @@ -0,0 +1,388 @@ +--- +title: Eval Gate for Azure DevOps +parent: Evaluation +grand_parent: Testing +nav_order: 1 +--- +# Eval Gate for Azure DevOps + +New +{: .label .label-green } + +An Azure DevOps pipeline that uses the [Copilot Studio Evaluation API](https://learn.microsoft.com/en-us/microsoft-copilot-studio/analytics-agent-evaluation-rest-api) as a PR quality gate. When a developer pushes changes to their feature branch and opens a PR, the pipeline automatically: + +1. Packs the solution from the PR branch +2. Imports it into a shared CI Dev environment +3. Runs evaluations against the draft agent +4. Blocks or allows the merge based on a configurable pass rate threshold +5. Publishes results to the ADO **Tests** tab + +## How It Works + +```mermaid +flowchart LR + A[Edit agent in\nCopilot Studio] --> B[Commit & push\nto feature branch] + B --> C[Open PR] + C --> D[Pipeline imports\nto CI Dev] + D --> E[Run evals\non draft] + E --> F{Pass rate\n>= threshold?} + F -->|Yes| G[Merge allowed] + F -->|No| H[Merge blocked] +``` + +## Pipeline in Action + +When a PR is pushed, the pipeline imports the solution, resolves the bot and test set, and runs the evaluation — all visible in the ADO build logs: + +![Pipeline running eval](./assets/pipeline-run.png) + +## Table of Contents + +- [How It Works](#how-it-works) +- [Components](#components) +- [Prerequisites](#prerequisites) +- [Setup](#setup) + - [Part 1: Power Platform](#part-1-power-platform) — Git integration, CI Dev environment, test set, MCS connection + - [Part 2: Authentication](#part-2-authentication) — App registration, refresh token + - [Part 3: Azure Resources](#part-3-azure-resources) — Key Vault, secrets, ARM service principal + - [Part 4: Azure DevOps](#part-4-azure-devops) — Config, service connection, pipeline, branch policy +- [Running It](#running-it) +- [Local Usage](#local-usage) +- [Known Limitations](#known-limitations) + +## Components + +| File | Description | +|------|-------------| +| `eval-config.json` | Environment IDs, bot schema name, pass threshold | +| `scripts/eval-gate.mjs` | Node.js script: MSAL auth + PPAPI client + JUnit output. Single dependency: `@azure/msal-node` | +| `pipelines/eval-gate.yml` | ADO pipeline with self-hosted (pac CLI) and hosted (Build Tools) options | + +--- + +## Prerequisites + +- **Power Platform**: Dev environment as [Managed Environment](https://learn.microsoft.com/en-us/power-platform/admin/managed-environment-overview), connected to ADO Git via [Dataverse git integration](https://learn.microsoft.com/en-us/power-platform/alm/git-integration/connecting-to-git) +- **Copilot Studio**: Agent with a [test set](https://learn.microsoft.com/en-us/microsoft-copilot-studio/analytics-agent-evaluation-intro) in the Evaluate tab +- **Azure**: Subscription with Key Vault access +- **Azure DevOps**: Project with Git repo, [Power Platform Build Tools](https://marketplace.visualstudio.com/items?itemName=microsoft-IsvExpTools.PowerPlatform-BuildTools) extension +- **Tooling**: [pac CLI](https://learn.microsoft.com/en-us/power-platform/developer/howto/install-cli-net-tool) (requires .NET 10), Node.js 18+, Azure CLI + +--- + +## Setup + +### Part 1: Power Platform + +#### 1.1 Connect Dev Environment to Git + +1. Power Platform Admin Center: enable your Dev environment as **Managed Environment** +2. Copilot Studio → Solutions → **Connect to Git** +3. Choose **Environment binding** → select your ADO org, project, repo +4. Select your **feature branch** (each developer connects to their own) +5. Set Git folder to `src` → **Connect** + +{: .important } +> You **must** use `src` as the Git folder — the pipeline is configured to pack from this path. If you use a different folder, update the `--folder` argument in `pipelines/eval-gate.yml`. + +{: .tip } +> To commit changes: open a solution → **Source control** (left pane) → review changes → **Commit & push**. + +#### 1.2 Create CI Dev Environment + +A dedicated environment where the pipeline imports and tests solutions. No git binding needed. + +```bash +# Install pac CLI (requires .NET 10 — earlier versions produce a DotnetToolSettings.xml error) +dotnet tool install --global Microsoft.PowerApps.CLI.Tool + +# Create the environment +pac admin create --name "CI - MyAgent" --type Developer --region unitedstates + +# Create a Service Principal for pipeline access +pac admin create-service-principal --environment +``` + +{: .important } +> Save the **Application ID**, **Client Secret**, and **Environment URL** from the output — you'll need all three. The `create-service-principal` command automatically assigns the System Administrator role to the SPN in the target environment. + +#### 1.3 Create a Test Set + +1. Open your agent in Copilot Studio → **Evaluate** tab → **New evaluation** +2. Add 10-20 representative test cases covering key scenarios +3. Commit the solution to git — test sets are part of the solution and travel with it on import + +{: .note } +> The script auto-discovers the test set after import — it uses the first available test set. If your agent has multiple test sets, set `testSetName` in `eval-config.json` to target a specific one by display name. + +#### 1.4 Create MCS Connection (Optional — for Authenticated Eval) + +If your agent uses authenticated actions or knowledge sources (e.g., SharePoint connectors), the eval API needs an `mcsConnectionId` to authenticate during the run. See [Manage user profiles and connections](https://learn.microsoft.com/en-us/microsoft-copilot-studio/analytics-agent-evaluation-edit#manage-user-profiles-and-connections). + +1. Open [Power Automate](https://make.powerautomate.com) **in the CI Dev environment** +2. Go to **Connections** → create a **Microsoft Copilot Studio** connection +3. Click on the connection → copy the ID from the URL: + ``` + .../connections/shared_microsoftcopilotstudio/{mcsConnectionId}/details + ``` + +{: .warning } +> This connection requires interactive OAuth and cannot be created programmatically. It's a one-time manual step per CI environment. Without it, the pipeline still works but authenticated actions and knowledge sources will return empty or error results during eval. + +--- + +### Part 2: Authentication + +#### 2.1 Create Eval API App Registration + +The evaluation API requires **delegated** (user-context) permissions: + +1. [Azure Portal → App Registrations](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) → **New registration** +2. Name it (e.g., "Copilot Studio Eval API") +3. Under **API permissions** → **Add a permission** → **APIs my organization uses**: + - Search **Power Platform API** → delegated: `CopilotStudio.MakerOperations.Read`, `CopilotStudio.MakerOperations.ReadWrite` + - Search **Dynamics CRM** → delegated: `user_impersonation` (for bot ID resolution) +4. Click **Grant admin consent** for the tenant +5. Under **Authentication** → **Add a platform** → **Mobile and desktop applications** → redirect URI: `http://localhost` +6. **Authentication** → **Advanced settings** → **Allow public client flows**: **Yes** +7. Copy the **Application (client) ID** + +#### 2.2 Seed the Refresh Token + +```bash +cd scripts && npm install + +# Start device code flow — follow the browser prompt +node eval-gate.mjs auth --config ../eval-config.json +``` + +The token is printed to stdout. Store it in Key Vault (see part 3): + +```bash +az keyvault secret set \ + --vault-name \ + --name copilot-studio-eval-refresh-token \ + --value "" +``` + +Or pipe directly: + +```bash +node eval-gate.mjs auth --config ../eval-config.json \ + | az keyvault secret set \ + --vault-name \ + --name copilot-studio-eval-refresh-token \ + --value @- +``` + +{: .note } +> The pipeline attempts to rotate the refresh token on each run. If MSAL issues a new token, it is written back to Key Vault automatically. MSAL does not always issue a new token on every call, so the existing token may remain. Re-run the `auth` command before the **90-day** expiry to be safe. + +--- + +### Part 3: Azure Resources + +#### 3.1 Create Key Vault + +```bash +az group create --name rg-copilot-cicd --location eastus + +az keyvault create \ + --name \ + --resource-group rg-copilot-cicd \ + --location eastus \ + --enable-rbac-authorization true + +# Grant yourself Secrets Officer to seed secrets +az role assignment create \ + --role "Key Vault Secrets Officer" \ + --assignee \ + --scope +``` + +#### 3.2 Store Secrets in Key Vault + +The pipeline reads three secrets from Key Vault: + +| Secret Name | Value | Source | +|-------------|-------|--------| +| `copilot-studio-eval-refresh-token` | MSAL refresh token | From `node eval-gate.mjs auth` (step 2.2) | +| `copilot-studio-ci-dev-client-secret` | CI Dev SPN client secret | From `pac admin create-service-principal` (step 1.2) | +| `copilot-studio-eval-mcs-connection-id` | MCS connector connection ID | From Power Automate URL (step 1.4, optional) | + +```bash +az keyvault secret set --vault-name \ + --name copilot-studio-eval-refresh-token --value "" + +az keyvault secret set --vault-name \ + --name copilot-studio-ci-dev-client-secret --value "" + +# Required even if not using authenticated eval — use a placeholder value if skipping step 1.4 +az keyvault secret set --vault-name \ + --name copilot-studio-eval-mcs-connection-id --value "${:-none}" +``` + +{: .warning } +> All three secrets must exist in Key Vault. The pipeline's `AzureKeyVault@2` task fails if any named secret is missing. If you're not using authenticated eval (step 1.4), create the `copilot-studio-eval-mcs-connection-id` secret with the value `none`. + +#### 3.3 Create ARM Service Connection SPN + +Create a Service Principal for the pipeline to access Key Vault: + +```bash +az ad sp create-for-rbac \ + --name "copilot-cicd-pipeline" \ + --role "Key Vault Secrets Officer" \ + --scopes /subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/ +``` + +Save the **Application ID** and **Password** for step 4.2. + +--- + +### Part 4: Azure DevOps + +#### 4.1 Configure eval-config.json + +Edit `eval-config.json` in your repo root and fill in the values from previous steps: + +```json +{ + "environmentId": "", + "environmentUrl": "https://.crm.dynamics.com/", + "botSchemaName": "", + "tenantId": "", + "clientId": "", + "passThreshold": 0.8 +} +``` + +Commit this file to your repo — the pipeline reads it at runtime. + +| Field | Description | Where to find it | +|-------|-------------|-----------------| +| `environmentId` | CI Dev environment GUID | Power Platform Admin Center → Environments | +| `environmentUrl` | CI Dev Dataverse URL (trailing slash) | Same page, or `pac env who` | +| `botSchemaName` | Bot schema name from the solution | `src/bots//bot.yml` → `@schemaname` | +| `tenantId` | Entra tenant GUID | Azure Portal → Entra ID → Overview | +| `clientId` | Eval API app registration client ID | From step 2.1 | +| `passThreshold` | Pass rate required (0.0-1.0) | Choose your quality bar | + +{: .note } +> `agentId` and `testSetId` are resolved dynamically at runtime — you don't set them. + +#### 4.2 Create ADO Service Connection + +In ADO: Project Settings → Service connections → New → **Azure Resource Manager** → **Service principal (manual)** + +| Field | Value | +|-------|-------| +| Name | `AzureRM-KeyVault` | +| Subscription ID | Your Azure subscription ID | +| Service Principal ID | App ID from step 3.3 | +| Service Principal Key | Password from step 3.3 | +| Tenant ID | Your Entra tenant ID | + +Enable for all pipelines. + +#### 4.3 Update Pipeline Variables + +Edit `pipelines/eval-gate.yml` and set: + +```yaml +variables: + AZURE_SERVICE_CONNECTION: '' + KEY_VAULT_NAME: '' + CI_DEV_ENV_URL: '' + CI_DEV_APP_ID: '' + CI_DEV_TENANT_ID: '' +``` + +Commit and push `eval-config.json` and `pipelines/eval-gate.yml` to your repo before proceeding. + +#### 4.4 Create the Pipeline + +1. ADO → Pipelines → New pipeline → Azure Repos Git → select your repo +2. Point to `pipelines/eval-gate.yml` +3. Save (don't run yet) +4. Pipeline Settings → set max concurrent runs to **1** (sequential execution) + +#### 4.5 Approve Service Connections (First Run Only) + +The first time the pipeline runs, ADO will prompt you to **Permit** access to the `AzureRM-KeyVault` service connection. Click **Permit** — this is a one-time approval. + +#### 4.6 Configure Branch Policy (Merge Gate) + +1. ADO Repos → Branches → `main` → Branch policies +2. **Build validation** → Add → select the eval-gate pipeline +3. Set to **Required** → Trigger: **Automatic** + +--- + +## Running It + +Once setup is complete, the eval gate runs automatically: + +1. **Make a change** to your agent in Copilot Studio (edit a topic, update instructions, etc.) +2. **Commit & push** from the Solutions → Source control page to your feature branch +3. **Open a PR** targeting `main` in Azure DevOps +4. The **pipeline triggers automatically** on push — it packs the solution, imports it into the CI Dev environment, and runs evaluations against the draft agent +5. **Check the results** in the PR: + - The **Summary** tab shows the pipeline pass/fail status + - The **Tests** tab shows individual test case results with metric breakdowns + - The eval results JSON is available as a **pipeline artifact** +6. If the pass rate meets the threshold, the **merge button is enabled**. Otherwise, the PR is blocked until the agent quality improves. + +Subsequent pushes to the same PR branch re-trigger the pipeline — each push gets a fresh eval run. + +**PR gate passed** — eval meets the threshold, merge is allowed: + +![PR with eval gate passed](./assets/pr-gate-passed.png) + +**PR gate failed** — eval below threshold, merge is blocked: + +![PR with eval gate failed](./assets/pr-gate-failed.png) + +**Tests tab** — individual test case results with pass/fail breakdown: + +![Tests tab showing eval results](./assets/tests-tab.png) + +## Local Usage + +Run evals locally against your own Dev environment: + +```bash +export EVAL_REFRESH_TOKEN="" + +# Verify bot ID resolution +node scripts/eval-gate.mjs resolve-bot --config eval-config.json + +# List test sets +node scripts/eval-gate.mjs list-testsets --config eval-config.json + +# Run eval +node scripts/eval-gate.mjs run \ + --config eval-config.json \ + --run-name "local test" \ + --output results.json \ + --junit-output results.junit.xml + +# Override for a different environment +node scripts/eval-gate.mjs run \ + --config eval-config.json \ + --environment-id "" \ + --agent-id "" \ + --threshold 0.9 \ + --run-name "local test" +``` + +--- + +## Known Limitations + +- **Delegated auth only**: The eval API requires user-context tokens. The pipeline uses a pre-cached refresh token (90-day expiry). There is no app-only authentication path. +- **MCS connection is manual**: The Microsoft Copilot Studio connector connection requires interactive OAuth and cannot be created programmatically. See [step 1.4](#14-create-mcs-connection-optional--for-authenticated-eval) for setup. Without it, authenticated actions and knowledge sources won't work during eval. +- **Single CI environment**: Pipeline runs are serialized (max 1 concurrent). Multiple PRs queue and run one at a time. +- **Self-hosted agent**: The `pac` CLI approach requires a self-hosted agent. The pipeline's `DOTNET_ROOT` path is configured for macOS with Homebrew — adjust it for Linux agents. For Microsoft-hosted agents (Windows/Linux), use the commented-out Power Platform Build Tools section in the pipeline YAML. +- **Errors count as failures**: Evaluation errors (e.g., model timeouts, missing MCS connection) count against the pass threshold. If a run produces unexpectedly low scores, check `eval-results.json` for test cases with `Error` status. +- **Test case names**: The eval API returns test case IDs (GUIDs), not display names. Results in the Tests tab show GUIDs. diff --git a/testing/evaluation/EvalGateADO/assets/pipeline-run.png b/testing/evaluation/EvalGateADO/assets/pipeline-run.png new file mode 100644 index 00000000..d0161ef2 Binary files /dev/null and b/testing/evaluation/EvalGateADO/assets/pipeline-run.png differ diff --git a/testing/evaluation/EvalGateADO/assets/pipeline-setup.png b/testing/evaluation/EvalGateADO/assets/pipeline-setup.png new file mode 100644 index 00000000..1c504785 Binary files /dev/null and b/testing/evaluation/EvalGateADO/assets/pipeline-setup.png differ diff --git a/testing/evaluation/EvalGateADO/assets/pr-gate-failed.png b/testing/evaluation/EvalGateADO/assets/pr-gate-failed.png new file mode 100644 index 00000000..f7eb9f74 Binary files /dev/null and b/testing/evaluation/EvalGateADO/assets/pr-gate-failed.png differ diff --git a/testing/evaluation/EvalGateADO/assets/pr-gate-passed.png b/testing/evaluation/EvalGateADO/assets/pr-gate-passed.png new file mode 100644 index 00000000..189ccd91 Binary files /dev/null and b/testing/evaluation/EvalGateADO/assets/pr-gate-passed.png differ diff --git a/testing/evaluation/EvalGateADO/assets/tests-tab.png b/testing/evaluation/EvalGateADO/assets/tests-tab.png new file mode 100644 index 00000000..954c3c96 Binary files /dev/null and b/testing/evaluation/EvalGateADO/assets/tests-tab.png differ diff --git a/testing/evaluation/EvalGateADO/eval-config.json b/testing/evaluation/EvalGateADO/eval-config.json new file mode 100644 index 00000000..14e5c2c4 --- /dev/null +++ b/testing/evaluation/EvalGateADO/eval-config.json @@ -0,0 +1,8 @@ +{ + "environmentId": "", + "environmentUrl": "https://.crm.dynamics.com/", + "botSchemaName": "", + "tenantId": "", + "clientId": "", + "passThreshold": 0.8 +} diff --git a/testing/evaluation/EvalGateADO/pipelines/eval-gate.yml b/testing/evaluation/EvalGateADO/pipelines/eval-gate.yml new file mode 100644 index 00000000..93760c53 --- /dev/null +++ b/testing/evaluation/EvalGateADO/pipelines/eval-gate.yml @@ -0,0 +1,223 @@ +# Copilot Studio Eval Gate Pipeline +# +# Runs Copilot Studio evaluations on every push to a PR targeting main. +# Used as a branch policy gate — PRs cannot merge unless eval pass rate +# meets the threshold defined in eval-config.json. +# +# Prerequisites: +# 1. Azure Key Vault with secrets (see README for full list) +# 2. ADO Service Connection (ARM) with Key Vault Secrets Officer role +# 3. eval-config.json populated with environment details and botSchemaName +# 4. Test set created in Copilot Studio Evaluate tab (travels with the solution) +# 5. Pipeline set to max 1 concurrent run (sequential execution) +# 6. First run: approve service connection permissions when prompted +# +# Agent options: +# - Self-hosted (macOS/Linux): Uses pac CLI directly (current config) +# - Microsoft-hosted (Windows/Linux): Use PowerPlatform Build Tools tasks instead +# (see commented-out section below) + +trigger: none + +# Run eval on every push to a PR targeting main +pr: + branches: + include: + - main + paths: + include: + - src/* + +# --------------------------------------------------------------------------- +# Option A: Self-hosted agent (uses pac CLI directly) +# --------------------------------------------------------------------------- +pool: 'self-hosted' + +# --------------------------------------------------------------------------- +# Option B: Microsoft-hosted agent (uncomment and use instead of Option A) +# --------------------------------------------------------------------------- +# pool: +# vmImage: 'ubuntu-latest' + +variables: + # Azure Resource Manager service connection for Key Vault access + AZURE_SERVICE_CONNECTION: '' + # Key Vault name storing refresh token, MCS connection ID, and CI Dev SPN secret + KEY_VAULT_NAME: '' + # CI Dev environment details (for pac CLI auth) + CI_DEV_ENV_URL: '' # e.g., https://org12345.crm.dynamics.com/ + CI_DEV_APP_ID: '' + CI_DEV_TENANT_ID: '' + +steps: + - checkout: self + + - task: NodeTool@0 + displayName: 'Install Node.js' + inputs: + versionSpec: '18.x' + + - script: cd $(Build.SourcesDirectory)/scripts && npm install + displayName: 'Install dependencies' + + # Fetch all secrets from Key Vault + - task: AzureKeyVault@2 + displayName: 'Fetch secrets from Key Vault' + inputs: + azureSubscription: '$(AZURE_SERVICE_CONNECTION)' + KeyVaultName: '$(KEY_VAULT_NAME)' + SecretsFilter: 'copilot-studio-eval-refresh-token,copilot-studio-eval-mcs-connection-id,copilot-studio-ci-dev-client-secret' + RunAsPreJob: false + + # --------------------------------------------------------------------------- + # Option A: Self-hosted agent — pack, import, and resolve via pac CLI + # --------------------------------------------------------------------------- + - script: | + set -euo pipefail + set +x # Prevent secrets from appearing in debug/trace logs + + export DOTNET_ROOT="/opt/homebrew/opt/dotnet/libexec" + PAC="$HOME/.dotnet/tools/pac" + + # Auth to CI Dev with SPN (secret via env var, not CLI arg) + $PAC auth create \ + --environment "$(CI_DEV_ENV_URL)" \ + --applicationId "$(CI_DEV_APP_ID)" \ + --clientSecret "$SPN_CLIENT_SECRET" \ + --tenant "$(CI_DEV_TENANT_ID)" + + # Pack unmanaged solution + echo "Packing solution from src/..." + $PAC solution pack \ + --zipfile "$(Build.ArtifactStagingDirectory)/solution_unmanaged.zip" \ + --folder "$(Build.SourcesDirectory)/src" + + # Import into CI Dev + echo "Importing solution into CI Dev..." + $PAC solution import \ + --path "$(Build.ArtifactStagingDirectory)/solution_unmanaged.zip" \ + --environment "$(CI_DEV_ENV_URL)" \ + --async + + # Resolve bot ID via Dataverse OData query using SPN client credentials + BOT_SCHEMA_NAME=$(python3 -c "import json; print(json.load(open('eval-config.json'))['botSchemaName'])") + ENV_URL=$(python3 -c "import json; print(json.load(open('eval-config.json'))['environmentUrl'])") + + echo "Resolving bot ID for: $BOT_SCHEMA_NAME in $ENV_URL" + + TOKEN=$(curl -sf --no-progress-meter -X POST \ + "https://login.microsoftonline.com/${SPN_TENANT_ID}/oauth2/v2.0/token" \ + -d "client_id=${SPN_APP_ID}" \ + -d "client_secret=${SPN_CLIENT_SECRET}" \ + -d "scope=${ENV_URL}.default" \ + -d "grant_type=client_credentials" 2>/dev/null \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])") + + BOT_ID=$(curl -sf "${ENV_URL}api/data/v9.2/bots?\$filter=schemaname%20eq%20'${BOT_SCHEMA_NAME}'&\$select=botid" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/json" \ + -H "OData-MaxVersion: 4.0" \ + -H "OData-Version: 4.0" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['value'][0]['botid'])") + + echo "Resolved bot ID: $BOT_ID" + echo "##vso[task.setvariable variable=RESOLVED_BOT_ID]$BOT_ID" + displayName: 'Pack, import, and resolve bot ID' + env: + SPN_TENANT_ID: $(CI_DEV_TENANT_ID) + SPN_APP_ID: $(CI_DEV_APP_ID) + SPN_CLIENT_SECRET: $(copilot-studio-ci-dev-client-secret) + + # --------------------------------------------------------------------------- + # Option B: Microsoft-hosted agent — use Power Platform Build Tools instead + # (uncomment this section and comment out Option A above) + # --------------------------------------------------------------------------- + # - task: PowerPlatformToolInstaller@2 + # displayName: 'Install Power Platform CLI' + # + # - task: PowerPlatformPackSolution@2 + # displayName: 'Pack unmanaged solution' + # inputs: + # SolutionSourceFolder: '$(Build.SourcesDirectory)/src' + # SolutionOutputFile: '$(Build.ArtifactStagingDirectory)/solution_unmanaged.zip' + # SolutionType: 'Unmanaged' + # + # - task: PowerPlatformImportSolution@2 + # displayName: 'Import into CI Dev' + # inputs: + # authenticationType: 'PowerPlatformSPN' + # PowerPlatformSPN: '' + # SolutionInputFile: '$(Build.ArtifactStagingDirectory)/solution_unmanaged.zip' + # AsyncOperation: true + # MaxAsyncWaitTime: 60 + # + # # Note: You still need to resolve the bot ID via Dataverse query or hardcode it. + # # The Build Tools tasks don't resolve bot IDs automatically. + + # Run the eval gate + - script: | + set -euo pipefail + set +x + + TOKEN_OUTPUT="$(Agent.TempDirectory)/updated-refresh-token" + + MCS_FLAG="" + if [ -n "${MCS_CONNECTION_ID:-}" ]; then + MCS_FLAG="--mcs-connection-id $MCS_CONNECTION_ID" + fi + + node scripts/eval-gate.mjs run \ + --config eval-config.json \ + --agent-id "$(RESOLVED_BOT_ID)" \ + $MCS_FLAG \ + --run-name "PR $(System.PullRequest.PullRequestNumber) @ $(Build.SourceVersion)" \ + --output "$(Build.ArtifactStagingDirectory)/eval-results.json" \ + --junit-output "$(Build.ArtifactStagingDirectory)/eval-results.junit.xml" \ + --token-output "$TOKEN_OUTPUT" + + echo "##vso[task.setvariable variable=TOKEN_OUTPUT_PATH]$TOKEN_OUTPUT" + displayName: 'Run Copilot Studio Eval' + env: + EVAL_REFRESH_TOKEN: $(copilot-studio-eval-refresh-token) + MCS_CONNECTION_ID: $(copilot-studio-eval-mcs-connection-id) + + # Publish test results to ADO Tests tab + - task: PublishTestResults@2 + displayName: 'Publish eval test results' + condition: always() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '$(Build.ArtifactStagingDirectory)/eval-results.junit.xml' + testRunTitle: 'Copilot Studio Eval' + failTaskOnFailedTests: false + + # Write updated refresh token back to Key Vault + - task: AzureCLI@2 + displayName: 'Update refresh token in Key Vault' + condition: always() + inputs: + azureSubscription: '$(AZURE_SERVICE_CONNECTION)' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + set +x + TOKEN_FILE="$(Agent.TempDirectory)/updated-refresh-token" + if [ -f "$TOKEN_FILE" ]; then + az keyvault secret set \ + --vault-name "$(KEY_VAULT_NAME)" \ + --name "copilot-studio-eval-refresh-token" \ + --value "$(cat "$TOKEN_FILE")" \ + --output none + rm -f "$TOKEN_FILE" + echo "Refresh token updated in Key Vault and temp file cleaned up" + else + echo "No updated refresh token found — skipping" + fi + + # Publish eval results as artifact + - task: PublishBuildArtifacts@1 + displayName: 'Publish eval results' + condition: always() + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'eval-$(Build.SourceVersion)' diff --git a/testing/evaluation/EvalGateADO/scripts/eval-gate.mjs b/testing/evaluation/EvalGateADO/scripts/eval-gate.mjs new file mode 100644 index 00000000..da7bf506 --- /dev/null +++ b/testing/evaluation/EvalGateADO/scripts/eval-gate.mjs @@ -0,0 +1,516 @@ +#!/usr/bin/env node + +/** + * eval-gate.mjs — Copilot Studio Evaluation API (PPAPI) client for CI/CD. + * + * Standalone script for running Copilot Studio evaluations as a pipeline gate. + * Uses a pre-cached MSAL refresh token (from Azure Key Vault) for unattended auth. + * + * Subcommands: + * node eval-gate.mjs run Start eval, poll, check results, exit 0/1 + * node eval-gate.mjs list-testsets List available test sets + * node eval-gate.mjs resolve-bot Resolve bot GUID from schema name in the target environment + * node eval-gate.mjs auth Interactive login (one-time setup) + * + * Options: + * --config Path to eval-config.json (default: ./eval-config.json) + * --environment-id Override environment ID from config + * --environment-url Override environment URL from config + * --agent-id Override agent ID from config (or auto-resolved from botSchemaName) + * --bot-schema-name Bot schema name (e.g., cr981_hotelfinder) — used to resolve agent ID dynamically + * --tenant-id Override tenant ID from config + * --client-id Override app registration client ID from config + * --testset-id Override test set ID from config + * --testset-name Match test set by display name + * --mcs-connection-id MCS connection ID for authenticated eval (connector actions, SharePoint, etc.) + * --threshold <0.0-1.0> Override pass threshold from config + * --run-name Display name for the eval run + * --output Write results JSON to this file + * --junit-output Write JUnit XML to this file (for ADO Tests tab) + * --token-output Write updated refresh token to this file + * + * Environment variables: + * EVAL_REFRESH_TOKEN Pre-cached MSAL refresh token (from Key Vault) + */ + +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { PublicClientApplication } from "@azure/msal-node"; + +// --------------------------------------------------------------------------- +// CLI parsing +// --------------------------------------------------------------------------- + +function parseArgs() { + const args = process.argv.slice(2); + const parsed = { command: null, config: "./eval-config.json" }; + + for (let i = 0; i < args.length; i++) { + if (args[i].startsWith("--")) { + const key = args[i].slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + parsed[key] = args[++i]; + } else if (!parsed.command) { + parsed.command = args[i]; + } + } + + if (!parsed.command) { + die("Usage: eval-gate.mjs [options]"); + } + + return parsed; +} + +function loadConfig(args) { + let config = {}; + if (existsSync(args.config)) { + config = JSON.parse(readFileSync(args.config, "utf8")); + log(`Loaded config from ${args.config}`); + } + + // CLI flags override config file + return { + environmentId: args.environmentId || config.environmentId, + environmentUrl: args.environmentUrl || config.environmentUrl, + agentId: args.agentId || config.agentId, + botSchemaName: args.botSchemaName || config.botSchemaName, + tenantId: args.tenantId || config.tenantId, + clientId: args.clientId || config.clientId, + testSetId: args.testsetId || config.testSetId, + testSetName: args.testsetName || config.testSetName, + mcsConnectionId: args.mcsConnectionId || config.mcsConnectionId, + passThreshold: parseFloat(args.threshold || config.passThreshold || "0.8"), + runName: args.runName || `Eval ${new Date().toISOString()}`, + output: args.output, + junitOutput: args.junitOutput, + tokenOutput: args.tokenOutput, + }; +} + +function validate(config, ...required) { + for (const key of required) { + if (!config[key]) die(`Missing required config: ${key}. Set in eval-config.json or --${key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}`); + } +} + +// --------------------------------------------------------------------------- +// Logging +// --------------------------------------------------------------------------- + +function log(msg) { console.error(`[eval-gate] ${msg}`); } +function die(msg) { console.error(`[eval-gate] ERROR: ${msg}`); process.exit(1); } + +// --------------------------------------------------------------------------- +// Auth — MSAL refresh token flow +// --------------------------------------------------------------------------- + +const PP_API_SCOPE = "https://api.powerplatform.com/.default"; + +async function getToken(config) { + const refreshToken = process.env.EVAL_REFRESH_TOKEN; + if (!refreshToken) { + die("EVAL_REFRESH_TOKEN environment variable is not set. Run 'eval-gate.mjs auth' first, or inject from Key Vault."); + } + + const pca = new PublicClientApplication({ + auth: { + clientId: config.clientId, + authority: `https://login.microsoftonline.com/${config.tenantId}`, + }, + }); + + try { + const result = await pca.acquireTokenByRefreshToken({ + refreshToken, + scopes: [PP_API_SCOPE], + }); + + log(`Token acquired (expires ${result.expiresOn.toISOString()})`); + + // Write updated refresh token if available (restrictive permissions) + if (config.tokenOutput && result.refreshToken) { + writeFileSync(config.tokenOutput, result.refreshToken, { encoding: "utf8", mode: 0o600 }); + log(`Updated refresh token written to ${config.tokenOutput}`); + } + + return result.accessToken; + } catch (err) { + die(`Token acquisition failed: ${err.message}\nRefresh token may be expired. Re-run 'eval-gate.mjs auth' to get a new one.`); + } +} + +async function interactiveAuth(config) { + validate(config, "tenantId", "clientId"); + + const pca = new PublicClientApplication({ + auth: { + clientId: config.clientId, + authority: `https://login.microsoftonline.com/${config.tenantId}`, + }, + }); + + log("Starting device code flow..."); + await pca.acquireTokenByDeviceCode({ + scopes: [PP_API_SCOPE], + deviceCodeCallback: (response) => { + console.error("\n" + response.message + "\n"); + }, + }); + + // MSAL doesn't expose refresh tokens on the result object. + // Read from the in-memory cache instead. + const serialized = pca.getTokenCache().serialize(); + const parsed = JSON.parse(serialized); + const rtKeys = Object.keys(parsed.RefreshToken || {}); + + if (rtKeys.length === 0) { + die("No refresh token in cache. Ensure the app registration has 'Allow public client flows' set to Yes."); + } + + const refreshToken = parsed.RefreshToken[rtKeys[0]].secret; + console.error("\nAuthentication successful!\n"); + console.log(refreshToken); + console.error(`\nStore it in Key Vault with:`); + console.error(` az keyvault secret set --vault-name --name copilot-studio-eval-refresh-token --value ""\n`); + console.error(`Or pipe directly:`); + console.error(` node eval-gate.mjs auth --config eval-config.json | az keyvault secret set --vault-name --name copilot-studio-eval-refresh-token --value @-\n`); +} + +// --------------------------------------------------------------------------- +// PPAPI HTTP client +// --------------------------------------------------------------------------- + +const API_VERSION = "2024-10-01"; + +function baseUrl(config) { + return `https://api.powerplatform.com/copilotstudio/environments/${config.environmentId}/bots/${config.agentId}/api/makerevaluation`; +} + +async function ppApi(method, url, accessToken, body) { + const opts = { + method, + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + signal: AbortSignal.timeout(30_000), + }; + if (body && method !== "GET") opts.body = JSON.stringify(body); + + const sep = url.includes("?") ? "&" : "?"; + const fullUrl = `${url}${sep}api-version=${API_VERSION}`; + + const res = await fetch(fullUrl, opts); + + if (!res.ok) { + const errBody = await res.text().catch(() => ""); + if (res.status === 401 || res.status === 403) die(`Auth failed (${res.status}). Re-run 'auth' to refresh token.\n${errBody.slice(0, 300)}`); + if (res.status === 409) die(`Conflict (409): An eval run is already in progress. Wait for it to complete.\n${errBody.slice(0, 300)}`); + if (res.status === 429) die(`Rate limited (429): Max 20 eval runs per bot per 24 hours.\n${errBody.slice(0, 300)}`); + die(`HTTP ${res.status}: ${errBody.slice(0, 500)}`); + } + + const text = await res.text(); + return text.trim() ? JSON.parse(text) : null; +} + +// --------------------------------------------------------------------------- +// Bot ID resolution — queries Dataverse for the bot GUID by schema name +// --------------------------------------------------------------------------- + +async function resolveBotId(config, accessToken) { + if (config.agentId && !config.agentId.startsWith("<")) return config.agentId; + + if (!config.botSchemaName || !config.environmentUrl) { + die("To auto-resolve bot ID, set both botSchemaName and environmentUrl in eval-config.json.\nAlternatively, set agentId directly via --agent-id."); + } + + validate(config, "environmentId"); + log(`Resolving bot ID for schema name: ${config.botSchemaName}`); + + // Query Dataverse OData API — requires a Dataverse-scoped token + const dvScope = `${config.environmentUrl}.default`; + const pca = new PublicClientApplication({ + auth: { + clientId: config.clientId, + authority: `https://login.microsoftonline.com/${config.tenantId}`, + }, + }); + + let dvToken; + try { + const result = await pca.acquireTokenByRefreshToken({ + refreshToken: process.env.EVAL_REFRESH_TOKEN, + scopes: [dvScope], + }); + dvToken = result.accessToken; + } catch (err) { + die(`Cannot get Dataverse token for bot resolution: ${err.message}\nEnsure your app registration has Dynamics CRM > user_impersonation permission.\nOr set agentId directly via --agent-id.`); + } + + const schemaName = config.botSchemaName.replace(/'/g, "''"); + const odataFilter = encodeURIComponent(`schemaname eq '${schemaName}'`); + const odataUrl = `${config.environmentUrl}api/data/v9.2/bots?$filter=${odataFilter}&$select=botid,name,schemaname`; + const res = await fetch(odataUrl, { + headers: { + Authorization: `Bearer ${dvToken}`, + Accept: "application/json", + "OData-MaxVersion": "4.0", + "OData-Version": "4.0", + }, + signal: AbortSignal.timeout(30_000), + }); + + if (!res.ok) { + const errBody = await res.text().catch(() => ""); + die(`Dataverse query failed (${res.status}): ${errBody.slice(0, 300)}\nSet agentId directly via --agent-id.`); + } + + const data = await res.json(); + const bots = data?.value || []; + + if (bots.length === 0) { + die(`Bot '${config.botSchemaName}' not found in environment. Ensure the solution has been imported.`); + } + + const bot = bots[0]; + log(`Resolved bot ID: ${bot.botid} (${bot.name})`); + config.agentId = bot.botid; + return bot.botid; +} + +async function resolveTestSetId(config, accessToken) { + if (config.testSetId && !config.testSetId.startsWith("<")) return config.testSetId; + + log("Resolving test set ID..."); + const data = await ppApi("GET", `${baseUrl(config)}/testsets`, accessToken); + const testsets = (data?.value || []).filter(ts => ts.state === "Ready" || ts.state === "Active"); + + if (testsets.length === 0) { + die("No test sets found for this bot. Create one in Copilot Studio (Evaluate tab)."); + } + + if (config.testSetName) { + const match = testsets.find(ts => ts.displayName === config.testSetName); + if (!match) { + const available = testsets.map(ts => ` ${ts.displayName} (${ts.id})`).join("\n"); + die(`Test set '${config.testSetName}' not found.\nAvailable:\n${available}`); + } + log(`Resolved test set: ${match.displayName} (${match.id}, ${match.totalTestCases} cases)`); + config.testSetId = match.id; + return match.id; + } + + const ts = testsets[0]; + log(`Using test set: ${ts.displayName} (${ts.id}, ${ts.totalTestCases} cases)`); + config.testSetId = ts.id; + return ts.id; +} + +// --------------------------------------------------------------------------- +// Subcommands +// --------------------------------------------------------------------------- + +async function cmdResolvBot(config) { + validate(config, "environmentId", "tenantId", "clientId"); + const token = await getToken(config); + const botId = await resolveBotId(config, token); + console.log(JSON.stringify({ botId, botSchemaName: config.botSchemaName }, null, 2)); +} + +async function cmdListTestsets(config) { + validate(config, "environmentId", "tenantId", "clientId"); + const token = await getToken(config); + if (!config.agentId || config.agentId.startsWith("<")) await resolveBotId(config, token); + const data = await ppApi("GET", `${baseUrl(config)}/testsets`, token); + const testsets = data?.value || []; + + console.log(JSON.stringify({ testsets: testsets.map(ts => ({ + id: ts.id, + displayName: ts.displayName, + totalTestCases: ts.totalTestCases, + state: ts.state, + }))}, null, 2)); +} + +async function cmdRun(config) { + validate(config, "environmentId", "tenantId", "clientId"); + const token = await getToken(config); + if (!config.agentId || config.agentId.startsWith("<")) await resolveBotId(config, token); + if (!config.testSetId || config.testSetId.startsWith("<")) await resolveTestSetId(config, token); + + // Step 1: Start eval run + log(`Starting eval run: "${config.runName}"`); + log(`Test set: ${config.testSetId} | Threshold: ${(config.passThreshold * 100).toFixed(0)}%`); + + const runBody = { + runOnPublishedBot: false, + evaluationRunName: config.runName, + }; + if (config.mcsConnectionId) { + runBody.mcsConnectionId = config.mcsConnectionId; + log(`Using MCS connection: ${config.mcsConnectionId}`); + } else { + log("No mcsConnectionId — authenticated actions/knowledge sources won't work during eval"); + } + + const startData = await ppApi("POST", `${baseUrl(config)}/testsets/${config.testSetId}/run`, token, runBody); + + const runId = startData.runId; + log(`Run started: ${runId}`); + + // Step 2: Poll until complete (max 10 min, every 20s) + let state = "unknown"; + for (let i = 0; i < 30; i++) { + await sleep(20_000); + const status = await ppApi("GET", `${baseUrl(config)}/testruns/${runId}`, token); + state = status.state; + const processed = status.testCasesProcessed || 0; + const total = status.totalTestCases || 0; + log(`Progress: ${processed}/${total} (${state})`); + + if (["Completed", "Failed", "Cancelled", "Abandoned"].includes(state)) break; + } + + if (state !== "Completed") { + die(`Eval run did not complete (state: ${state})`); + } + + // Step 3: Fetch results + const results = await ppApi("GET", `${baseUrl(config)}/testruns/${runId}`, token); + + if (config.output) { + writeFileSync(config.output, JSON.stringify(results, null, 2), "utf8"); + log(`Results written to ${config.output}`); + } + + // Step 4: Evaluate pass/fail + const testCases = results.testCasesResults || []; + const total = testCases.length; + + function getOverallStatus(tc) { + const metrics = tc.metricsResults || []; + if (metrics.length === 0) return "Error"; + const allPass = metrics.every(m => m.result?.status === "Pass"); + const anyError = metrics.some(m => m.result?.status === "Error"); + if (allPass) return "Pass"; + if (anyError) return "Error"; + return "Fail"; + } + + const passed = testCases.filter(tc => getOverallStatus(tc) === "Pass").length; + const failed = total - passed; + const passRate = total > 0 ? passed / total : 0; + + console.log(""); + console.log("========================================="); + console.log(` EVAL RESULTS: ${passed}/${total} passed (${(passRate * 100).toFixed(1)}%)`); + console.log(` Threshold: ${(config.passThreshold * 100).toFixed(0)}%`); + console.log(` Verdict: ${passRate >= config.passThreshold ? "PASS" : "FAIL"}`); + console.log("========================================="); + + if (failed > 0) { + console.log("\nFailed test cases:"); + for (const tc of testCases.filter(t => getOverallStatus(t) !== "Pass")) { + const failedMetrics = (tc.metricsResults || []) + .filter(m => m.result?.status !== "Pass") + .map(m => `${m.type}: ${m.result?.status} (${Object.entries(m.result?.data || {}).filter(([,v]) => v === "No").map(([k]) => k).join(", ") || m.result?.errorReason || "unknown"})`) + .join("; "); + console.log(` - ${tc.testCaseId}: ${getOverallStatus(tc)} [${failedMetrics}]`); + } + } + + // Calculate run duration + const startTime = results.startTime ? new Date(results.startTime) : null; + const endTime = results.endTime ? new Date(results.endTime) : null; + const durationSec = (startTime && endTime) ? ((endTime - startTime) / 1000).toFixed(1) : "0"; + + // Write JUnit XML for ADO Tests tab + if (config.junitOutput) { + const escXml = (s) => String(s || "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + + function metricSummary(tc) { + return (tc.metricsResults || []).map(m => { + const data = m.result?.data || {}; + const dims = Object.entries(data).map(([k, v]) => `${k}: ${v}`).join(", "); + return `[${m.type}] status=${m.result?.status} | ${dims}${m.result?.errorReason ? ` | error: ${m.result.errorReason}` : ""}${m.result?.aiResultReason ? `\nAI reason: ${m.result.aiResultReason}` : ""}`; + }).join("\n"); + } + + const perTestDuration = total > 0 ? (parseFloat(durationSec) / total).toFixed(1) : "0"; + + const tcXml = testCases.map(tc => { + const name = escXml(tc.testCaseId); + const status = getOverallStatus(tc); + const details = escXml(metricSummary(tc)); + + if (status === "Pass") { + return ` \n ${details}\n `; + } + + const failedMetrics = (tc.metricsResults || []) + .filter(m => m.result?.status !== "Pass") + .map(m => { + const failedDims = Object.entries(m.result?.data || {}).filter(([,v]) => v === "No").map(([k]) => k).join(", "); + return `${m.type}: ${m.result?.status} (${failedDims || m.result?.errorReason || "unknown"})`; + }).join("; "); + + return ` \n ${details}\n `; + }).join("\n"); + + const xml = ` + + +${tcXml} + +`; + + writeFileSync(config.junitOutput, xml, "utf8"); + log(`JUnit XML written to ${config.junitOutput}`); + } + + // Output summary as JSON + console.log("\n" + JSON.stringify({ + runId, + runName: config.runName, + total, + passed, + failed, + passRate, + threshold: config.passThreshold, + verdict: passRate >= config.passThreshold ? "PASS" : "FAIL", + }, null, 2)); + + if (passRate < config.passThreshold) { + process.exit(1); + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +const args = parseArgs(); +const config = loadConfig(args); + +switch (args.command) { + case "run": + await cmdRun(config); + break; + case "list-testsets": + await cmdListTestsets(config); + break; + case "resolve-bot": + await cmdResolvBot(config); + break; + case "auth": + await interactiveAuth(config); + break; + default: + die(`Unknown command: ${args.command}\nCommands: run, list-testsets, resolve-bot, auth`); +} diff --git a/testing/evaluation/EvalGateADO/scripts/package.json b/testing/evaluation/EvalGateADO/scripts/package.json new file mode 100644 index 00000000..5e4ddc5f --- /dev/null +++ b/testing/evaluation/EvalGateADO/scripts/package.json @@ -0,0 +1,9 @@ +{ + "name": "eval-gate", + "version": "1.0.0", + "type": "module", + "description": "Copilot Studio Evaluation API client for CI/CD pipelines", + "dependencies": { + "@azure/msal-node": "^2.0.0" + } +} diff --git a/testing/evaluation/README.md b/testing/evaluation/README.md new file mode 100644 index 00000000..22fd08e9 --- /dev/null +++ b/testing/evaluation/README.md @@ -0,0 +1,16 @@ +--- +title: Evaluation +parent: Testing +nav_order: 2 +has_children: true +has_toc: false +--- +# Evaluation + +Automated evaluation samples for Copilot Studio agents using the [Evaluation REST API](https://learn.microsoft.com/en-us/microsoft-copilot-studio/analytics-agent-evaluation-rest-api). + +## Contents + +| Sample | Description | +|--------|-------------| +| [EvalGateADO/](./EvalGateADO/) | Azure DevOps pipeline that uses the Copilot Studio Evaluation API as a PR quality gate | diff --git a/ui/embed/sharepoint-customizer/.eslintrc.js b/ui/embed/sharepoint-customizer/.eslintrc.js index f6e5edb0..562b8f68 100644 --- a/ui/embed/sharepoint-customizer/.eslintrc.js +++ b/ui/embed/sharepoint-customizer/.eslintrc.js @@ -12,48 +12,14 @@ module.exports = { 'sourceType': 'module' }, rules: { - '@typescript-eslint/no-empty-function': 0, // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin - '@rushstack/no-new-null': 0, + '@rushstack/no-new-null': 1, // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin '@rushstack/hoist-jest-mock': 1, // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security '@rushstack/security/no-unsafe-regexp': 1, // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json '@typescript-eslint/adjacent-overload-signatures': 1, - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - // - // CONFIGURATION: By default, these are banned: String, Boolean, Number, Object, Symbol - '@typescript-eslint/ban-types': [ - 1, - { - 'extendDefaults': false, - 'types': { - 'String': { - 'message': 'Use \'string\' instead', - 'fixWith': 'string' - }, - 'Boolean': { - 'message': 'Use \'boolean\' instead', - 'fixWith': 'boolean' - }, - 'Number': { - 'message': 'Use \'number\' instead', - 'fixWith': 'number' - }, - 'Object': { - 'message': 'Use \'object\' instead, or else define a proper TypeScript type:' - }, - 'Symbol': { - 'message': 'Use \'symbol\' instead', - 'fixWith': 'symbol' - }, - 'Function': { - 'message': 'The \'Function\' type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with \'new\'.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape.' - } - } - } - ], // RATIONALE: Code is more readable when the type of every variable is immediately obvious. // Even if the compiler may be able to infer a type, this inference will be unavailable // to a person who is reviewing a GitHub diff. This rule makes writing code harder, @@ -61,7 +27,7 @@ module.exports = { // // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json '@typescript-eslint/explicit-function-return-type': [ - 0, + 1, { 'allowExpressions': true, 'allowTypedFunctionExpressions': true, @@ -80,7 +46,7 @@ module.exports = { // This rule should be suppressed only in very special cases such as JSON.stringify() // where the type really can be anything. Even if the type is flexible, another type // may be more appropriate such as "unknown", "{}", or "Record". - '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-explicit-any': 1, // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() // handler. Thus wherever a Promise arises, the code must either append a catch handler, // or else return the object to a caller (who assumes this responsibility). Unterminated @@ -324,7 +290,7 @@ module.exports = { // ==================================================================== '@microsoft/spfx/import-requires-chunk-name': 1, '@microsoft/spfx/no-require-ensure': 2, - '@microsoft/spfx/pair-react-dom-render-unmount': 0 + '@microsoft/spfx/pair-react-dom-render-unmount': 1 } }, { diff --git a/ui/embed/sharepoint-customizer/.vscode/settings.json b/ui/embed/sharepoint-customizer/.vscode/settings.json index a31a2c33..16141662 100644 --- a/ui/embed/sharepoint-customizer/.vscode/settings.json +++ b/ui/embed/sharepoint-customizer/.vscode/settings.json @@ -6,6 +6,7 @@ "**/.DS_Store": true, "**/bower_components": true, "**/coverage": true, + "**/jest-output": true, "**/lib-amd": true, "src/**/*.scss.ts": true }, diff --git a/ui/embed/sharepoint-customizer/.yo-rc.json b/ui/embed/sharepoint-customizer/.yo-rc.json index ffa4db88..571035ec 100644 --- a/ui/embed/sharepoint-customizer/.yo-rc.json +++ b/ui/embed/sharepoint-customizer/.yo-rc.json @@ -2,18 +2,15 @@ "@microsoft/generator-sharepoint": { "plusBeta": false, "isCreatingSolution": true, - "nodeVersion": "16.20.2", - "sdksVersions": { - "@microsoft/microsoft-graph-client": "3.0.2", - "@microsoft/teams-js": "2.12.0" - }, - "version": "1.18.0", - "libraryName": "pva-extension-sso", - "libraryId": "14634225-91e5-41a4-b9cc-161ccb3400b4", + "nodeVersion": "22.17.0", + "sdksVersions": {}, + "version": "1.21.1", + "libraryName": "sidebar-agent", + "libraryId": "54cbba6a-2459-43a9-80a4-bc0675a81ac0", "environment": "spo", "packageManager": "npm", - "solutionName": "pva-extension-sso", - "solutionShortDescription": "pva-extension-sso description", + "solutionName": "sidebar-agent", + "solutionShortDescription": "sidebar-agent description", "skipFeatureDeployment": true, "isDomainIsolated": false, "componentType": "extension", diff --git a/ui/embed/sharepoint-customizer/Configure-McsForSite.ps1 b/ui/embed/sharepoint-customizer/Configure-McsForSite.ps1 deleted file mode 100644 index 2f3d1eb1..00000000 --- a/ui/embed/sharepoint-customizer/Configure-McsForSite.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -param ( - [Parameter(Mandatory=$true)] - [string]$siteUrl, - - [Parameter(Mandatory=$true)] - [string]$botUrl, - - [Parameter(Mandatory=$true)] - [string]$botName, - - [Parameter(Mandatory=$true)] - [string]$customScope, - - [Parameter(Mandatory=$true)] - [string]$clientId, - - [Parameter(Mandatory=$true)] - [string]$authority, - - [Parameter(Mandatory=$true)] - [string]$buttonLabel, - - [Parameter(Mandatory=$true)] - [switch]$greet -) - -Connect-PnPOnline -Url $siteUrl -Interactive -$action = (Get-PnPCustomAction | Where-Object { $_.Title -eq "PvaSso" })[0] - -$action.ClientSideComponentProperties = @{ - "botURL" = $botUrl - "customScope" = $customScope - "clientID" = $clientId - "authority" = $authority - "greet" = $greet.isPresent - "buttonLabel" = $buttonLabel - "botName" = $botName -} | ConvertTo-Json -Compress - -$action.Update() -Invoke-PnPQuery \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/README.md b/ui/embed/sharepoint-customizer/README.md index f631688d..ef3c2346 100644 --- a/ui/embed/sharepoint-customizer/README.md +++ b/ui/embed/sharepoint-customizer/README.md @@ -1,39 +1,253 @@ --- -title: SharePoint Customizer +title: SharePoint Sidepanel parent: Embed grand_parent: UI nav_order: 6 --- -# SharePoint SSO Component +# Copilot Studio Agent Side Panel for SharePoint +![Copilot Studio Agent in SharePoint](./images/agent%20sidepanel.png) -This code sample demonstrates how to create a SharePoint SPFx component which is a wrapper for a copilot, created with Microsoft Copilot Studio. The SPFx component included in the sample supports SSO, providing seamless authentication for users interacting with the copilot. +## Summary + +This sample demonstrates how to integrate a Microsoft Copilot Studio agent into SharePoint using a SharePoint Framework (SPFx) Application Customizer. It leverages the ReactWebChat component with the [Fluent UI theme pack](https://github.com/microsoft/BotFramework-WebChat#experimental-fluent-ui-theme-pack) and the [Microsoft 365 Agents SDK for NodeJS/TypeScript](https://github.com/microsoft/Agents-for-js) to establish a secure connection with a Copilot Studio agent configured with ['Authenticate with Microsoft'](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft). + +The key advantage of using the Microsoft 365 Agents SDK is its ability to connect to agents that support Microsoft authentication, which enables [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) capabilities. {: .important } -> ⚠️ **This sample has been superseded.** -> Please use the updated sample here: [SharePoint SSO App Customizer](./SharePointSSOAppCustomizer/). -> -> The new sample: -> - Uses a **modern UI** -> - Leverages the **M365 Agents SDK**, which supports **tenant Graph grounding** -> - Requires **less complex setup** +> ⚠️ **This is an open-source reference sample only.** +> For production deployments, we strongly recommend using the [built-in SharePoint channel in Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/publication-add-bot-to-sharepoint#configure-the-sharepoint-channel). + +## Features of this sample + +- **Single Sign-On (SSO)**: Leverages the current user's SharePoint authentication for seamless agent access +- **Sliding Panel UI**: Modern, responsive chat interface that slides in from the right side of the page +- **Customizable Appearance**: Configure header color and agent title to match your branding +- **New Conversation Support**: Users can start fresh conversations without leaving the page +- **Fluent UI Styling**: Uses the Fluent UI theme pack for a native Microsoft 365 look and feel +- **Tenant Graph Grounding**: Enables the agent to access and search Microsoft 365 content through [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) + +## Prerequisites + +Before you begin, ensure you have the following: + +1. **Published Copilot Studio Agent** + - Agent must be configured with ['Authenticate with Microsoft'](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft) authentication + - Agent should be published and accessible + - Consider enabling [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) for access to Microsoft 365 content + +2. **Azure Portal Access** + - Required for creating and configuring app registrations + - Permissions to create new app registrations in your Azure AD tenant + +3. **SharePoint Administrator Access** + - Required to deploy the SPFx extension to the app catalog + +4. **Development Environment** + - Node.js v22 (as per SPFx 1.21.1 requirements) + - npm or yarn package manager + - Git for cloning the repository + +## Technical Architecture + +This solution uses: +- **[Microsoft 365 Agents SDK for NodeJS/TypeScript](https://github.com/microsoft/Agents-for-js)**: Provides secure connection and authentication handling for Copilot Studio agents +- **[BotFramework-WebChat with Fluent UI Theme](https://github.com/microsoft/BotFramework-WebChat#experimental-fluent-ui-theme-pack)**: Renders the chat interface with Microsoft 365 styling +- **SharePoint Framework**: Enables deployment as a site-wide extension +- **[Tenant Graph Grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding)**: Allows the agent to search and access Microsoft 365 content + +## Setup Instructions + +### Step 1: Configure App Registration + +This step requires permissions to create application identities in your Azure tenant. For this sample, create a Native Client Application Identity (no secrets required): + +1. **Open Azure Portal** + - Navigate to [portal.azure.com](https://portal.azure.com) + - Go to **Azure Active Directory** (Entra ID) + +2. **Create a new App Registration** + - Click **App registrations** → **New registration** + - Provide a **Name** (e.g., "SharePoint Copilot Studio Agent") + - Choose **"Accounts in this organization directory only"** + - Under **Redirect URI**: + - Select **"Single-page application"** from the platform dropdown + - Enter your first SharePoint site URL without the trailing slash (e.g., `https://contoso.sharepoint.com/sites/mysite`) + - Click **Register** + +3. **Configure Authentication - Add SharePoint URLs** + - Go to **Authentication** in the Manage section + - Under **Single-page application**, add redirect URIs: + - `https://localhost:4321` (only if you plan to test locally - this is the default SPFx port) + - For each SharePoint site where you'll deploy the extension, add: + - The site URL **without** trailing slash (e.g., `https://contoso.sharepoint.com/sites/mysite`) + - Under **Implicit grant and hybrid flows**, ensure both checkboxes are **unchecked** (SPAs use PKCE flow) + - Click **Save** + + **Example for multiple sites:** + ``` + https://localhost:4321 (for local testing - default SPFx port) + https://contoso.sharepoint.com/sites/hr + https://contoso.sharepoint.com/sites/finance + https://contoso.sharepoint.com/sites/it + ``` + +4. **Configure API Permissions** + - In your new application, go to **API Permissions** in the Manage section + - Click **Add Permission** + - In the side panel, click the **APIs my organization uses** tab + - Search for **"Power Platform API"** or use the GUID `8578e004-a5c6-46e7-913e-12f58912df43` + - Under **Delegated permissions**, expand **CopilotStudio** and check: + - `CopilotStudio.Copilots.Invoke` + - Click **Add Permissions** + - (Optional) Click **Grant admin consent** for your organization + +5. **Note Required Values** + - On the **Overview** page, copy and save: + - **Application (client) ID** (e.g., `12345678-1234-1234-1234-123456789012`) + - **Directory (tenant) ID** (e.g., `87654321-4321-4321-4321-210987654321`) + +{: .note } +> If you don't see "Power Platform API" in the list, you need to add it to your tenant first. See [Power Platform API Authentication](https://learn.microsoft.com/en-us/power-platform/admin/programmability-authentication) and follow Step 2 to add the API. + +### Step 2: Get Copilot Studio Configuration + +1. Open your Copilot Studio environment +2. Navigate to your published agent +3. Ensure the agent is configured with ['Authenticate with Microsoft'](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft) +4. Optionally enable [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) for Microsoft 365 content access +5. Collect the required configuration values using one of these methods: + + **Option A: Get Direct Connect URL (Recommended)** + - Go to **Channels** → **Web app** + - Under **Microsoft 365 Agents SDK**, copy the **Connection string** + - This will look like: `https://xxxxxxx.07.environment.api.powerplatform.com/...` + + **Option B: Get Environment ID and Agent Identifier** + - Go to **Settings** → **Advanced** → **Metadata** + - Copy the following values: + - **Environment ID** + - **Schema name** (this is your Agent Identifier, e.g., `cr770_myAgent`) + +### Step 3: Clone the Repository + +```bash +git clone https://github.com/microsoft/CopilotStudioSamples.git +cd CopilotStudioSamples/ui/embed/sharepoint-customizer +``` + +### Step 4: Configure the Extension + +1. Open `sharepoint/assets/elements.xml` in your editor +2. Update the `ClientSideComponentProperties` with your values: + +```xml +ClientSideComponentProperties="{ + "appClientId": "YOUR_APP_CLIENT_ID", + "tenantId": "YOUR_TENANT_ID", + "environmentId": "YOUR_ENVIRONMENT_ID", + "agentIdentifier": "YOUR_AGENT_IDENTIFIER", + "directConnectUrl": "YOUR_DIRECT_CONNECT_URL", + "showTyping": true, + "headerBackgroundColor": "white", + "agentTitle": "Copilot Studio Agent" +}" +``` + +**Configuration Options:** + +| Property | Required | Description | Default | +|----------|----------|-------------|---------| +| `appClientId` | Yes | Your Azure AD app registration client ID | - | +| `tenantId` | Yes | Your Azure AD tenant ID | - | +| `environmentId` | Conditional | Copilot Studio environment ID (required if not using directConnectUrl) | - | +| `agentIdentifier` | Conditional | Your agent's identifier (required if not using directConnectUrl) | - | +| `directConnectUrl` | Conditional | Direct connection URL (use this OR environmentId + agentIdentifier) | - | +| `showTyping` | No | Show typing indicators | `true` | +| `headerBackgroundColor` | No | Header bar color (accepts any CSS color value) | `white` | +| `agentTitle` | No | Display title for the agent | `"Copilot Studio Agent"` | + +{: .note } +> You must provide either `directConnectUrl` OR both `environmentId` and `agentIdentifier`. + +### Step 5: Test Locally (Optional) + +Before building and deploying, you can test the extension locally: + +```bash +# Install dependencies +npm install + +# Check if gulp is installed, if not, install it +which gulp || npm install -g gulp-cli +# On Windows, use: where gulp || npm install -g gulp-cli + +# Serve the solution locally +gulp serve --nobrowser +``` + +After running `gulp serve`, navigate to the following URL (replace the values with your configuration): + +{% raw %} +``` +https://YOUR-TENANT.sharepoint.com/sites/YOUR-SITE/SitePages/Home.aspx?debugManifestsFile=https://localhost:4321/temp/build/manifests.js&loadSPFX=true&customActions={%224c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2%22:{%22location%22:%22ClientSideExtension.ApplicationCustomizer%22,%22properties%22:{%22appClientId%22:%22YOUR_APP_CLIENT_ID%22,%22tenantId%22:%22YOUR_TENANT_ID%22,%22directConnectUrl%22:%22YOUR_DIRECT_CONNECT_URL%22,%22showTyping%22:true,%22headerBackgroundColor%22:%22white%22,%22agentTitle%22:%22Your%20Agent%20Title%22}}} +``` +{% endraw %} + +- Replace `YOUR-TENANT`, `YOUR-SITE`, `YOUR_APP_CLIENT_ID`, `YOUR_TENANT_ID`, and `YOUR_DIRECT_CONNECT_URL` with your actual values +- Ensure you've added `https://localhost:4321` to your app registration's redirect URIs if testing locally + +### Step 6: Build and Package + +Once you've tested locally (or if you're ready to deploy directly): + +```bash +# Build the solution +gulp build + +# Bundle the solution +gulp bundle --ship + +# Package the solution +gulp package-solution --ship +``` + +This will create a `.sppkg` file in the `sharepoint/solution` folder. + +### Step 7: Deploy to SharePoint + +Upload the `.sppkg` file from `sharepoint/solution` to your SharePoint App Catalog. For detailed instructions on creating an app catalog, deploying apps, and adding them to sites, see [Add custom apps to SharePoint](https://learn.microsoft.com/en-us/sharepoint/use-app-catalog#add-custom-apps). + +## Troubleshooting + +### "Unable to acquire token" error + +If you encounter this error: +1. Open your browser's Developer Tools (F12) +2. Go to the **Network** tab +3. Look for failed authentication requests (usually to `login.microsoftonline.com`) +4. Click on the failed requests to see detailed error messages +5. Follow the authentication URLs that MSAL is attempting - the actual error details are often revealed in the response or redirect parameters -## Getting Started +Common causes: +- Missing or incorrect redirect URIs in your app registration (check for trailing slashes!) +- Incorrect permissions in the app registration (ensure `CopilotStudio.Copilots.Invoke` is granted) +- Incorrect tenant ID or client ID +- MSAL authentication flow being blocked by browser settings -1. Create an app registration in Azure and configure authentication settings for your copilot in Copilot Studio -2. Create an app registration for your SharePoint site -3. Clone this repo and cd into the SharePointSSOComponent folder -4. Install the dependencies and build the component: +### "The redirect URI specified in the request does not match the redirect URIs configured for the application" error - ```shell - npm install - gulp bundle --ship - gulp package-solution --ship - ``` +Check that the current SharePoint URL has been added as an allowed redirect URI for the provided app registration. -5. Upload the component to your tenant app catalog and enable on your site +## Additional Resources -For more detailed instructions, please refer to the [step-by-step setup guide](./SETUP). +- [Built-in SharePoint Channel for Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/publication-add-bot-to-sharepoint#configure-the-sharepoint-channel) +- [Authenticate with Microsoft in Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft) +- [Tenant Graph Grounding in Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) +- [Microsoft 365 Agents SDK for NodeJS/TypeScript](https://github.com/microsoft/Agents-for-js) +- [BotFramework-WebChat with Fluent UI Theme](https://github.com/microsoft/BotFramework-WebChat#experimental-fluent-ui-theme-pack) +- [Copilot Studio Documentation](https://learn.microsoft.com/en-us/microsoft-copilot-studio/) +- [SharePoint Framework Documentation](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/sharepoint-framework-overview) -## The Deployed Component +## License -![Microsoft Copilot Studio SSO](./images/SharePointSSOComponent.png) +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/ui/embed/sharepoint-customizer/SETUP.md b/ui/embed/sharepoint-customizer/SETUP.md deleted file mode 100644 index 2b3410fd..00000000 --- a/ui/embed/sharepoint-customizer/SETUP.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -nav_exclude: true -search_exclude: false ---- -# Deploy a Microsoft Copilot Studio copilot as a SharePoint component with single sign-on (SSO) enabled. - -To follow through the end-to-end setup process, you would need to: - -1. Configure Microsoft Entra ID authentication for your copilot. -2. Register your SharePoint site as a canvas app – an application that will host your copilot and handle the single sign-on flow. -3. Build the SharePoint component and configure its properties based on values from step (2). -4. Upload the component to SharePoint and add the component to your site. - -## Step 1 - Configure Microsoft Entra ID authentication for your copilot - -This step can be completed mostly by following the instructions here: [Configure user authentication with Microsoft Entra ID](https://learn.microsoft.com/en-us/power-virtual-agents/configuration-authentication-azure-ad), with some added configuration which is specified below. - -1. **Optional – add scopes for SharePoint and OneDrive**. For your copilot to use the Generative Answers capability over a SharePoint or OneDrive data source, you would need to configure additional scopes for the API permissions assigned to your app. Please refer to [Generative answers with Search and summarize: Authentication](https://learn.microsoft.com/en-us/power-virtual-agents/nlu-boost-node#authentication). - - -

- API Permissions -
- API Permissions of the copilot app registration -

- - -2. **Mandatory – populate the token exchange URL in the copilot’s authentication settings.** Your copilot will send this URL to any custom application hosting it, instructing the custom application it should sign users in by acquiring a token matching this custom scope. The value for “token exchange URL” is the full URI for the custom scope you have added when configuring a custom API. - -

- Custom Scope -
- The custom scope for the copilot app registration -

-
-

- Authentication Settings -
- Authentication configuration of the copilot, including token exchange URL -

- - -Once all the steps under [Configure user authentication with Microsoft Entra ID](https://learn.microsoft.com/en-us/power-virtual-agents/configuration-authentication-azure-ad) have been completed and the optional additional scopes have been specified, you should be able to use Generative Answers over a SharePoint or OneDrive data source from the Microsoft Copilot Studio authoring experience. Please refer to [Use content on SharePoint or OneDrive for Business for generative answers](https://learn.microsoft.com/en-us/power-virtual-agents/nlu-generative-answers-sharepoint-onedrive) for instructions on add a SharePoint or OneDrive data source for your Copilot Generative Answers node. - -Before moving to Step 2, make sure the Copilot Studio authoring canvas can successfully sign you in. If "Require users to sign in" is selected in the authentication settings, the canvas will try to sign in you in as soon as the conversation starts. Otherwise, the-sign in topic will have to be triggered by a specific event in the conversation. In case Generative Answers is configured over SharePoint or OneDrive, please make sure your copilot responds to questions as expected. - -**Important:** For now, the copilot canvas will use a validation code to sign you in, but once the setup is complete, users will be signed-in seamlessly. - -## Step 2 - Register your SharePoint site as a custom canvas - -A custom canvas is a custom application that hosts your copilot. In our case, it is also the application that will be responsible for a seamless sign-in experience. - -In order to configure your SharePoint site as a canvas application with single sign-on enabled, follow the steps specified in [Configure single sign-on with Microsoft Entra ID](https://learn.microsoft.com/en-us/power-virtual-agents/configure-sso?tabs=webApp#create-app-registrations-for-your-custom-website). - -When configuring the canvas app registration, pay attention to the following details: - -1. When adding a platform to the canvas app registration, select “Single-page application” and not “Web”. Web redirect URIs only support the implicit grant flow for authentication, which is considered less secure and cannot be used with MSAL.js 2.x, which is the authentication library included in the code sample provided here. For a discussion about the differences between Web and SPA redirects, please refer to: [https://github.com/MicrosoftDocs/azure-docs/issues/70484#issuecomment-791077654](https://github.com/MicrosoftDocs/azure-docs/issues/70484#issuecomment-791077654) - -2. The redirect URI should be the same as the URL for your SharePoint site that will host the copilot. For example, if you plan to deploy the copilot on , set this as your redirect URI. - - **Important:** Users can reach your SharePoint site via addresses that include trailing slashes. Since redirect URIs are sensitive to this variation, consider creating two redirect URIs representing the same site, with and without a trailing slash (for example: and ) - -3. The canvas app registration will need permissions for the custom API that was configured in *Step 1*. To add this permission, select an API from “APIs my organization uses” and search for the name you have given your copilot app registration in *Step 1*. For example, if your copilot app registration is called “SharePoint Bot Authentication” search for that name in the list of APIs, and select your custom scope (a name for your custom scope has been selected while configuring a custom API for your copilot app registration) - -

- APIs my organization uses -
- The API can be found under “APIs my organization uses” -

-
-

- Permissions for the custom scope -
- Selecting the scope for the API -

- -4. After registering your canvas app, you will not have to use the code sample the page refers to. The code sample provided is a standalone web page implementing SSO for Microsoft Copilot Studio which can be used for testing purposes, but it is not a SharePoint component. - - However, you will need to document the Application (client) ID for the SharePoint component configuration in the next step. - -

- Document the Client ID -
- The Application (client) ID -

- - - -## Step 3 - Download and configure the SharePoint SPFx component - -At this point you have a choice whether to configure and build the component yourself, or use the pre-built package that is included with this sample. Since this is only a reference sample, we encourage you to build the component yourself, but if you choose to deploy the pre-built package, skip ahead to **step 4** - -1. Make sure your development environment includes the following tools and libraries: - 1. VS Code (or a similar code editor) - 2. A version of Node.JS which is [supported by the SPFx framework](https://learn.microsoft.com/en-us/sharepoint/dev/spfx/compatibility#spfx-development-environment-compatibility) (for this sample, use either v16 or v18) - 3. A [Git](https://git-scm.com/downloads) client for your OS -2. If the prerequisites above are satisfied, clone [CopilotStudioSamples (github.com)](https://github.com/microsoft/CopilotStudioSamples) into a local folder. - - In this repo, you will find the SharePointSSOComponent project, which is a code sample for a SharePoint SPFx component (an Application Customizer), which renders a copilot at the bottom of all pages on a specific site. This SPFx component uses the MSAL library to perform a silent login and shares the user’s token with Microsoft Copilot Studio, providing a seamless single sign-on experience. -3. Using Visual Studio Code, open the local folder to which you have cloned the repository. The folder structure should look like below: - -

- The project folder structure -
- The Project Folder Structure -

- - -1. Locate elements.xml under SharePointSSOComponent/sharepoint/assets, and update the values in the file, using one of the two following options: - - *Option 1*: run the following python script and provide values based values from Steps 1 & 2 - - ```python - python .\populate_elements_xml.py - ``` - - *Option 2*: manually replace placeholders in elements.xml with actual values. ClientSideComponentProperties accepts an escaped JSON string. - - ```xml - ClientSideComponentProperties="{"botURL":"YOUR_BOT_URL","customScope":"YOUR_CUSTOM_SCOPE","clientID":"YOU_CLIENT_ID","authority":"YOUR_AAD_LOGIN_URL","greet":TRUE,"buttonLabel":"CHAT_BUTTON_LABEL","botName":"BOT_NAME"}" - ``` - - - *Option 3*: leave elements.xml without changing any details, build and deploy the component on a site, and later configure the component by running [Configure-MCSForSite.ps1](./Configure-MCSForSite.ps1) (see instructions on how to run this script in step 4) - - - ### Property details - - |Property Name|Explanation|Mandatory?| - | :- | :- | :- | - |botURL|The token endpoint for MCS. This can be found in the MCS designer, under Settings -> Channels -> Mobile App|Yes| - |customScope|

The scope defined for the custom API in the copilot app registration (Step 1). For example:

api://35337616-eee1-4049-9d37-a78b24c3bef2/SPO.Read

|Yes| - |clientID|The Application ID from the Canvas app registration configured in step 2|Yes| - |authority|

The login URL for your tenant. For example:
https://login.microsoftonline.com/mytenant.onmicrosoft.com|Yes| - |greet|Should the copilot greet users at the beginning of the conversation|No| - |buttonLabel|The label for the button opening the chat dialog|No| - |botName|The title for the copilot dialog|No| - -
- -5. after populating properties in elements.xml, or if you left elements.xml untouched and plan to run Configure-McsForSite.ps1 after building and deploying the component, open a new terminal in VS Code and navigate to the solution folder (the SharePointSSOComponent folder). Run the following commands: - - ```shell - npm install - gulp bundle --ship - gulp package-solution --ship - ``` - - if gulp is not available, install it by running: - - ```shell - npm install gulp-cli --global - ``` - -6. The gulp package-solution command should create a packaged solution (.sppkg) in the sharepoint/solution folder - -## Step 4 – Upload the component to SharePoint - -1. Whether you have built the component yourself, or opted to use the pre-built package, you should see a file called **pva-extension-sso.sppkg** under [sharepoint/solution](./sharepoint/solution/). Follow the instructions in [Manage apps using the Apps site - SharePoint - SharePoint in Microsoft 365 | Microsoft Learn](https://learn.microsoft.com/en-us/sharepoint/use-app-catalog#add-custom-apps) to upload the sppkg file using your SharePoint admin center. After uploading the sppkg file, choose **Enable App** and not **Enable this app and add it to all sites**. - - Once the app has been successfully uploaded and enabled, it will be visible under “Apps for SharePoint” - -2. Add the app to a site where your copilot should be available for users. This should be the same site as the one you provided for “Redirect URI” in step 2. - - To add an app to your site, follow the instructions in: [Add an app to a site - Microsoft Support](https://support.microsoft.com/en-us/office/add-an-app-to-a-site-ef9c0dbd-7fe1-4715-a1b0-fe3bc81317cb?ui=en-us&rs=en-us&ad=us). - -3. If you left elements.xml untouched, or if you are uploading the pre-built package, or even in case you would like to override the values configured in elements.xml for the site on which the component has been deployed, you can now run [Configure-McsForSite.ps1](./Configure-McsForSite.ps1): - -```PowerShell -.\Configure-McsForSite.ps1 -siteUrl "" -botUrl "" -botName "" -greet $True -customScope "" -clientId "" -authority "" -buttonLabel "" -``` - -4. After adding the app (and running Configure-MCSForSite.ps1 in case elements.xml has been left untouched), a button will be appear at the bottom of all the pages under the target site. Clicking on the button will open a dialog with a chat canvas for your copilot. Based on the logic of your copilot, users will be signed in automatically at the beginning of the conversation, or when a specific event occurs. - -

- Copilot Component -
- The Copilot Component Dialog -

- - diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.eslintrc.js b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.eslintrc.js deleted file mode 100644 index 562b8f68..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.eslintrc.js +++ /dev/null @@ -1,319 +0,0 @@ -require('@rushstack/eslint-config/patch/modern-module-resolution'); -module.exports = { - extends: ['@microsoft/eslint-config-spfx/lib/profiles/default'], - parserOptions: { tsconfigRootDir: __dirname }, - overrides: [ - { - files: ['*.ts', '*.tsx'], - parser: '@typescript-eslint/parser', - 'parserOptions': { - 'project': './tsconfig.json', - 'ecmaVersion': 2018, - 'sourceType': 'module' - }, - rules: { - // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin - '@rushstack/no-new-null': 1, - // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin - '@rushstack/hoist-jest-mock': 1, - // Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security - '@rushstack/security/no-unsafe-regexp': 1, - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - '@typescript-eslint/adjacent-overload-signatures': 1, - // RATIONALE: Code is more readable when the type of every variable is immediately obvious. - // Even if the compiler may be able to infer a type, this inference will be unavailable - // to a person who is reviewing a GitHub diff. This rule makes writing code harder, - // but writing code is a much less important activity than reading it. - // - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - '@typescript-eslint/explicit-function-return-type': [ - 1, - { - 'allowExpressions': true, - 'allowTypedFunctionExpressions': true, - 'allowHigherOrderFunctions': false - } - ], - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - // Rationale to disable: although this is a recommended rule, it is up to dev to select coding style. - // Set to 1 (warning) or 2 (error) to enable. - '@typescript-eslint/explicit-member-accessibility': 0, - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - '@typescript-eslint/no-array-constructor': 1, - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - // - // RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript. - // This rule should be suppressed only in very special cases such as JSON.stringify() - // where the type really can be anything. Even if the type is flexible, another type - // may be more appropriate such as "unknown", "{}", or "Record". - '@typescript-eslint/no-explicit-any': 1, - // RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch() - // handler. Thus wherever a Promise arises, the code must either append a catch handler, - // or else return the object to a caller (who assumes this responsibility). Unterminated - // promise chains are a serious issue. Besides causing errors to be silently ignored, - // they can also cause a NodeJS process to terminate unexpectedly. - '@typescript-eslint/no-floating-promises': 2, - // RATIONALE: Catches a common coding mistake. - '@typescript-eslint/no-for-in-array': 2, - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - '@typescript-eslint/no-misused-new': 2, - // RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks - // a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler - // optimizations. If you are declaring loose functions/variables, it's better to make them - // static members of a class, since classes support property getters and their private - // members are accessible by unit tests. Also, the exercise of choosing a meaningful - // class name tends to produce more discoverable APIs: for example, search+replacing - // the function "reverse()" is likely to return many false matches, whereas if we always - // write "Text.reverse()" is more unique. For large scale organization, it's recommended - // to decompose your code into separate NPM packages, which ensures that component - // dependencies are tracked more conscientiously. - // - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - '@typescript-eslint/no-namespace': [ - 1, - { - 'allowDeclarations': false, - 'allowDefinitionFiles': false - } - ], - // RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)" - // that avoids the effort of declaring "title" as a field. This TypeScript feature makes - // code easier to write, but arguably sacrifices readability: In the notes for - // "@typescript-eslint/member-ordering" we pointed out that fields are central to - // a class's design, so we wouldn't want to bury them in a constructor signature - // just to save some typing. - // - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - // Set to 1 (warning) or 2 (error) to enable the rule - '@typescript-eslint/parameter-properties': 0, - // RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code - // may impact performance. - // - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - '@typescript-eslint/no-unused-vars': [ - 1, - { - 'vars': 'all', - // Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code, - // the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures - // that are overriding a base class method or implementing an interface. - 'args': 'none' - } - ], - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - '@typescript-eslint/no-use-before-define': [ - 2, - { - 'functions': false, - 'classes': true, - 'variables': true, - 'enums': true, - 'typedefs': true - } - ], - // Disallows require statements except in import statements. - // In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports. - '@typescript-eslint/no-var-requires': 'error', - // RATIONALE: The "module" keyword is deprecated except when describing legacy libraries. - // - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - '@typescript-eslint/prefer-namespace-keyword': 1, - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - // Rationale to disable: it's up to developer to decide if he wants to add type annotations - // Set to 1 (warning) or 2 (error) to enable the rule - '@typescript-eslint/no-inferrable-types': 0, - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - // Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios - '@typescript-eslint/no-empty-interface': 0, - // RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake. - 'accessor-pairs': 1, - // RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking. - 'dot-notation': [ - 1, - { - 'allowPattern': '^_' - } - ], - // RATIONALE: Catches code that is likely to be incorrect - 'eqeqeq': 1, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'for-direction': 1, - // RATIONALE: Catches a common coding mistake. - 'guard-for-in': 2, - // RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time - // to split up your code. - 'max-lines': ['warn', { max: 2000 }], - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-async-promise-executor': 2, - // RATIONALE: Deprecated language feature. - 'no-caller': 2, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-compare-neg-zero': 2, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-cond-assign': 2, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-constant-condition': 1, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-control-regex': 2, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-debugger': 1, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-delete-var': 2, - // RATIONALE: Catches code that is likely to be incorrect - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-duplicate-case': 2, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-empty': 1, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-empty-character-class': 2, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-empty-pattern': 1, - // RATIONALE: Eval is a security concern and a performance concern. - 'no-eval': 1, - // RATIONALE: Catches code that is likely to be incorrect - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-ex-assign': 2, - // RATIONALE: System types are global and should not be tampered with in a scalable code base. - // If two different libraries (or two versions of the same library) both try to modify - // a type, only one of them can win. Polyfills are acceptable because they implement - // a standardized interoperable contract, but polyfills are generally coded in plain - // JavaScript. - 'no-extend-native': 1, - // Disallow unnecessary labels - 'no-extra-label': 1, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-fallthrough': 2, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-func-assign': 1, - // RATIONALE: Catches a common coding mistake. - 'no-implied-eval': 2, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-invalid-regexp': 2, - // RATIONALE: Catches a common coding mistake. - 'no-label-var': 2, - // RATIONALE: Eliminates redundant code. - 'no-lone-blocks': 1, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-misleading-character-class': 2, - // RATIONALE: Catches a common coding mistake. - 'no-multi-str': 2, - // RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to - // a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()", - // or else implies that the constructor is doing nontrivial computations, which is often - // a poor class design. - 'no-new': 1, - // RATIONALE: Obsolete language feature that is deprecated. - 'no-new-func': 2, - // RATIONALE: Obsolete language feature that is deprecated. - 'no-new-object': 2, - // RATIONALE: Obsolete notation. - 'no-new-wrappers': 1, - // RATIONALE: Catches code that is likely to be incorrect - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-octal': 2, - // RATIONALE: Catches code that is likely to be incorrect - 'no-octal-escape': 2, - // RATIONALE: Catches code that is likely to be incorrect - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-regex-spaces': 2, - // RATIONALE: Catches a common coding mistake. - 'no-return-assign': 2, - // RATIONALE: Security risk. - 'no-script-url': 1, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-self-assign': 2, - // RATIONALE: Catches a common coding mistake. - 'no-self-compare': 2, - // RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use - // commas to create compound expressions. In general code is more readable if each - // step is split onto a separate line. This also makes it easier to set breakpoints - // in the debugger. - 'no-sequences': 1, - // RATIONALE: Catches code that is likely to be incorrect - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-shadow-restricted-names': 2, - // RATIONALE: Obsolete language feature that is deprecated. - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-sparse-arrays': 2, - // RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception, - // such flexibility adds pointless complexity, by requiring every catch block to test - // the type of the object that it receives. Whereas if catch blocks can always assume - // that their object implements the "Error" contract, then the code is simpler, and - // we generally get useful additional information like a call stack. - 'no-throw-literal': 2, - // RATIONALE: Catches a common coding mistake. - 'no-unmodified-loop-condition': 1, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-unsafe-finally': 2, - // RATIONALE: Catches a common coding mistake. - 'no-unused-expressions': 1, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-unused-labels': 1, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-useless-catch': 1, - // RATIONALE: Avoids a potential performance problem. - 'no-useless-concat': 1, - // RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior. - // Always use "let" or "const" instead. - // - // STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json - 'no-var': 2, - // RATIONALE: Generally not needed in modern code. - 'no-void': 1, - // RATIONALE: Obsolete language feature that is deprecated. - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'no-with': 2, - // RATIONALE: Makes logic easier to understand, since constants always have a known value - // @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js - 'prefer-const': 1, - // RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused. - 'promise/param-names': 2, - // RATIONALE: Catches code that is likely to be incorrect - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'require-atomic-updates': 2, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'require-yield': 1, - // "Use strict" is redundant when using the TypeScript compiler. - 'strict': [ - 2, - 'never' - ], - // RATIONALE: Catches code that is likely to be incorrect - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - 'use-isnan': 2, - // STANDARDIZED BY: eslint\conf\eslint-recommended.js - // Set to 1 (warning) or 2 (error) to enable. - // Rationale to disable: !!{} - 'no-extra-boolean-cast': 0, - // ==================================================================== - // @microsoft/eslint-plugin-spfx - // ==================================================================== - '@microsoft/spfx/import-requires-chunk-name': 1, - '@microsoft/spfx/no-require-ensure': 2, - '@microsoft/spfx/pair-react-dom-render-unmount': 1 - } - }, - { - // For unit tests, we can be a little bit less strict. The settings below revise the - // defaults specified in the extended configurations, as well as above. - files: [ - // Test files - '*.test.ts', - '*.test.tsx', - '*.spec.ts', - '*.spec.tsx', - - // Facebook convention - '**/__mocks__/*.ts', - '**/__mocks__/*.tsx', - '**/__tests__/*.ts', - '**/__tests__/*.tsx', - - // Microsoft convention - '**/test/*.ts', - '**/test/*.tsx' - ], - rules: {} - } - ] -}; \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.gitignore b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.gitignore deleted file mode 100644 index 1eac77fa..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* - -# Dependency directories -node_modules - -# Build generated files -dist -lib -release -solution/* -!solution/*.sppkg -temp -.heft - -# Coverage directory used by tools like istanbul -coverage - -# OSX -.DS_Store -gulp -# Visual Studio files -.ntvs_analysis.dat -.vs -bin -obj - -# Resx Generated Code -*.resx.ts - -# Styles Generated Code -*.scss.ts diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.npmignore b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.npmignore deleted file mode 100644 index ae0b487c..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.npmignore +++ /dev/null @@ -1,16 +0,0 @@ -!dist -config - -gulpfile.js - -release -src -temp - -tsconfig.json -tslint.json - -*.log - -.yo-rc.json -.vscode diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.vscode/launch.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.vscode/launch.json deleted file mode 100644 index 53ca22cd..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.vscode/launch.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Hosted workbench", - "type": "msedge", - "request": "launch", - "url": "https://{tenantDomain}/_layouts/workbench.aspx", - "webRoot": "${workspaceRoot}", - "sourceMaps": true, - "sourceMapPathOverrides": { - "webpack:///.././src/*": "${webRoot}/src/*", - "webpack:///../../../src/*": "${webRoot}/src/*", - "webpack:///../../../../src/*": "${webRoot}/src/*", - "webpack:///../../../../../src/*": "${webRoot}/src/*" - }, - "runtimeArgs": [ - "--remote-debugging-port=9222", - "-incognito" - ] - } - ] -} \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.vscode/settings.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.vscode/settings.json deleted file mode 100644 index 16141662..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.vscode/settings.json +++ /dev/null @@ -1,14 +0,0 @@ -// Place your settings in this file to overwrite default and user settings. -{ - // Configure glob patterns for excluding files and folders in the file explorer. - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/bower_components": true, - "**/coverage": true, - "**/jest-output": true, - "**/lib-amd": true, - "src/**/*.scss.ts": true - }, - "typescript.tsdk": ".\\node_modules\\typescript\\lib" -} \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.yo-rc.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.yo-rc.json deleted file mode 100644 index 571035ec..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/.yo-rc.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "@microsoft/generator-sharepoint": { - "plusBeta": false, - "isCreatingSolution": true, - "nodeVersion": "22.17.0", - "sdksVersions": {}, - "version": "1.21.1", - "libraryName": "sidebar-agent", - "libraryId": "54cbba6a-2459-43a9-80a4-bc0675a81ac0", - "environment": "spo", - "packageManager": "npm", - "solutionName": "sidebar-agent", - "solutionShortDescription": "sidebar-agent description", - "skipFeatureDeployment": true, - "isDomainIsolated": false, - "componentType": "extension", - "extensionType": "ApplicationCustomizer" - } -} diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/README.md b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/README.md deleted file mode 100644 index 996653d5..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/README.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -nav_exclude: true -search_exclude: false ---- -# Copilot Studio Agent Side Panel for SharePoint -![Copilot Studio Agent in SharePoint](./images/agent%20sidepanel.png) - -## Summary - -This sample demonstrates how to integrate a Microsoft Copilot Studio agent into SharePoint using a SharePoint Framework (SPFx) Application Customizer. It leverages the ReactWebChat component with the [Fluent UI theme pack](https://github.com/microsoft/BotFramework-WebChat#experimental-fluent-ui-theme-pack) and the [Microsoft 365 Agents SDK for NodeJS/TypeScript](https://github.com/microsoft/Agents-for-js) to establish a secure connection with a Copilot Studio agent configured with ['Authenticate with Microsoft'](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft). - -The key advantage of using the Microsoft 365 Agents SDK is its ability to connect to agents that support Microsoft authentication, which enables [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) capabilities. - -> **Note**: This sample supersedes the [legacy SharePoint SSO Component sample](../) which uses DirectLine and a modal dialog. - -{: .important } -> ⚠️ **This is an open-source reference sample only.** -> For production deployments, we strongly recommend using the [built-in SharePoint channel in Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/publication-add-bot-to-sharepoint#configure-the-sharepoint-channel). - -## Features of this sample - -- **Single Sign-On (SSO)**: Leverages the current user's SharePoint authentication for seamless agent access -- **Sliding Panel UI**: Modern, responsive chat interface that slides in from the right side of the page -- **Customizable Appearance**: Configure header color and agent title to match your branding -- **New Conversation Support**: Users can start fresh conversations without leaving the page -- **Fluent UI Styling**: Uses the Fluent UI theme pack for a native Microsoft 365 look and feel -- **Tenant Graph Grounding**: Enables the agent to access and search Microsoft 365 content through [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) - -## Prerequisites - -Before you begin, ensure you have the following: - -1. **Published Copilot Studio Agent** - - Agent must be configured with ['Authenticate with Microsoft'](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft) authentication - - Agent should be published and accessible - - Consider enabling [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) for access to Microsoft 365 content - -2. **Azure Portal Access** - - Required for creating and configuring app registrations - - Permissions to create new app registrations in your Azure AD tenant - -3. **SharePoint Administrator Access** - - Required to deploy the SPFx extension to the app catalog - -4. **Development Environment** - - Node.js v22 (as per SPFx 1.21.1 requirements) - - npm or yarn package manager - - Git for cloning the repository - -## Technical Architecture - -This solution uses: -- **[Microsoft 365 Agents SDK for NodeJS/TypeScript](https://github.com/microsoft/Agents-for-js)**: Provides secure connection and authentication handling for Copilot Studio agents -- **[BotFramework-WebChat with Fluent UI Theme](https://github.com/microsoft/BotFramework-WebChat#experimental-fluent-ui-theme-pack)**: Renders the chat interface with Microsoft 365 styling -- **SharePoint Framework**: Enables deployment as a site-wide extension -- **[Tenant Graph Grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding)**: Allows the agent to search and access Microsoft 365 content - -## Setup Instructions - -### Step 1: Configure App Registration - -This step requires permissions to create application identities in your Azure tenant. For this sample, create a Native Client Application Identity (no secrets required): - -1. **Open Azure Portal** - - Navigate to [portal.azure.com](https://portal.azure.com) - - Go to **Azure Active Directory** (Entra ID) - -2. **Create a new App Registration** - - Click **App registrations** → **New registration** - - Provide a **Name** (e.g., "SharePoint Copilot Studio Agent") - - Choose **"Accounts in this organization directory only"** - - Under **Redirect URI**: - - Select **"Single-page application"** from the platform dropdown - - Enter your first SharePoint site URL without the trailing slash (e.g., `https://contoso.sharepoint.com/sites/mysite`) - - Click **Register** - -3. **Configure Authentication - Add SharePoint URLs** - - Go to **Authentication** in the Manage section - - Under **Single-page application**, add redirect URIs: - - `https://localhost:4321` (only if you plan to test locally - this is the default SPFx port) - - For each SharePoint site where you'll deploy the extension, add: - - The site URL **without** trailing slash (e.g., `https://contoso.sharepoint.com/sites/mysite`) - - Under **Implicit grant and hybrid flows**, ensure both checkboxes are **unchecked** (SPAs use PKCE flow) - - Click **Save** - - **Example for multiple sites:** - ``` - https://localhost:4321 (for local testing - default SPFx port) - https://contoso.sharepoint.com/sites/hr - https://contoso.sharepoint.com/sites/finance - https://contoso.sharepoint.com/sites/it - ``` - -4. **Configure API Permissions** - - In your new application, go to **API Permissions** in the Manage section - - Click **Add Permission** - - In the side panel, click the **APIs my organization uses** tab - - Search for **"Power Platform API"** or use the GUID `8578e004-a5c6-46e7-913e-12f58912df43` - - Under **Delegated permissions**, expand **CopilotStudio** and check: - - `CopilotStudio.Copilots.Invoke` - - Click **Add Permissions** - - (Optional) Click **Grant admin consent** for your organization - -5. **Note Required Values** - - On the **Overview** page, copy and save: - - **Application (client) ID** (e.g., `12345678-1234-1234-1234-123456789012`) - - **Directory (tenant) ID** (e.g., `87654321-4321-4321-4321-210987654321`) - -{: .note } -> If you don't see "Power Platform API" in the list, you need to add it to your tenant first. See [Power Platform API Authentication](https://learn.microsoft.com/en-us/power-platform/admin/programmability-authentication) and follow Step 2 to add the API. - -### Step 2: Get Copilot Studio Configuration - -1. Open your Copilot Studio environment -2. Navigate to your published agent -3. Ensure the agent is configured with ['Authenticate with Microsoft'](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft) -4. Optionally enable [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) for Microsoft 365 content access -5. Collect the required configuration values using one of these methods: - - **Option A: Get Direct Connect URL (Recommended)** - - Go to **Channels** → **Web app** - - Under **Microsoft 365 Agents SDK**, copy the **Connection string** - - This will look like: `https://xxxxxxx.07.environment.api.powerplatform.com/...` - - **Option B: Get Environment ID and Agent Identifier** - - Go to **Settings** → **Advanced** → **Metadata** - - Copy the following values: - - **Environment ID** - - **Schema name** (this is your Agent Identifier, e.g., `cr770_myAgent`) - -### Step 3: Clone the Repository - -```bash -git clone https://github.com/microsoft/CopilotStudioSamples.git -cd CopilotStudioSamples/SSOSamples/SharePointSSOAppCustomizer -``` - -### Step 4: Configure the Extension - -1. Open `sharepoint/assets/elements.xml` in your editor -2. Update the `ClientSideComponentProperties` with your values: - -```xml -ClientSideComponentProperties="{ - "appClientId": "YOUR_APP_CLIENT_ID", - "tenantId": "YOUR_TENANT_ID", - "environmentId": "YOUR_ENVIRONMENT_ID", - "agentIdentifier": "YOUR_AGENT_IDENTIFIER", - "directConnectUrl": "YOUR_DIRECT_CONNECT_URL", - "showTyping": true, - "headerBackgroundColor": "white", - "agentTitle": "Copilot Studio Agent" -}" -``` - -**Configuration Options:** - -| Property | Required | Description | Default | -|----------|----------|-------------|---------| -| `appClientId` | Yes | Your Azure AD app registration client ID | - | -| `tenantId` | Yes | Your Azure AD tenant ID | - | -| `environmentId` | Conditional | Copilot Studio environment ID (required if not using directConnectUrl) | - | -| `agentIdentifier` | Conditional | Your agent's identifier (required if not using directConnectUrl) | - | -| `directConnectUrl` | Conditional | Direct connection URL (use this OR environmentId + agentIdentifier) | - | -| `showTyping` | No | Show typing indicators | `true` | -| `headerBackgroundColor` | No | Header bar color (accepts any CSS color value) | `white` | -| `agentTitle` | No | Display title for the agent | `"Copilot Studio Agent"` | - -{: .note } -> You must provide either `directConnectUrl` OR both `environmentId` and `agentIdentifier`. - -### Step 5: Test Locally (Optional) - -Before building and deploying, you can test the extension locally: - -```bash -# Install dependencies -npm install - -# Check if gulp is installed, if not, install it -which gulp || npm install -g gulp-cli -# On Windows, use: where gulp || npm install -g gulp-cli - -# Serve the solution locally -gulp serve --nobrowser -``` - -After running `gulp serve`, navigate to the following URL (replace the values with your configuration): - -{% raw %} -``` -https://YOUR-TENANT.sharepoint.com/sites/YOUR-SITE/SitePages/Home.aspx?debugManifestsFile=https://localhost:4321/temp/build/manifests.js&loadSPFX=true&customActions={%224c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2%22:{%22location%22:%22ClientSideExtension.ApplicationCustomizer%22,%22properties%22:{%22appClientId%22:%22YOUR_APP_CLIENT_ID%22,%22tenantId%22:%22YOUR_TENANT_ID%22,%22directConnectUrl%22:%22YOUR_DIRECT_CONNECT_URL%22,%22showTyping%22:true,%22headerBackgroundColor%22:%22white%22,%22agentTitle%22:%22Your%20Agent%20Title%22}}} -``` -{% endraw %} - -- Replace `YOUR-TENANT`, `YOUR-SITE`, `YOUR_APP_CLIENT_ID`, `YOUR_TENANT_ID`, and `YOUR_DIRECT_CONNECT_URL` with your actual values -- Ensure you've added `https://localhost:4321` to your app registration's redirect URIs if testing locally - -### Step 6: Build and Package - -Once you've tested locally (or if you're ready to deploy directly): - -```bash -# Build the solution -gulp build - -# Bundle the solution -gulp bundle --ship - -# Package the solution -gulp package-solution --ship -``` - -This will create a `.sppkg` file in the `sharepoint/solution` folder. - -### Step 7: Deploy to SharePoint - -Upload the `.sppkg` file from `sharepoint/solution` to your SharePoint App Catalog. For detailed instructions on creating an app catalog, deploying apps, and adding them to sites, see [Add custom apps to SharePoint](https://learn.microsoft.com/en-us/sharepoint/use-app-catalog#add-custom-apps). - -## Troubleshooting - -### "Unable to acquire token" error - -If you encounter this error: -1. Open your browser's Developer Tools (F12) -2. Go to the **Network** tab -3. Look for failed authentication requests (usually to `login.microsoftonline.com`) -4. Click on the failed requests to see detailed error messages -5. Follow the authentication URLs that MSAL is attempting - the actual error details are often revealed in the response or redirect parameters - -Common causes: -- Missing or incorrect redirect URIs in your app registration (check for trailing slashes!) -- Incorrect permissions in the app registration (ensure `CopilotStudio.Copilots.Invoke` is granted) -- Incorrect tenant ID or client ID -- MSAL authentication flow being blocked by browser settings - -### "The redirect URI specified in the request does not match the redirect URIs configured for the application" error - -Check that the current SharePoint URL has been added as an allowed redirect URI for the provided app registration. - -## Additional Resources - -- [Built-in SharePoint Channel for Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/publication-add-bot-to-sharepoint#configure-the-sharepoint-channel) -- [Authenticate with Microsoft in Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft) -- [Tenant Graph Grounding in Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) -- [Microsoft 365 Agents SDK for NodeJS/TypeScript](https://github.com/microsoft/Agents-for-js) -- [BotFramework-WebChat with Fluent UI Theme](https://github.com/microsoft/BotFramework-WebChat#experimental-fluent-ui-theme-pack) -- [Copilot Studio Documentation](https://learn.microsoft.com/en-us/microsoft-copilot-studio/) -- [SharePoint Framework Documentation](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/sharepoint-framework-overview) - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/config.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/config.json deleted file mode 100644 index 64aac1c1..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/config.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", - "version": "2.0", - "bundles": { - "sidebar-agent-application-customizer": { - "components": [ - { - "entrypoint": "./lib/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.js", - "manifest": "./src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.manifest.json" - } - ] - } - }, - "externals": {}, - "localizedResources": { - "SidebarAgentApplicationCustomizerStrings": "lib/extensions/sidebarAgent/loc/{locale}.js" - } -} diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/deploy-azure-storage.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/deploy-azure-storage.json deleted file mode 100644 index a549ff80..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/deploy-azure-storage.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", - "workingDir": "./release/assets/", - "account": "", - "container": "sidebar-agent", - "accessKey": "" -} \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/package-solution.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/package-solution.json deleted file mode 100644 index a4a21d53..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/package-solution.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", - "solution": { - "name": "sidebar-agent-client-side-solution", - "id": "54cbba6a-2459-43a9-80a4-bc0675a81ac0", - "version": "1.0.0.0", - "includeClientSideAssets": true, - "skipFeatureDeployment": true, - "isDomainIsolated": false, - "developer": { - "name": "", - "websiteUrl": "", - "privacyUrl": "", - "termsOfUseUrl": "", - "mpnId": "Undefined-1.21.1" - }, - "metadata": { - "shortDescription": { - "default": "sidebar-agent description" - }, - "longDescription": { - "default": "sidebar-agent description" - }, - "screenshotPaths": [], - "videoUrl": "", - "categories": [] - }, - "features": [ - { - "title": "Application Extension - Deployment of custom action", - "description": "Deploys a custom action with ClientSideComponentId association", - "id": "f757a2ed-fea6-4478-abd2-080c4a1f6a84", - "version": "1.0.0.0", - "assets": { - "elementManifests": [ - "elements.xml", - "ClientSideInstance.xml" - ] - } - } - ] - }, - "paths": { - "zippedPackage": "solution/sidebar-agent.sppkg" - } -} diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/sass.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/sass.json deleted file mode 100644 index 5e78c982..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/sass.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json" -} \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/serve.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/serve.json deleted file mode 100644 index 0337190d..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/serve.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json", - "port": 4321, - "https": true, - "serveConfigurations": { - "default": { - "pageUrl": "https://m365x66364744.sharepoint.com/sites/GlobalSales/SitePages/Home.aspx", - "customActions": { - "4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2": { - "location": "ClientSideExtension.ApplicationCustomizer", - "properties": { - "appClientId": "APP-REGISTRATION-ID", - "tenantId": "TENANT-ID", - "environmentId": "ENV-ID", - "agentIdentifier": "AGENT-ID", - "directConnectUrl": "YOUR-DIRECT-CONNECT-URL", - "showTyping": true - } - } - } - }, - "sidebarAgent": { - "pageUrl": "https://m365x66364744.sharepoint.com/sites/GlobalSales/SitePages/Home.aspx", - "customActions": { - "4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2": { - "location": "ClientSideExtension.ApplicationCustomizer", - "properties": { - "appClientId": "APP-REGISTRATION-ID", - "tenantId": "TENANT-ID", - "environmentId": "ENV-ID", - "agentIdentifier": "AGENT-ID", - "directConnectUrl": "YOUR-DIRECT-CONNECT-URL", - "showTyping": true - } - } - } - } - } -} diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/write-manifests.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/write-manifests.json deleted file mode 100644 index bad35260..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/config/write-manifests.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json", - "cdnBasePath": "" -} \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/gulpfile.js b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/gulpfile.js deleted file mode 100644 index be291870..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/gulpfile.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -const build = require('@microsoft/sp-build-web'); - -build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); - -var getTasks = build.rig.getTasks; -build.rig.getTasks = function () { - var result = getTasks.call(build.rig); - - result.set('serve', result.get('serve-deprecated')); - - return result; -}; - -build.initialize(require('gulp')); diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/package.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/package.json deleted file mode 100644 index 8fe1cf5c..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "sidebar-agent", - "version": "0.0.1", - "private": true, - "engines": { - "node": ">=22.14.0 < 23.0.0" - }, - "main": "lib/index.js", - "scripts": { - "build": "gulp bundle", - "clean": "gulp clean", - "test": "gulp test" - }, - "dependencies": { - "@azure/msal-browser": "^4.13.1", - "@fluentui/react": "^8.121.4", - "@microsoft/agents-copilotstudio-client": "^1.0.0", - "@microsoft/decorators": "1.21.1", - "@microsoft/sp-application-base": "1.21.1", - "@microsoft/sp-core-library": "1.21.1", - "botframework-webchat": "4.18.1-main.20250804.93043a9", - "botframework-webchat-fluent-theme": "4.18.1-main.20250804.93043a9", - "react": "17.0.1", - "react-dom": "17.0.1", - "tslib": "2.3.1" - }, - "devDependencies": { - "@microsoft/eslint-config-spfx": "1.21.1", - "@microsoft/eslint-plugin-spfx": "1.21.1", - "@microsoft/rush-stack-compiler-5.3": "0.1.0", - "@microsoft/sp-build-web": "1.21.1", - "@microsoft/sp-module-interfaces": "1.21.1", - "@rushstack/eslint-config": "4.0.1", - "@types/webpack-env": "~1.15.2", - "ajv": "^6.12.5", - "eslint": "8.57.1", - "gulp": "4.0.2", - "typescript": "~5.3.3" - } -} diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/sharepoint/assets/ClientSideInstance.xml b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/sharepoint/assets/ClientSideInstance.xml deleted file mode 100644 index 04b50040..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/sharepoint/assets/ClientSideInstance.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/sharepoint/assets/elements.xml b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/sharepoint/assets/elements.xml deleted file mode 100644 index 70f90775..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/sharepoint/assets/elements.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/index.ts b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/index.ts deleted file mode 100644 index fb81db1e..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -// A file is required to be in the root of the /src directory by the TypeScript compiler diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/tsconfig.json b/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/tsconfig.json deleted file mode 100644 index 419cd861..00000000 --- a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/tsconfig.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "extends": "./node_modules/@microsoft/rush-stack-compiler-5.3/includes/tsconfig-web.json", - "compilerOptions": { - "target": "es5", - "forceConsistentCasingInFileNames": true, - "module": "esnext", - "moduleResolution": "node", - "jsx": "react", - "declaration": true, - "sourceMap": true, - "experimentalDecorators": true, - "skipLibCheck": true, - "outDir": "lib", - "inlineSources": false, - "noImplicitAny": true, - - "typeRoots": [ - "./node_modules/@types", - "./node_modules/@microsoft" - ], - "types": [ - "webpack-env" - ], - "lib": [ - "es5", - "dom", - "es2015.collection", - "es2015.promise" - ] - }, - "include": [ - "src/**/*.ts", - "src/**/*.tsx" - ] -} diff --git a/ui/embed/sharepoint-customizer/config/config.json b/ui/embed/sharepoint-customizer/config/config.json index 18435363..64aac1c1 100644 --- a/ui/embed/sharepoint-customizer/config/config.json +++ b/ui/embed/sharepoint-customizer/config/config.json @@ -2,17 +2,17 @@ "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json", "version": "2.0", "bundles": { - "pva-sso-application-customizer": { + "sidebar-agent-application-customizer": { "components": [ { - "entrypoint": "./lib/extensions/pvaSso/PvaSsoApplicationCustomizer.js", - "manifest": "./src/extensions/pvaSso/PvaSsoApplicationCustomizer.manifest.json" + "entrypoint": "./lib/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.js", + "manifest": "./src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.manifest.json" } ] } }, "externals": {}, "localizedResources": { - "PvaSsoApplicationCustomizerStrings": "lib/extensions/pvaSso/loc/{locale}.js" + "SidebarAgentApplicationCustomizerStrings": "lib/extensions/sidebarAgent/loc/{locale}.js" } } diff --git a/ui/embed/sharepoint-customizer/config/deploy-azure-storage.json b/ui/embed/sharepoint-customizer/config/deploy-azure-storage.json index 9749f859..a549ff80 100644 --- a/ui/embed/sharepoint-customizer/config/deploy-azure-storage.json +++ b/ui/embed/sharepoint-customizer/config/deploy-azure-storage.json @@ -2,6 +2,6 @@ "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", "workingDir": "./release/assets/", "account": "", - "container": "pva-extension-sso", + "container": "sidebar-agent", "accessKey": "" } \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/config/package-solution.json b/ui/embed/sharepoint-customizer/config/package-solution.json index f17b9eca..a4a21d53 100644 --- a/ui/embed/sharepoint-customizer/config/package-solution.json +++ b/ui/embed/sharepoint-customizer/config/package-solution.json @@ -1,8 +1,8 @@ { "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json", "solution": { - "name": "pva-extension-sso-client-side-solution", - "id": "14634225-91e5-41a4-b9cc-161ccb3400b4", + "name": "sidebar-agent-client-side-solution", + "id": "54cbba6a-2459-43a9-80a4-bc0675a81ac0", "version": "1.0.0.0", "includeClientSideAssets": true, "skipFeatureDeployment": true, @@ -12,14 +12,14 @@ "websiteUrl": "", "privacyUrl": "", "termsOfUseUrl": "", - "mpnId": "Undefined-1.18.0" + "mpnId": "Undefined-1.21.1" }, "metadata": { "shortDescription": { - "default": "pva-extension-sso description" + "default": "sidebar-agent description" }, "longDescription": { - "default": "pva-extension-sso description" + "default": "sidebar-agent description" }, "screenshotPaths": [], "videoUrl": "", @@ -29,7 +29,7 @@ { "title": "Application Extension - Deployment of custom action", "description": "Deploys a custom action with ClientSideComponentId association", - "id": "b9966a99-b9c1-4136-a847-d3e236c784a0", + "id": "f757a2ed-fea6-4478-abd2-080c4a1f6a84", "version": "1.0.0.0", "assets": { "elementManifests": [ @@ -41,6 +41,6 @@ ] }, "paths": { - "zippedPackage": "solution/pva-extension-sso.sppkg" + "zippedPackage": "solution/sidebar-agent.sppkg" } } diff --git a/ui/embed/sharepoint-customizer/config/serve.json b/ui/embed/sharepoint-customizer/config/serve.json index 3f996aaa..0337190d 100644 --- a/ui/embed/sharepoint-customizer/config/serve.json +++ b/ui/embed/sharepoint-customizer/config/serve.json @@ -4,31 +4,33 @@ "https": true, "serveConfigurations": { "default": { - "pageUrl": "YOUR_SHAREPOINT_SITE", + "pageUrl": "https://m365x66364744.sharepoint.com/sites/GlobalSales/SitePages/Home.aspx", "customActions": { - "bbcf8287-ea2d-4bb6-868f-19b9cf4b0812": { + "4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2": { "location": "ClientSideExtension.ApplicationCustomizer", "properties": { - "botURL": "YOUR_BOT_URL", - "customScope" : "YOUR_CUSTOM_SCOPE", - "clientID" : "YOUR_CLIENT_ID", - "authority" : "YOUR_TENANT", - "greet" : true + "appClientId": "APP-REGISTRATION-ID", + "tenantId": "TENANT-ID", + "environmentId": "ENV-ID", + "agentIdentifier": "AGENT-ID", + "directConnectUrl": "YOUR-DIRECT-CONNECT-URL", + "showTyping": true } } } }, - "pvaSso": { - "pageUrl": "YOUR_SHAREPOINT_SITE", + "sidebarAgent": { + "pageUrl": "https://m365x66364744.sharepoint.com/sites/GlobalSales/SitePages/Home.aspx", "customActions": { - "bbcf8287-ea2d-4bb6-868f-19b9cf4b0812": { + "4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2": { "location": "ClientSideExtension.ApplicationCustomizer", "properties": { - "botURL": "YOUR_BOT_URL", - "customScope" : "YOUR_CUSTOM_SCOPE", - "clientID" : "YOUR_CLIENT_ID", - "authority" : "YOUR_TENANT", - "greet" : true + "appClientId": "APP-REGISTRATION-ID", + "tenantId": "TENANT-ID", + "environmentId": "ENV-ID", + "agentIdentifier": "AGENT-ID", + "directConnectUrl": "YOUR-DIRECT-CONNECT-URL", + "showTyping": true } } } diff --git a/ui/embed/sharepoint-customizer/gulpfile.js b/ui/embed/sharepoint-customizer/gulpfile.js index 4312f1ff..be291870 100644 --- a/ui/embed/sharepoint-customizer/gulpfile.js +++ b/ui/embed/sharepoint-customizer/gulpfile.js @@ -13,23 +13,4 @@ build.rig.getTasks = function () { return result; }; -build.configureWebpack.mergeConfig({ - additionalConfiguration: (generatedConfiguration) => { - generatedConfiguration.module.rules.push( - { - test: /\.js$/, - exclude: /node_modules\/(?!htmlparser2)/, - use: { - loader: 'babel-loader', - options: { - presets: ['@babel/preset-env'] - } - } - } - ); - - return generatedConfiguration; - } -}); - build.initialize(require('gulp')); diff --git a/ui/embed/sharepoint-customizer/image.png b/ui/embed/sharepoint-customizer/image.png deleted file mode 100644 index a95c41be..00000000 Binary files a/ui/embed/sharepoint-customizer/image.png and /dev/null differ diff --git a/ui/embed/sharepoint-customizer/images/SharePointSSOComponent.png b/ui/embed/sharepoint-customizer/images/SharePointSSOComponent.png deleted file mode 100644 index 44efd097..00000000 Binary files a/ui/embed/sharepoint-customizer/images/SharePointSSOComponent.png and /dev/null differ diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/images/agent sidepanel.png b/ui/embed/sharepoint-customizer/images/agent sidepanel.png similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/images/agent sidepanel.png rename to ui/embed/sharepoint-customizer/images/agent sidepanel.png diff --git a/ui/embed/sharepoint-customizer/images/apiPermissions.png b/ui/embed/sharepoint-customizer/images/apiPermissions.png deleted file mode 100644 index 23314c73..00000000 Binary files a/ui/embed/sharepoint-customizer/images/apiPermissions.png and /dev/null differ diff --git a/ui/embed/sharepoint-customizer/images/apisMyOrganization.png b/ui/embed/sharepoint-customizer/images/apisMyOrganization.png deleted file mode 100644 index 6e7cd424..00000000 Binary files a/ui/embed/sharepoint-customizer/images/apisMyOrganization.png and /dev/null differ diff --git a/ui/embed/sharepoint-customizer/images/clientID.png b/ui/embed/sharepoint-customizer/images/clientID.png deleted file mode 100644 index 6d1c93fc..00000000 Binary files a/ui/embed/sharepoint-customizer/images/clientID.png and /dev/null differ diff --git a/ui/embed/sharepoint-customizer/images/customScope.png b/ui/embed/sharepoint-customizer/images/customScope.png deleted file mode 100644 index 402aa065..00000000 Binary files a/ui/embed/sharepoint-customizer/images/customScope.png and /dev/null differ diff --git a/ui/embed/sharepoint-customizer/images/folderStructure.png b/ui/embed/sharepoint-customizer/images/folderStructure.png deleted file mode 100644 index 1553cdae..00000000 Binary files a/ui/embed/sharepoint-customizer/images/folderStructure.png and /dev/null differ diff --git a/ui/embed/sharepoint-customizer/images/scopePermissions.png b/ui/embed/sharepoint-customizer/images/scopePermissions.png deleted file mode 100644 index bfde64d2..00000000 Binary files a/ui/embed/sharepoint-customizer/images/scopePermissions.png and /dev/null differ diff --git a/ui/embed/sharepoint-customizer/images/toeknExchangeURL.png b/ui/embed/sharepoint-customizer/images/toeknExchangeURL.png deleted file mode 100644 index 7d8c3f37..00000000 Binary files a/ui/embed/sharepoint-customizer/images/toeknExchangeURL.png and /dev/null differ diff --git a/ui/embed/sharepoint-customizer/package.json b/ui/embed/sharepoint-customizer/package.json index 48dfede8..8fe1cf5c 100644 --- a/ui/embed/sharepoint-customizer/package.json +++ b/ui/embed/sharepoint-customizer/package.json @@ -1,9 +1,9 @@ { - "name": "pva-extension-sso", + "name": "sidebar-agent", "version": "0.0.1", "private": true, "engines": { - "node": ">=16.13.0 <17.0.0 || >=18.17.1 <19.0.0" + "node": ">=22.14.0 < 23.0.0" }, "main": "lib/index.js", "scripts": { @@ -12,32 +12,29 @@ "test": "gulp test" }, "dependencies": { - "@microsoft/decorators": "1.18.0", - "@microsoft/sp-application-base": "1.18.0", - "@microsoft/sp-core-library": "1.18.0", - "@microsoft/sp-dialog": "1.18.0", - "@uifabric/react-hooks": "^7.16.4", - "botframework-webchat": "^4.15.9", - "office-ui-fabric-react": "^7.204.0", - "p-defer-es5": "^2.0.1", - "react": "^17.0.1", - "react-dom": "^17.0.1", + "@azure/msal-browser": "^4.13.1", + "@fluentui/react": "^8.121.4", + "@microsoft/agents-copilotstudio-client": "^1.0.0", + "@microsoft/decorators": "1.21.1", + "@microsoft/sp-application-base": "1.21.1", + "@microsoft/sp-core-library": "1.21.1", + "botframework-webchat": "4.18.1-main.20250804.93043a9", + "botframework-webchat-fluent-theme": "4.18.1-main.20250804.93043a9", + "react": "17.0.1", + "react-dom": "17.0.1", "tslib": "2.3.1" }, "devDependencies": { - "@microsoft/eslint-config-spfx": "1.18.0", - "@microsoft/eslint-plugin-spfx": "1.18.0", - "@microsoft/rush-stack-compiler-4.7": "0.1.0", - "@microsoft/sp-build-web": "1.18.0", - "@microsoft/sp-module-interfaces": "1.18.0", - "@rushstack/eslint-config": "2.5.1", + "@microsoft/eslint-config-spfx": "1.21.1", + "@microsoft/eslint-plugin-spfx": "1.21.1", + "@microsoft/rush-stack-compiler-5.3": "0.1.0", + "@microsoft/sp-build-web": "1.21.1", + "@microsoft/sp-module-interfaces": "1.21.1", + "@rushstack/eslint-config": "4.0.1", "@types/webpack-env": "~1.15.2", "ajv": "^6.12.5", - "eslint": "8.7.0", + "eslint": "8.57.1", "gulp": "4.0.2", - "typescript": "4.7.4", - "babel-loader": "^8.3.0", - "@babel/core": "^7.23.0", - "@babel/preset-env": "^7.22.20" + "typescript": "~5.3.3" } } diff --git a/ui/embed/sharepoint-customizer/populate_elements_xml.py b/ui/embed/sharepoint-customizer/populate_elements_xml.py deleted file mode 100644 index 806576fb..00000000 --- a/ui/embed/sharepoint-customizer/populate_elements_xml.py +++ /dev/null @@ -1,84 +0,0 @@ -import xml.etree.ElementTree as ET -import json -import re - -# Define the namespace -ns = {'sp': 'http://schemas.microsoft.com/sharepoint/'} - -def get_user_input(key, value_type, current_value): - if value_type == bool: - while True: - user_input = input(f"Enter new value for '{key}' boolean ('true' or 'false', current: {current_value}): ").strip().lower() - if user_input in ['true', 'false']: - return user_input == 'true' - print("Invalid input for boolean, please enter 'true' or 'false'.") - else: - return input(f"Enter new value for '{key}' {value_type} (current: {current_value}): ").strip() - -def parse_properties(properties_str): - # Replace placeholder boolean value with an actual boolean for parsing - properties_str = properties_str.replace("TRUE_OR_FALSE", "true") # Assuming default as true - return json.loads(properties_str) - -def update_properties(properties): - new_properties = {} - for key, value in properties.items(): - value_type = bool if isinstance(value, bool) else str - new_properties[key] = get_user_input(key, value_type, value) - return new_properties - -def escape_json_for_xml(json_obj): - # Dump the JSON object to a string with double quotes, and without spaces after separators - json_str = json.dumps(json_obj, separators=(',', ':')) - # Replace double quotes with the XML escape sequence for a quote - escaped_json_str = json_str.replace('"', '"') - return escaped_json_str - -def update_xml(file_path, new_properties): - tree = ET.parse(file_path) - root = tree.getroot() - - # Set the default namespace for the XML file - ET.register_namespace('', 'http://schemas.microsoft.com/sharepoint/') - - # Convert our properties to the correctly escaped string for XML - escaped_properties_str = escape_json_for_xml(new_properties) - - # Find the correct CustomAction element and update it - for custom_action in root.findall(".//{http://schemas.microsoft.com/sharepoint/}CustomAction[@ClientSideComponentProperties]"): - # Set the escaped string directly, avoiding further XML escaping - custom_action.set('ClientSideComponentProperties', escaped_properties_str) - - # Write the updated XML to a string - xml_str = ET.tostring(root, encoding='unicode') - - # Replace the namespace prefixes that ElementTree adds to the tags - xml_str = re.sub(r' xmlns:ns0="[^"]+"', '', xml_str, count=1) # Remove the xmlns attribute - xml_str = xml_str.replace('ns0:', '') # Remove the ns0 prefix - - # Correct the ampersand escaping issue - xml_str = xml_str.replace('&quot;', '"') - - # Write the corrected XML string to the file - with open(file_path, 'w', encoding='utf-8') as file: - file.write('\n') # Write the XML declaration - file.write(xml_str) - -# Use the provided file path -file_path = 'sharepoint/assets/elements.xml' -tree = ET.parse(file_path) -root = tree.getroot() - -# Include the namespace when finding the tag -for custom_action in root.findall("sp:CustomAction[@ClientSideComponentProperties]", ns): - properties_str = custom_action.get('ClientSideComponentProperties') - - # Check if properties_str is not None or empty - if properties_str: - properties = parse_properties(properties_str) - new_properties = update_properties(properties) - update_xml(file_path, new_properties) - - print("XML file has been updated with new properties.") - else: - print("No ClientSideComponentProperties attribute found.") \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/sharepoint/assets/ClientSideInstance.xml b/ui/embed/sharepoint-customizer/sharepoint/assets/ClientSideInstance.xml index 77f1638a..04b50040 100644 --- a/ui/embed/sharepoint-customizer/sharepoint/assets/ClientSideInstance.xml +++ b/ui/embed/sharepoint-customizer/sharepoint/assets/ClientSideInstance.xml @@ -1,9 +1,9 @@ + ComponentId="4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2" + Properties=""> \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml b/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml index 57cbd0bd..70f90775 100644 --- a/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml +++ b/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml @@ -1,9 +1,18 @@ + ClientSideComponentId="4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2" + ClientSideComponentProperties="{ + "appClientId": "YOUR_APP_CLIENT_ID", + "tenantId": "YOUR_TENANT_ID", + "environmentId": "YOUR_ENVIRONMENT_ID", + "agentIdentifier": "YOUR_AGENT_IDENTIFIER", + "directConnectUrl": "YOUR_DIRECT_CONNECT_URL", + "showTyping": true, + "headerBackgroundColor": "white", + "agentTitle": "Copilot Studio Agent" + }"> \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/sharepoint/solution/pva-extension-sso.sppkg b/ui/embed/sharepoint-customizer/sharepoint/solution/pva-extension-sso.sppkg deleted file mode 100644 index bd6a8b10..00000000 Binary files a/ui/embed/sharepoint-customizer/sharepoint/solution/pva-extension-sso.sppkg and /dev/null differ diff --git a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/PvaSsoApplicationCustomizer.manifest.json b/ui/embed/sharepoint-customizer/src/extensions/pvaSso/PvaSsoApplicationCustomizer.manifest.json deleted file mode 100644 index 66679957..00000000 --- a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/PvaSsoApplicationCustomizer.manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-extension-manifest.schema.json", - - "id": "bbcf8287-ea2d-4bb6-868f-19b9cf4b0812", - "alias": "PvaSsoApplicationCustomizer", - "componentType": "Extension", - "extensionType": "ApplicationCustomizer", - - // The "*" signifies that the version should be taken from the package.json - "version": "*", - "manifestVersion": 2, - - // If true, the component can only be installed on sites where Custom Script is allowed. - // Components that allow authors to embed arbitrary script code should set this to true. - // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f - "requiresCustomScript": false -} diff --git a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/PvaSsoApplicationCustomizer.ts b/ui/embed/sharepoint-customizer/src/extensions/pvaSso/PvaSsoApplicationCustomizer.ts deleted file mode 100644 index d68bd81b..00000000 --- a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/PvaSsoApplicationCustomizer.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Log } from '@microsoft/sp-core-library'; -import { - BaseApplicationCustomizer, - PlaceholderContent, - PlaceholderName -} from '@microsoft/sp-application-base'; -//import { Dialog } from '@microsoft/sp-dialog'; -import * as ReactDOM from "react-dom"; -import * as React from "react"; -import Chatbot from './components/ChatBot'; - - - -import * as strings from 'PvaSsoApplicationCustomizerStrings'; - - -import { override } from '@microsoft/decorators'; -import { IChatbotProps } from './components/IChatBotProps'; - -const LOG_SOURCE: string = 'PvaSsoApplicationCustomizer'; - -/** - * If your command set uses the ClientSideComponentProperties JSON input, - * it will be deserialized into the BaseExtension.properties object. - * You can define an interface to describe it. - */ -/** - * Properties for the PvaSsoApplicationCustomizer. - */ -export interface IPvaSsoApplicationCustomizerProperties { - /** - * The URL of the bot. - */ - botURL: string; - /** - * The name of the bot. - */ - botName?: string; - /** - * The label for the button. - */ - buttonLabel?: string; - /** - * The email of the user. - */ - userEmail: string; - /** - * The URL of the bot's avatar image. - */ - botAvatarImage?: string; - /** - * The initials of the bot's avatar. - */ - botAvatarInitials?: string; - /** - * Whether or not to greet the user. - */ - greet?: boolean; - /** - * The custom scope defined in the Azure AD app registration for the bot. - */ - customScope: string; - /** - * The client ID from the Azure AD app registration for the bot. - */ - clientID: string; - /** - * Azure AD tenant login URL - */ - authority: string; -} - -/** A Custom Action which can be run during execution of a Client Side Application */ -export default class PvaSsoApplicationCustomizer - extends BaseApplicationCustomizer { - - private _bottomPlaceholder: PlaceholderContent | undefined; - - - @override - public onInit(): Promise { - - Log.info(LOG_SOURCE, `Bot URL ${this.properties.botURL}`); - - if (!this.properties.buttonLabel || this.properties.buttonLabel === "") { - this.properties.buttonLabel = strings.DefaultButtonLabel; - } - - if (!this.properties.botName || this.properties.botName === "") { - this.properties.botName = strings.DefaultBotName; - } - - if (this.properties.greet !== true) { - this.properties.greet = false; - } - - this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders); - - return Promise.resolve(); - } - - private _renderPlaceHolders(): void { - // Handling the bottom placeholder - if (!this._bottomPlaceholder) { - this._bottomPlaceholder = this.context.placeholderProvider.tryCreateContent( - PlaceholderName.Bottom, - { onDispose: this._onDispose } - ); - - // The extension should not assume that the expected placeholder is available. - if (!this._bottomPlaceholder) { - console.error("The expected placeholder (Bottom) was not found."); - return; - } - const user = this.context.pageContext.user; - const elem: React.ReactElement = React.createElement(Chatbot, { ...this.properties, userEmail: user.email, userFriendlyName: user.displayName }); - ReactDOM.render(elem, this._bottomPlaceholder.domElement); - } - } - - private _onDispose(): void { - } - -} diff --git a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/components/ChatBot.tsx b/ui/embed/sharepoint-customizer/src/extensions/pvaSso/components/ChatBot.tsx deleted file mode 100644 index a79d4070..00000000 --- a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/components/ChatBot.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import * as React from "react"; -import { useBoolean, useId } from '@uifabric/react-hooks'; -import * as ReactWebChat from 'botframework-webchat'; -import { Dialog, DialogType } from 'office-ui-fabric-react/lib/Dialog'; -import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; -import { Spinner } from 'office-ui-fabric-react/lib/Spinner'; -import { Dispatch } from 'redux' -import { useRef } from "react"; - -import { IChatbotProps } from "./IChatBotProps"; -import MSALWrapper from "./MSALWrapper"; - -export const PVAChatbotDialog: React.FunctionComponent = (props) => { - - // Dialog properties and states - const dialogContentProps = { - type: DialogType.normal, - title: props.botName, - closeButtonAriaLabel: 'Close' - }; - - const [hideDialog, { toggle: toggleHideDialog }] = useBoolean(true); - const labelId: string = useId('dialogLabel'); - const subTextId: string = useId('subTextLabel'); - - const modalProps = React.useMemo( - () => ({ - isBlocking: false, - }), - [labelId, subTextId], - ); - - // Your bot's token endpoint - const botURL = props.botURL; - - // constructing URL using regional settings - const environmentEndPoint = botURL.slice(0,botURL.indexOf('/powervirtualagents')); - const apiVersion = botURL.slice(botURL.indexOf('api-version')).split('=')[1]; - const regionalChannelSettingsURL = `${environmentEndPoint}/powervirtualagents/regionalchannelsettings?api-version=${apiVersion}`; - - // Using refs instead of IDs to get the webchat and loading spinner elements - const webChatRef = useRef(null); - const loadingSpinnerRef = useRef(null); - - // A utility function that extracts the OAuthCard resource URI from the incoming activity or return undefined - function getOAuthCardResourceUri(activity: any): string | undefined { - const attachment = activity?.attachments?.[0]; - if (attachment?.contentType === 'application/vnd.microsoft.card.oauth' && attachment.content.tokenExchangeResource) { - return attachment.content.tokenExchangeResource.uri; - } - } - - const handleLayerDidMount = async () => { - - const MSALWrapperInstance = new MSALWrapper(props.clientID, props.authority); - - // Trying to get token if user is already signed-in - let responseToken = await MSALWrapperInstance.handleLoggedInUser([props.customScope], props.userEmail); - - if (!responseToken) { - // Trying to get token if user is not signed-in - responseToken = await MSALWrapperInstance.acquireAccessToken([props.customScope], props.userEmail); - } - - const token = responseToken?.accessToken || null; - - // Get the regional channel URL - let regionalChannelURL; - - const regionalResponse = await fetch(regionalChannelSettingsURL); - if(regionalResponse.ok){ - const data = await regionalResponse.json(); - regionalChannelURL = data.channelUrlsById.directline; - } - else { - console.error(`HTTP error! Status: ${regionalResponse.status}`); - } - - - // Create DirectLine object - let directline: any; - - const response = await fetch(botURL); - - if (response.ok) { - const conversationInfo = await response.json(); - directline = ReactWebChat.createDirectLine({ - token: conversationInfo.token, - domain: regionalChannelURL + 'v3/directline', - }); - } else { - console.error(`HTTP error! Status: ${response.status}`); - } - - const store = ReactWebChat.createStore( - {}, - ({ dispatch }: { dispatch: Dispatch }) => (next: any) => (action: any) => { - - // Checking whether we should greet the user - if (props.greet) - { - if (action.type === "DIRECT_LINE/CONNECT_FULFILLED") { - console.log("Action:" + action.type); - dispatch({ - meta: { - method: "keyboard", - }, - payload: { - activity: { - channelData: { - postBack: true, - }, - //Web Chat will show the 'Greeting' System Topic message which has a trigger-phrase 'hello' - name: 'startConversation', - type: "event" - }, - }, - type: "DIRECT_LINE/POST_ACTIVITY", - }); - return next(action); - } - } - - // Checking whether the bot is asking for authentication - if (action.type === "DIRECT_LINE/INCOMING_ACTIVITY") { - const activity = action.payload.activity; - if (activity.from && activity.from.role === 'bot' && - (getOAuthCardResourceUri(activity))){ - directline.postActivity({ - type: 'invoke', - name: 'signin/tokenExchange', - value: { - id: activity.attachments[0].content.tokenExchangeResource.id, - connectionName: activity.attachments[0].content.connectionName, - token - }, - "from": { - id: props.userEmail, - name: props.userFriendlyName, - role: "user" - } - }).subscribe( - (id: any) => { - if(id === "retry"){ - // bot was not able to handle the invoke, so display the oauthCard (manual authentication) - console.log("bot was not able to handle the invoke, so display the oauthCard") - return next(action); - } - }, - (error: any) => { - // an error occurred to display the oauthCard (manual authentication) - console.log("An error occurred so display the oauthCard"); - return next(action); - } - ) - // token exchange was successful, do not show OAuthCard - return; - } - } else { - return next(action); - } - - return next(action); - } - ); - - // hide the upload button - other style options can be added here - const canvasStyleOptions = { - hideUploadButton: true - } - - // Render webchat - if (token && directline) { - if (webChatRef.current && loadingSpinnerRef.current) { - webChatRef.current.style.minHeight = '50vh'; - loadingSpinnerRef.current.style.display = 'none'; - ReactWebChat.renderWebChat( - { - directLine: directline, - store: store, - styleOptions: canvasStyleOptions, - userID: props.userEmail, - }, - webChatRef.current - ); - } else { - console.error("Webchat or loading spinner not found"); - } - } - - }; - - return ( - <> - - -
-
-
-
-
- - - ); -}; - -export default class Chatbot extends React.Component { - constructor(props: IChatbotProps) { - super(props); - } - public render(): JSX.Element { - return ( -
- -
- ); - } -} \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/components/IChatBotProps.ts b/ui/embed/sharepoint-customizer/src/extensions/pvaSso/components/IChatBotProps.ts deleted file mode 100644 index a5edc684..00000000 --- a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/components/IChatBotProps.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface IChatbotProps { - botURL: string; - buttonLabel?: string; - botName?: string; - userEmail: string; - userFriendlyName: string; - botAvatarImage?: string; - botAvatarInitials?: string; - greet?: boolean; - customScope: string; - clientID: string; - authority: string; -} \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/components/MSALWrapper.ts b/ui/embed/sharepoint-customizer/src/extensions/pvaSso/components/MSALWrapper.ts deleted file mode 100644 index 143266d7..00000000 --- a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/components/MSALWrapper.ts +++ /dev/null @@ -1,83 +0,0 @@ -// MSALWrapper.ts -import { PublicClientApplication, AuthenticationResult, - Configuration, InteractionRequiredAuthError} from "@azure/msal-browser"; - -export class MSALWrapper { - private msalConfig: Configuration; - - private msalInstance: PublicClientApplication; - - constructor(clientId: string, authority: string) { - this.msalConfig = { - auth: { - clientId: clientId, - authority: authority, - }, - cache: { - cacheLocation: "localStorage", - }, - }; - - this.msalInstance = new PublicClientApplication(this.msalConfig); - } - - public async handleLoggedInUser(scopes: string[], userEmail: string): Promise { - - let userAccount = null; - const accounts = this.msalInstance.getAllAccounts(); - - if(accounts === null || accounts.length === 0) { - console.log("No users are signed in"); - return null; - } else if (accounts.length > 1) - { - userAccount = this.msalInstance.getAccountByUsername(userEmail); - } else { - userAccount = accounts[0]; - } - - if(userAccount !== null) { - const accessTokenRequest = { - scopes: scopes, - account: userAccount - }; - - return this.msalInstance.acquireTokenSilent(accessTokenRequest).then((response) => { - return response; - }).catch((errorinternal) => { - console.log(errorinternal); - return null; - }); - } - return null; - } - - - public async acquireAccessToken(scopes: string[], userEmail: string): Promise { - - - const accessTokenRequest = { - scopes: scopes, - loginHint: userEmail - } - - return this.msalInstance.ssoSilent(accessTokenRequest).then((response) => { - return response - }).catch((silentError) => { - console.log(silentError); - if (silentError instanceof InteractionRequiredAuthError) { - return this.msalInstance.loginPopup(accessTokenRequest).then((response) => { - return response; - } - ).catch((error) => { - console.log(error); - return null; - }); - } - return null; - }) -} - -} - -export default MSALWrapper; \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/loc/en-us.js b/ui/embed/sharepoint-customizer/src/extensions/pvaSso/loc/en-us.js deleted file mode 100644 index 93e839ba..00000000 --- a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/loc/en-us.js +++ /dev/null @@ -1,7 +0,0 @@ -define([], function() { - return { - "Title": "PvaSsoApplicationCustomizer", - "DefaultButtonLabel": "Chat Now", - "DefaultBotName" : "MCS SSO Sample" - } -}); \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/loc/myStrings.d.ts b/ui/embed/sharepoint-customizer/src/extensions/pvaSso/loc/myStrings.d.ts deleted file mode 100644 index 8ef0b80b..00000000 --- a/ui/embed/sharepoint-customizer/src/extensions/pvaSso/loc/myStrings.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare interface IPvaSsoApplicationCustomizerStrings { - Title: string; - DefaultButtonLabel: string; - DefaultBotName: string; -} - -declare module 'PvaSsoApplicationCustomizerStrings' { - const strings: IPvaSsoApplicationCustomizerStrings; - export = strings; -} diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.manifest.json b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.manifest.json similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.manifest.json rename to ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.manifest.json diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.tsx b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.tsx similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.tsx rename to ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.tsx diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/components/Chat/Chat.tsx b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/Chat.tsx similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/components/Chat/Chat.tsx rename to ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/Chat.tsx diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/components/Chat/acquireToken.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/acquireToken.ts similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/components/Chat/acquireToken.ts rename to ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/acquireToken.ts diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.module.scss b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.module.scss similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.module.scss rename to ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.module.scss diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.tsx b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.tsx similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.tsx rename to ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.tsx diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/loc/en-us.js b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/loc/en-us.js similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/loc/en-us.js rename to ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/loc/en-us.js diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/loc/myStrings.d.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/loc/myStrings.d.ts similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/loc/myStrings.d.ts rename to ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/loc/myStrings.d.ts diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/models/ISidebarAgentProperties.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/models/ISidebarAgentProperties.ts similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/extensions/sidebarAgent/models/ISidebarAgentProperties.ts rename to ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/models/ISidebarAgentProperties.ts diff --git a/ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/scss.d.ts b/ui/embed/sharepoint-customizer/src/scss.d.ts similarity index 100% rename from ui/embed/sharepoint-customizer/SharePointSSOAppCustomizer/src/scss.d.ts rename to ui/embed/sharepoint-customizer/src/scss.d.ts diff --git a/ui/embed/sharepoint-customizer/tsconfig.json b/ui/embed/sharepoint-customizer/tsconfig.json index c4cd392a..419cd861 100644 --- a/ui/embed/sharepoint-customizer/tsconfig.json +++ b/ui/embed/sharepoint-customizer/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json", + "extends": "./node_modules/@microsoft/rush-stack-compiler-5.3/includes/tsconfig-web.json", "compilerOptions": { "target": "es5", "forceConsistentCasingInFileNames": true,