chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,99 @@
# Extend Copilot Studio with Semantic Kernel
This template demonstrates how to build a [Copilot Studio Skill](https://learn.microsoft.com/en-us/microsoft-copilot-studio/advanced-use-skills) that allows to extend agent capabilities with a custom API running in Azure with the help of the Semantic Kernel.
![Copilot Studio using the Semantic Kernel skill within a topic](image.png)
## Rationale
[Microsoft Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/fundamentals-what-is-copilot-studio) is a graphical, low-code tool for both creating an agent — including building automation with Power Automate — and extending a Microsoft 365 Copilot with your own enterprise data and scenarios.
However, in some cases you may need to extend the default agent capabilities by leveraing a pro-code approach, where specific requirements apply.
## Prerequisites
- Azure Subscription
- Azure CLI
- Azure Developer CLI
- Python 3.12 or later
- A Microsoft 365 tenant with Copilot Studio enabled
> [!NOTE]
> You don't need the Azure subscription to be on the same tenant as the Microsoft 365 tenant where Copilot Studio is enabled.
>
> However, you need to have the necessary permissions to register an application in the Azure Active Directory of the tenant where Copilot Studio is enabled.
## Getting Started
1. Clone this repository to your local machine.
```bash
git clone https://github.com/microsoft/semantic-kernel
cd semantic-kernel/python/samples/demos/copilot_studio_skill
```
2. Create a App Registration in Azure Entra ID, with a client secret.
```powershell
az login --tenant <COPILOT-tenant-id>
$appId = az ad app create --display-name "SKCopilotSkill" --query appId -o tsv
$secret = az ad app credential reset --id $appId --append --query password -o tsv
```
4. Run `azd up` to deploy the Azure resources.
```bash
azd auth login --tenant <AZURE-tenant-id>
azd up
```
> [!NOTE]
> When prompted, provide the `botAppId`, `botPassword` and `botTenantId` values from above.
>
> You will also need to input and existing Azure OpenAI resource name and its resource group.
> [!TIP]
> Once the deployment is complete, you can find the URL of the deployed API in the `output` section of the Azure Developer CLI. Copy this URL.
5. Ensure the App Registration `homeUrl` is set to the URL of the deployed API. This is required for the bot to be able to respond to requests from Copilot Studio.
6. Register the bot in Copilot Studio as skill
- Open the Copilot Studio in your Microsoft 365 tenant.
- Create a new agent or reuse an existing one.
- Go to "Settings" in the upper right corner of the agent page.
- Go to the "Skills" tab and click on "Add a skill".
- Now input as URL `API_URL/manifest` where `API_URL` is the URL of the deployed API.
- Click on "Next" to register the skill.
- Once the skill is registered, you can [start using it in your agent](https://learn.microsoft.com/en-us/microsoft-copilot-studio/advanced-use-skills). Edit a Topic or create a new one, and add the skill as a node to the topic flow.
## Architecture
The architecture features `Azure Bot Service` as the main entry point for the requests. The bot service is responsible for routing the requests to the appropriate backend service, which in this case is a custom API running in `Azure Container Apps` leveraging Semantic Kernel.
Below is the updated markdown content with the new call included:
```mermaid
flowchart LR
subgraph Clients
A[Copilot Studio]
end
C[Azure Bot Service]
D["SK App<br/>(Azure Container Apps)"]
A -- "Initiates Request" --> C
C -- "Forwards Request" --> D
D -- "Processes & Returns Response" --> C
C -- "Routes Response" --> A
%% Una tantum call to fetch manifest directly from SK App
A -- "Fetch Manifest" --> D
```
### Implementation
Please refer to the original [Bot Framework documentation](https://learn.microsoft.com/en-us/azure/bot-service/skill-implement-skill?view=azure-bot-service-4.0&tabs=python) for more details on how to implement the bot service skill and the custom API.
> [!NOTE]
> As of today, Bot Framework SDK offers only `aiohttp` support for Python.
@@ -0,0 +1,11 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: azure-bot
services:
api:
project: src/api
host: containerapp
language: python
docker:
path: dockerfile
remoteBuild: true
Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

@@ -0,0 +1,115 @@
param uniqueId string
param prefix string
param userAssignedIdentityResourceId string
param userAssignedIdentityClientId string
param openAiEndpoint string
param openAiApiKey string
param openAiApiVersion string = '2024-08-01-preview'
param openAiModel string = 'gpt-4o'
param applicationInsightsConnectionString string
param containerRegistry string = '${prefix}acr${uniqueId}'
param location string = resourceGroup().location
param logAnalyticsWorkspaceName string
param apiAppExists bool
param emptyContainerImage string = 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
param botAppId string
@secure()
param botPassword string
param botTenantId string
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = {
name: logAnalyticsWorkspaceName
}
// see https://azureossd.github.io/2023/01/03/Using-Managed-Identity-and-Bicep-to-pull-images-with-Azure-Container-Apps/
resource containerAppEnv 'Microsoft.App/managedEnvironments@2023-11-02-preview' = {
name: '${prefix}-containerAppEnv-${uniqueId}'
location: location
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${userAssignedIdentityResourceId}': {}
}
}
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalyticsWorkspace.properties.customerId
sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey
}
}
}
}
// When azd passes parameters, it will tell if apps were already created
// In this case, we don't overwrite the existing image
// See https://johnnyreilly.com/using-azd-for-faster-incremental-azure-container-app-deployments-in-azure-devops#the-does-your-service-exist-parameter
module fetchLatestImageApi './fetch-container-image.bicep' = {
name: 'api-app-image'
params: {
exists: apiAppExists
name: '${prefix}-api-${uniqueId}'
}
}
resource apiContainerApp 'Microsoft.App/containerApps@2023-11-02-preview' = {
name: '${prefix}-api-${uniqueId}'
location: location
tags: { 'azd-service-name': 'api' }
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${userAssignedIdentityResourceId}': {}
}
}
properties: {
managedEnvironmentId: containerAppEnv.id
configuration: {
activeRevisionsMode: 'Single'
ingress: {
external: true
targetPort: 80
transport: 'auto'
}
registries: [
{
server: '${containerRegistry}.azurecr.io'
identity: userAssignedIdentityResourceId
}
]
}
template: {
scale: {
minReplicas: 1
maxReplicas: 1
}
containers: [
{
name: 'api'
image: apiAppExists ? fetchLatestImageApi.outputs.containers[0].image : emptyContainerImage
resources: {
cpu: 1
memory: '2Gi'
}
env: [
{ name: 'AZURE_CLIENT_ID', value: userAssignedIdentityClientId }
{ name: 'BOT_APP_ID', value: botAppId }
{ name: 'BOT_PASSWORD', value: botPassword }
{ name: 'BOT_TENANT_ID', value: botTenantId }
{ name: 'APPLICATIONINSIGHTS_CONNECTIONSTRING', value: applicationInsightsConnectionString }
{ name: 'APPLICATIONINSIGHTS_SERVICE_NAME', value: 'api' }
{ name: 'AZURE_OPENAI_ENDPOINT', value: openAiEndpoint }
{ name: 'AZURE_OPENAI_CHAT_DEPLOYMENT_NAME', value: openAiModel }
{ name: 'AZURE_OPENAI_API_KEY', value: '' }
{ name: 'AZURE_OPENAI_API_VERSION', value: openAiApiVersion }
]
}
]
}
}
}
output messagesEndpoint string = 'https://${apiContainerApp.properties.configuration.ingress.fqdn}/api/messages'
output manifestUrl string = 'https://${apiContainerApp.properties.configuration.ingress.fqdn}/manifest'
output homeUrl string = 'https://${apiContainerApp.properties.configuration.ingress.fqdn}'
@@ -0,0 +1,29 @@
param uniqueId string
param prefix string
param userAssignedIdentityPrincipalId string
param acrName string = '${prefix}acr${uniqueId}'
param location string = resourceGroup().location
resource acr 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = {
name: acrName
location: location
sku: {
name: 'Standard' // Choose between Basic, Standard, and Premium based on your needs
}
properties: {
adminUserEnabled: false
}
}
resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
name: guid(acr.id, userAssignedIdentityPrincipalId, 'acrpull')
scope: acr
properties: {
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // Role definition ID for AcrPull
principalId: userAssignedIdentityPrincipalId
principalType: 'ServicePrincipal'
}
}
output acrName string = acrName
output acrEndpoint string = acr.properties.loginServer
@@ -0,0 +1,46 @@
param uniqueId string
param prefix string
@secure()
param userAssignedIdentityPrincipalId string
param location string = resourceGroup().location
param appInsightsName string = '${prefix}-appin-${uniqueId}'
param logAnalyticsWorkspaceName string = '${prefix}-law-${uniqueId}'
// Create or reference an existing Log Analytics Workspace
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2020-08-01' = {
name: logAnalyticsWorkspaceName
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}
// Create Application Insights resource linked to the Log Analytics Workspace
resource applicationInsights 'Microsoft.Insights/components@2020-02-02-preview' = {
name: appInsightsName
location: location
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalyticsWorkspace.id
}
}
// Assign "Monitoring Metrics Publisher" role to the Application Insights resource
resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
name: guid(applicationInsights.id, userAssignedIdentityPrincipalId, 'appinsightsPublisher')
scope: applicationInsights
properties: {
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '3913510d-42f4-4e42-8a64-420c390055eb') // Role definition ID for Monitoring Metrics Publisher
principalId: userAssignedIdentityPrincipalId
principalType: 'ServicePrincipal'
}
}
output logAnalyticsWorkspaceId string = logAnalyticsWorkspace.id
output logAnalyticsWorkspaceName string = logAnalyticsWorkspaceName
output applicationInsightsInstrumentationKey string = applicationInsights.properties.InstrumentationKey
output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString
@@ -0,0 +1,37 @@
param uniqueId string
param prefix string
param messagesEndpoint string
param botAppId string
param botTenantId string
resource bot 'Microsoft.BotService/botServices@2023-09-15-preview' = {
name: '${prefix}bot${uniqueId}'
location: 'global'
sku: {
name: 'F0'
}
kind: 'azurebot'
properties: {
iconUrl: 'https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png'
displayName: '${prefix}bot${uniqueId}'
endpoint: messagesEndpoint
description: 'Bot created by Bicep'
publicNetworkAccess: 'Enabled'
msaAppId: botAppId
msaAppTenantId: botTenantId
msaAppType: 'SingleTenant'
msaAppMSIResourceId: null
schemaTransformationVersion: '1.3'
isStreamingSupported: false
}
}
// Connect the bot service to Microsoft Teams
resource botServiceMsTeamsChannel 'Microsoft.BotService/botServices/channels@2021-03-01' = {
parent: bot
location: 'global'
name: 'MsTeamsChannel'
properties: {
channelName: 'MsTeamsChannel'
}
}
@@ -0,0 +1,8 @@
param exists bool
param name string
resource existingApp 'Microsoft.App/containerApps@2023-05-01' existing = if (exists) {
name: name
}
output containers array = exists ? existingApp.properties.template.containers : []
@@ -0,0 +1,141 @@
targetScope = 'subscription'
@minLength(1)
@maxLength(64)
@description('Name of the environment that can be used as part of naming resource convention')
param environmentName string
@description('The current user ID, to assign RBAC permissions to')
param currentUserId string
// Main deployment parameters
param prefix string = 'copstsk'
@minLength(1)
@description('Primary location for all resources')
param location string
@minLength(1)
@description('Name of the Azure OpenAI resource')
param openAIName string
@minLength(1)
@description('Name of the Azure Resource Group where the OpenAI resource is located')
param openAIResourceGroupName string
@description('Azure Bot app ID')
param botAppId string
@description('Azure Bot app password')
@secure()
param botPassword string
@description('Azure Bot tenant ID')
param botTenantId string
param openAIModel string
param openAIApiVersion string
param apiAppExists bool = false
param runningOnGh string = ''
var tags = {
'azd-env-name': environmentName
}
resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = {
name: 'rg-${environmentName}'
location: location
tags: tags
}
var uniqueId = uniqueString(rg.id)
var principalType = empty(runningOnGh) ? 'User' : 'ServicePrincipal'
module uami './uami.bicep' = {
name: 'uami'
scope: rg
params: {
uniqueId: uniqueId
prefix: prefix
location: location
}
}
module appin './appin.bicep' = {
name: 'appin'
scope: rg
params: {
uniqueId: uniqueId
prefix: prefix
location: location
userAssignedIdentityPrincipalId: uami.outputs.principalId
}
}
module acrModule './acr.bicep' = {
name: 'acr'
scope: rg
params: {
uniqueId: uniqueId
prefix: prefix
userAssignedIdentityPrincipalId: uami.outputs.principalId
location: location
}
}
module openAI './openAI.bicep' = {
name: 'openAI'
scope: resourceGroup(openAIResourceGroupName)
params: {
openAIName: openAIName
userAssignedIdentityPrincipalId: uami.outputs.principalId
}
}
module aca './aca.bicep' = {
name: 'aca'
scope: rg
params: {
uniqueId: uniqueId
prefix: prefix
userAssignedIdentityResourceId: uami.outputs.identityId
containerRegistry: acrModule.outputs.acrName
location: location
logAnalyticsWorkspaceName: appin.outputs.logAnalyticsWorkspaceName
applicationInsightsConnectionString: appin.outputs.applicationInsightsConnectionString
openAiApiKey: '' // Force ManId, otherwise set openAI.listKeys().key1
openAiEndpoint: openAI.outputs.openAIEndpoint
openAiModel: openAIModel
openAiApiVersion: openAIApiVersion
userAssignedIdentityClientId: uami.outputs.clientId
apiAppExists: apiAppExists
botAppId: botAppId
botPassword: botPassword
botTenantId: botTenantId
}
}
module bot 'bot.bicep' = {
name: 'bot'
scope: rg
params: {
uniqueId: uniqueId
prefix: prefix
botAppId: botAppId
botTenantId: botTenantId
messagesEndpoint: aca.outputs.messagesEndpoint
}
}
// These outputs are copied by azd to .azure/<env name>/.env file
// post provision script will use these values, too
output AZURE_RESOURCE_GROUP string = rg.name
output APPLICATIONINSIGHTS_CONNECTIONSTRING string = appin.outputs.applicationInsightsConnectionString
output AZURE_TENANT_ID string = subscription().tenantId
output AZURE_USER_ASSIGNED_IDENTITY_ID string = uami.outputs.identityId
output AZURE_CONTAINER_REGISTRY_ENDPOINT string = acrModule.outputs.acrEndpoint
output AZURE_OPENAI_MODEL string = openAIModel
output AZURE_OPENAI_ENDPOINT string = openAI.outputs.openAIEndpoint
output AZURE_OPENAI_API_VERSION string = openAIApiVersion
output ENDPOINT_URL string = aca.outputs.messagesEndpoint
output MANIFEST_URL string = aca.outputs.manifestUrl
output HOME_URL string = aca.outputs.homeUrl
output BOT_APP_ID string = botAppId
output BOT_TENANT_ID string = botTenantId
@@ -0,0 +1,42 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": {
"value": "${AZURE_ENV_NAME}"
},
"currentUserId": {
"value": "${AZURE_PRINCIPAL_ID}"
},
"runningOnGh": {
"value": "${GITHUB_ACTIONS}"
},
"location": {
"value": "${AZURE_LOCATION}"
},
"openAiName": {
"value": "${AZURE_OPENAI_NAME}"
},
"openAiResourceGroupName": {
"value": "${AZURE_OPENAI_RG}"
},
"openAIModel": {
"value": "${AZURE_OPENAI_MODEL=gpt-4o}"
},
"openAIApiVersion": {
"value": "${AZURE_OPENAI_API_VERSION=2024-08-01-preview}"
},
"apiAppExists": {
"value": "${SERVICE_API_RESOURCE_EXISTS=false}"
},
"botAppId": {
"value": "${BOT_APPID}"
},
"botPassword": {
"value": "${BOT_PASSWORD}"
},
"botTenantId": {
"value": "${BOT_TENANT_ID}"
}
}
}
@@ -0,0 +1,20 @@
targetScope = 'resourceGroup'
param openAIName string
param userAssignedIdentityPrincipalId string
resource openAI 'Microsoft.CognitiveServices/accounts@2022-03-01' existing = {
name: openAIName
}
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
name: guid(openAI.id, userAssignedIdentityPrincipalId, 'Cognitive Services OpenAI User')
scope: openAI
properties: {
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd') // Role definition ID for Cognitive Services OpenAI User
principalId: userAssignedIdentityPrincipalId
principalType: 'ServicePrincipal'
}
}
output openAIEndpoint string = openAI.properties.endpoint
@@ -0,0 +1,13 @@
param uniqueId string
param prefix string
param location string = resourceGroup().location
param identityName string = '${prefix}uami${uniqueId}'
resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
name: identityName
location: location
}
output identityId string = userAssignedIdentity.id
output clientId string = userAssignedIdentity.properties.clientId
output principalId string = userAssignedIdentity.properties.principalId
@@ -0,0 +1,47 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*.pyd
# Caches of various types
.cache/
.pip/
# Development environments
.env
.venv/
venv/
ENV/
# Version control
.git/
.gitignore
.github/
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Project backups
*.bak
# Log files
*.log
# OS generated files
.DS_Store
Thumbs.db
# Editor directories and files
.idea/
.vscode/
*.swp
*.swo
*~
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft. All rights reserved.
import sys
import traceback
from botbuilder.core import (
MessageFactory,
TurnContext,
)
from botbuilder.integration.aiohttp import (
CloudAdapter,
ConfigurationBotFrameworkAuthentication,
)
from botbuilder.schema import Activity, ActivityTypes, InputHints
class AdapterWithErrorHandler(CloudAdapter):
def __init__(
self,
settings: ConfigurationBotFrameworkAuthentication,
):
super().__init__(settings)
self.on_turn_error = self._handle_turn_error
async def _handle_turn_error(self, turn_context: TurnContext, error: Exception):
# This check writes out errors to console log
# NOTE: In production environment, you should consider logging this to Azure
# application insights.
print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
traceback.print_exc()
await self._send_error_message(turn_context, error)
await self._send_eoc_to_parent(turn_context, error)
async def _send_error_message(self, turn_context: TurnContext, error: Exception):
try:
# Send a message to the user.
error_message_text = "The skill encountered an error or bug."
error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input)
await turn_context.send_activity(error_message)
error_message_text = "To continue to run this bot, please fix the bot source code."
error_message = MessageFactory.text(error_message_text, error_message_text, InputHints.ignoring_input)
await turn_context.send_activity(error_message)
# Send a trace activity, which will be displayed in Bot Framework Emulator.
await turn_context.send_trace_activity(
label="TurnError",
name="on_turn_error Trace",
value=f"{error}",
value_type="https://www.botframework.com/schemas/error",
)
except Exception as exception:
print(
f"\n Exception caught on _send_error_message : {exception}",
file=sys.stderr,
)
traceback.print_exc()
async def _send_eoc_to_parent(self, turn_context: TurnContext, error: Exception):
try:
# Send an EndOfConversation activity to the skill caller with the error to end the conversation,
# and let the caller decide what to do.
end_of_conversation = Activity(type=ActivityTypes.end_of_conversation)
end_of_conversation.code = "SkillError"
end_of_conversation.text = str(error)
await turn_context.send_activity(end_of_conversation)
except Exception as exception:
print(
f"\n Exception caught on _send_eoc_to_parent : {exception}",
file=sys.stderr,
)
traceback.print_exc()
@@ -0,0 +1,55 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
import os
from aiohttp import web
from aiohttp.web import Request, Response
from bot import bot
from config import config
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Endpoint for processing messages
async def messages(req: Request):
"""
Endpoint for processing messages with the Skill Bot.
"""
logger.info("Received a message.")
body = await req.json()
logger.info("Request body: %s", body)
# Process the incoming request
# NOTE in the context of Skills, we MUST return the response to the Copilot Studio as the response to the request
# In other channel (ex. Teams), this would not be required, and activities would be sent to the Bot Framework
return await bot.process(req)
async def copilot_manifest(req: Request):
# load manifest from file and interpolate with env vars
with open("copilot-studio.manifest.json") as f:
manifest = f.read()
# Get container app current ingress fqdn
# See https://learn.microsoft.com/en-us/azure/container-apps/environment-variables?tabs=portal
fqdn = f"https://{os.getenv('CONTAINER_APP_NAME')}.{os.getenv('CONTAINER_APP_ENV_DNS_SUFFIX')}/api/messages"
manifest = manifest.replace("__botEndpoint", fqdn).replace("__botAppId", config.APP_ID)
return Response(
text=manifest,
content_type="application/json",
)
APP = web.Application()
APP.router.add_post("/api/messages", messages)
APP.router.add_get("/manifest", copilot_manifest)
if __name__ == "__main__":
try:
web.run_app(APP, host=config.HOST, port=config.PORT)
except Exception as error:
raise error
@@ -0,0 +1,41 @@
# Copyright (c) Microsoft. All rights reserved.
# See https://github.com/microsoft/BotBuilder-Samples/blob/main/samples/python/80.skills-simple-bot-to-bot/echo-skill-bot/authentication/allowed_callers_claims_validator.py
from collections.abc import Awaitable, Callable
from botframework.connector.auth import JwtTokenValidation, SkillValidation
from config import Config
class AllowedCallersClaimsValidator:
config_key = "ALLOWED_CALLERS"
def __init__(self, config: Config):
if not config:
raise TypeError("AllowedCallersClaimsValidator: config object cannot be None.")
# ALLOWED_CALLERS is the setting in config.py file
# that consists of the list of parent bot ids that are allowed to access the skill
# to add a new parent bot simply go to the AllowedCallers and add
# the parent bot's microsoft app id to the list
caller_list = getattr(config, self.config_key)
if caller_list is None:
raise TypeError(f'"{self.config_key}" not found in configuration.')
self._allowed_callers = frozenset(caller_list)
@property
def claims_validator(self) -> Callable[[list[dict]], Awaitable]:
async def allow_callers_claims_validator(claims: dict[str, object]):
# if allowed_callers is None we allow all calls
if "*" not in self._allowed_callers and SkillValidation.is_skill_claim(claims):
# Check that the appId claim in the skill request is in the list of skills configured for this bot.
app_id = JwtTokenValidation.get_app_id_from_claims(claims)
if app_id not in self._allowed_callers:
raise PermissionError(
f'Received a request from a bot with an app ID of "{app_id}".'
f" To enable requests from this caller, add the app ID to your configuration file."
)
return
return allow_callers_claims_validator
@@ -0,0 +1,79 @@
# Copyright (c) Microsoft. All rights reserved.
from adapter import AdapterWithErrorHandler
# Custom classes to handle errors and claims validation
from auth import AllowedCallersClaimsValidator
from botbuilder.core import MemoryStorage, MessageFactory, TurnContext
from botbuilder.integration.aiohttp import ConfigurationBotFrameworkAuthentication
from botbuilder.schema import (
Activity,
EndOfConversationCodes,
InputHints,
)
from botframework.connector.auth import AuthenticationConfiguration
from config import config
# This is the SK agent that will be used to handle the conversation
from sk_conversation_agent import agent
from teams import Application, ApplicationOptions
from teams.state import TurnState
from semantic_kernel.contents import ChatHistory
# This is required for bot to work as Copilot Skill,
# not adding a claims validator will result in an error
claims_validator = AllowedCallersClaimsValidator(config)
auth = AuthenticationConfiguration(tenant_id=config.APP_TENANTID, claims_validator=claims_validator.claims_validator)
# Create the bot application
# We use the Teams Application class to create the bot application,
# then we added a custom adapter for skill errors handling.
bot = Application[TurnState](
ApplicationOptions(
bot_app_id=config.APP_ID,
storage=MemoryStorage(),
# CANNOT PASS A DICT HERE; MUST PASS A CLASS WITH APP_ID, APP_PASSWORD, AND APP_TENANTID ATTRIBUTES
adapter=AdapterWithErrorHandler(ConfigurationBotFrameworkAuthentication(config, auth_configuration=auth)),
)
)
@bot.before_turn
async def setup_chathistory(context: TurnContext, state: TurnState):
chat_history = state.conversation.get("chat_history") or ChatHistory()
state.conversation["chat_history"] = chat_history
return state
@bot.activity("message")
async def on_message(context: TurnContext, state: TurnState):
user_message = context.activity.text
# Get the chat_history from the conversation state
chat_history: ChatHistory = state.conversation.get("chat_history")
# Add the new user message
chat_history.add_user_message(user_message)
# Get the response from the semantic kernel agent (v1.22.0 and later)
sk_response = await agent.get_response(history=chat_history, user_input=user_message)
# Store the updated chat_history back into conversation state
state.conversation["chat_history"] = chat_history
# Send the response back to the user
# NOTE in the context of a Copilot Skill,
# the response is sent as a Response from /api/messages endpoint
await context.send_activity(MessageFactory.text(sk_response, input_hint=InputHints.ignoring_input))
# Skills must send an EndOfConversation activity to indicate the conversation is complete
# NOTE: this is a simple example, in a real skill you would likely want to send this
# only when the user has completed their task
end = Activity.create_end_of_conversation_activity()
end.code = EndOfConversationCodes.completed_successfully
await context.send_activity(end)
return True
@@ -0,0 +1,45 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from dotenv import load_dotenv
load_dotenv(override=True)
class Config:
"""Bot Configuration"""
HOST = os.getenv("HOST", "localhost")
PORT = int(os.getenv("PORT", 8080))
# DO NOT CHANGE THIS KEYS!!
# These keys are used to validate the bot's identity
# and must match these named as Bot configuration expects
APP_ID = os.getenv("BOT_APP_ID")
APP_PASSWORD = os.getenv("BOT_PASSWORD")
APP_TENANTID = os.getenv("BOT_TENANT_ID")
APP_TYPE = os.getenv("APP_TYPE", "singletenant")
# Required for Copilot Skill
# Can be a list of allowed agent Ids,
# or "*" to allow any agent
ALLOWED_CALLERS = os.getenv("ALLOWED_CALLERS", ["*"])
# Required for Azure OpenAI
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME = os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME")
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
AZURE_OPENAI_API_VERSION = os.getenv("AZURE_OPENAI_API_VERSION")
def validate(self):
if not self.HOST or not self.PORT:
raise Exception("Missing required configuration. HOST and PORT must be set.")
if not self.APP_ID or not self.APP_PASSWORD or not self.APP_TENANTID:
raise Exception("Missing required configuration. APP_ID, APP_PASSWORD, and APP_TENANT_ID must be set.")
if not self.ALLOWED_CALLERS:
raise Exception("Missing required configuration. ALLOWED_CALLERS must be set.")
config = Config()
config.validate()
@@ -0,0 +1,25 @@
{
"$schema": "https://schemas.botframework.com/schemas/skills/v2.2/skill-manifest.json",
"$id": "SKCopilotSkill",
"name": "SK Copilot Skill",
"version": "1.0",
"description": "This is a sample skill using Semantic Kernel",
"publisherName": "Microsoft",
"privacyUrl": "https://www.microsoft.com/en-us/privacy/privacystatement",
"iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png",
"endpoints": [
{
"name": "default",
"protocol": "BotFrameworkV3",
"description": "Default endpoint for the bot",
"endpointUrl": "__botEndpoint",
"msAppId": "__botAppId"
}
],
"activities": {
"message": {
"type": "message",
"description": "Invoke Semantic Kernel skill"
}
}
}
@@ -0,0 +1,23 @@
FROM python:3.12-slim
# Step 1 - Install dependencies
WORKDIR /app
# Step 2 - Copy only requirements.txt
COPY requirements.txt /app
# Step 4 - Install pip dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Step 5 - Copy the rest of the files
COPY . .
ENV PYTHONUNBUFFERED=1
# Expose the application port
EXPOSE 80
ENV HOST 0.0.0.0
ENV PORT 80
# do not change the arguments
CMD ["python", "app.py"]
@@ -0,0 +1,4 @@
python-dotenv>=1.0.1
botbuilder-integration-aiohttp>=4.15.0
teams-ai>=1.4.0,<2.0.0
semantic-kernel>=1.22.0
@@ -0,0 +1,12 @@
# Copyright (c) Microsoft. All rights reserved.
from azure.identity import AzureCliCredential
from semantic_kernel.agents import ChatCompletionAgent
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
agent = ChatCompletionAgent(
service=AzureChatCompletion(credential=AzureCliCredential()),
name="ChatAgent",
instructions="You invent jokes to have a fun conversation with the user.",
)