mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
Copilot Chat: Removing copilot-chat-app from semantic-kernel repo as it has moved t… (#2282)
Removing copilot-chat-app from semantic-kernel repo. ### Motivation and Context The semantic-kernel copilot-chat-app sample has moved to [its own repository](https://github.com/microsoft/chat-copilot). ### Description Removed all files related to the copilot-chat-app sample and replaced them with a README linking to the new repository. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄 --------- Co-authored-by: Ben Thomas <[email protected]>
This commit is contained in:
@@ -1,59 +0,0 @@
|
||||
#
|
||||
# This workflow will package the Copilot Chat application for deployment.
|
||||
#
|
||||
|
||||
name: copilot-chat-package
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "main", "feature*" ]
|
||||
paths:
|
||||
- 'samples/apps/copilot-chat-app/**'
|
||||
push:
|
||||
branches: [ "main", "feature*" ]
|
||||
paths:
|
||||
- 'samples/apps/copilot-chat-app/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { dotnet: '6.0', configuration: Release, os: ubuntu-latest }
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
NUGET_CERT_REVOCATION_MODE: offline
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
clean: true
|
||||
|
||||
- name: Pull container dotnet/sdk:${{ matrix.dotnet }}
|
||||
run: docker pull mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }}
|
||||
|
||||
- name: Package Copilot Chat WebAPI
|
||||
run: |
|
||||
chmod +x $(pwd)/samples/apps/copilot-chat-app/deploy/package-webapi.sh;
|
||||
docker run --rm -v $(pwd):/app -w /app -e GITHUB_ACTIONS='true' mcr.microsoft.com/dotnet/sdk:${{ matrix.dotnet }} /bin/sh -c "/app/samples/apps/copilot-chat-app/deploy/package-webapi.sh --no-zip";
|
||||
|
||||
- name: Set version tag
|
||||
id: versiontag
|
||||
run: |
|
||||
VERSION_TAG="$(date +'%Y%m%d').${{ github.run_number }}.${{ github.run_attempt }}"
|
||||
echo $VERSION_TAG
|
||||
echo "versiontag=$VERSION_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload package to artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: copilotchat-webapi-${{ steps.versiontag.outputs.versiontag }}
|
||||
path: ./samples/apps/copilot-chat-app/deploy/publish
|
||||
@@ -1,97 +0,0 @@
|
||||
name: Copilot Chat Tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "samples/apps/copilot-chat-app/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: samples/apps/copilot-chat-app/webapp
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache-dependency-path: samples/apps/copilot-chat-app/webapp/yarn.lock
|
||||
cache: "yarn"
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Update AIService configuration
|
||||
working-directory: samples/apps/copilot-chat-app/webapi
|
||||
env:
|
||||
AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }}
|
||||
AzureOpenAI__Endpoint: ${{ secrets.AZUREOPENAI__ENDPOINT }}
|
||||
run: |
|
||||
dotnet dev-certs https
|
||||
dotnet user-secrets set "AIService:Key" "$AzureOpenAI__ApiKey"
|
||||
dotnet user-secrets set "AIService:Endpoint" "$AzureOpenAI__Endpoint"
|
||||
|
||||
- name: Start service in background
|
||||
working-directory: samples/apps/copilot-chat-app/webapi
|
||||
run: |
|
||||
dotnet run > service-log.txt 2>&1 &
|
||||
for attempt in {0..20}; do
|
||||
jobs
|
||||
echo 'Waiting for service to start...';
|
||||
if curl -k https://localhost:40443/healthz; then
|
||||
echo;
|
||||
echo 'Service started';
|
||||
break;
|
||||
fi;
|
||||
|
||||
sleep 5;
|
||||
done
|
||||
|
||||
- name: Run Playwright tests
|
||||
env:
|
||||
REACT_APP_BACKEND_URI: https://localhost:40443/
|
||||
REACT_APP_AAD_CLIENT_ID: ${{ secrets.COPILOT_CHAT_REACT_APP_AAD_CLIENT_ID }}
|
||||
REACT_APP_AAD_AUTHORITY: https://login.microsoftonline.com/common
|
||||
|
||||
REACT_APP_TEST_USER_ACCOUNT1: ${{ secrets.COPILOT_CHAT_TEST_USER_ACCOUNT1 }}
|
||||
REACT_APP_TEST_USER_PASSWORD1: ${{ secrets.COPILOT_CHAT_TEST_USER_PASSWORD1 }}
|
||||
REACT_APP_TEST_USER_ACCOUNT2: ${{ secrets.COPILOT_CHAT_TEST_USER_ACCOUNT2 }}
|
||||
REACT_APP_TEST_USER_PASSWORD2: ${{ secrets.COPILOT_CHAT_TEST_USER_PASSWORD2 }}
|
||||
|
||||
REACT_APP_TEST_JIRA_EMAIL: ${{ secrets.COPILOT_CHAT_TEST_JIRA_EMAIL }}
|
||||
REACT_APP_TEST_JIRA_ACCESS_TOKEN: ${{ secrets.COPILOT_CHAT_TEST_JIRA_ACCESS_TOKEN }}
|
||||
REACT_APP_TEST_JIRA_SERVER_URL: ${{ secrets.COPILOT_CHAT_TEST_JIRA_SERVER_URL }}
|
||||
|
||||
REACT_APP_TEST_GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REACT_APP_TEST_GITHUB_ACCOUNT_OWNER: ${{ secrets.COPILOT_CHAT_TEST_GITHUB_ACCOUNT_OWNER }}
|
||||
REACT_APP_TEST_GITHUB_REPOSITORY_NAME: ${{ secrets.COPILOT_CHAT_TEST_GITHUB_REPOSITORY_NAME }}
|
||||
run: yarn playwright test
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: samples/apps/copilot-chat-app/webapp/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: service-log
|
||||
path: samples/apps/copilot-chat-app/webapi/service-log.txt
|
||||
retention-days: 30
|
||||
@@ -1,9 +1,9 @@
|
||||
name: Label title prefix
|
||||
on:
|
||||
issues:
|
||||
types: [ labeled ]
|
||||
types: [labeled]
|
||||
pull_request_target:
|
||||
types: [ labeled ]
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
add_title_prefix:
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
name: "Issue/PR: update title"
|
||||
@@ -23,10 +23,9 @@ jobs:
|
||||
let prefixLabels = {
|
||||
"python": "Python",
|
||||
"java": "Java",
|
||||
".NET": ".Net",
|
||||
"copilot chat": "Copilot Chat"
|
||||
".NET": ".Net"
|
||||
};
|
||||
|
||||
|
||||
function addTitlePrefix(title, prefix)
|
||||
{
|
||||
// Update the title based on the label and prefix
|
||||
@@ -41,12 +40,12 @@ jobs:
|
||||
title = prefix + ": " + title;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
|
||||
labelAdded = context.payload.label.name
|
||||
|
||||
|
||||
// Check if the issue or PR has the label
|
||||
if (labelAdded in prefixLabels) {
|
||||
let prefix = prefixLabels[labelAdded];
|
||||
@@ -59,7 +58,7 @@ jobs:
|
||||
title: addTitlePrefix(context.payload.issue.title, prefix)
|
||||
});
|
||||
break
|
||||
|
||||
|
||||
case 'pull_request_target':
|
||||
github.rest.pulls.update({
|
||||
pull_number: context.issue.number,
|
||||
|
||||
Vendored
-13
@@ -1,19 +1,6 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": ".NET Core Launch (CopilotChatWebApi)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build (CopilotChatWebApi)",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/samples/apps/copilot-chat-app/webapi/bin/Debug/net6.0/CopilotChatWebApi.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/samples/apps/copilot-chat-app/webapi",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
|
||||
Vendored
-132
@@ -225,131 +225,6 @@
|
||||
// ****************
|
||||
// Samples (dotnet)
|
||||
// ****************
|
||||
// Copilot Chat
|
||||
{
|
||||
"label": "run (Copilot Chat)",
|
||||
"detail": "Run all copilot chat components",
|
||||
"group": "test",
|
||||
"dependsOn": ["run (CopilotChatWebApi)", "run (CopilotChatApp)"],
|
||||
"dependsOrder": "parallel"
|
||||
},
|
||||
// Copilot Setup
|
||||
{
|
||||
"label": "install (CopilotChatApp)",
|
||||
"detail": "Install all copilot chat app dependencies",
|
||||
"type": "shell",
|
||||
"group": "build",
|
||||
"command": "yarn",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"showReuseMessage": false,
|
||||
"panel": "shared",
|
||||
"group": "buildTasks"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/samples/apps/copilot-chat-app/webapp"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "setup (Copilot Chat)",
|
||||
"detail": "Setup (like setting secrets) for copilot chat app and api",
|
||||
"group": "test",
|
||||
"dependsOn": ["GetSecret (AIService:Key)"],
|
||||
"dependsOrder": "sequence"
|
||||
// TODO -- add tasks for configuring environment variables
|
||||
},
|
||||
{
|
||||
"label": "GetSecret (AIService:Key)",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"user-secrets",
|
||||
"set",
|
||||
"AIService:Key",
|
||||
"${input:aiServiceSecret}"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/samples/apps/copilot-chat-app/webapi"
|
||||
}
|
||||
},
|
||||
// Copilot Chat App
|
||||
{
|
||||
"label": "build (CopilotChatApp)",
|
||||
"type": "shell",
|
||||
"group": "build",
|
||||
"command": "yarn build",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"group": "buildTasks"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/samples/apps/copilot-chat-app/webapp"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "run (CopilotChatApp)",
|
||||
"type": "shell",
|
||||
"group": "test",
|
||||
"command": "yarn start",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"group": "copilot"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/samples/apps/copilot-chat-app/webapp"
|
||||
}
|
||||
},
|
||||
// Copilot Chat Api
|
||||
{
|
||||
"label": "build (CopilotChatWebApi)",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/samples/apps/copilot-chat-app/webapi/CopilotChatWebApi.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary",
|
||||
"/property:DebugType=portable"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "run (CopilotChatWebApi)",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/samples/apps/copilot-chat-app/webapi/CopilotChatWebApi.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "test",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"group": "copilot"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "watch (CopilotChatWebApi)",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/samples/apps/copilot-chat-app/webapi/CopilotChatWebApi.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
// Kernel Syntax Examples
|
||||
{
|
||||
"label": "build (KernelSyntaxExamples)",
|
||||
@@ -466,13 +341,6 @@
|
||||
"type": "promptString",
|
||||
"default": "",
|
||||
"description": "Enter a filter for the tests"
|
||||
},
|
||||
{
|
||||
"id": "aiServiceSecret",
|
||||
"type": "promptString",
|
||||
"default": "",
|
||||
"description": "Enter a secret for Copilot Chat AIService:Key",
|
||||
"password": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
-134
@@ -1,134 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
// Copilot Chat
|
||||
{
|
||||
"label": "run (Copilot Chat)",
|
||||
"detail": "Run all copilot chat components",
|
||||
"group": "test",
|
||||
"dependsOn": ["run (CopilotChatWebApi)", "run (CopilotChatApp)"],
|
||||
"dependsOrder": "parallel"
|
||||
},
|
||||
// Copilot Setup
|
||||
{
|
||||
"label": "install (CopilotChatApp)",
|
||||
"detail": "Install all copilot chat app dependencies",
|
||||
"type": "shell",
|
||||
"group": "build",
|
||||
"command": "yarn",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"showReuseMessage": false,
|
||||
"panel": "shared",
|
||||
"group": "buildTasks"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/samples/apps/copilot-chat-app/webapp"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "setup (Copilot Chat)",
|
||||
"detail": "Setup (like setting secrets) for copilot chat app and api",
|
||||
"group": "test",
|
||||
"dependsOn": ["GetSecret (AIService:Key)"],
|
||||
"dependsOrder": "sequence"
|
||||
// TODO -- add tasks for configuring environment variables
|
||||
},
|
||||
{
|
||||
"label": "GetSecret (AIService:Key)",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": ["user-secrets", "set", "AIService:Key", "${input:aiServiceSecret}"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/samples/apps/copilot-chat-app/webapi"
|
||||
}
|
||||
},
|
||||
// Copilot Chat App
|
||||
{
|
||||
"label": "build (CopilotChatApp)",
|
||||
"type": "shell",
|
||||
"group": "build",
|
||||
"command": "yarn build",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"group": "buildTasks"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/samples/apps/copilot-chat-app/webapp"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "run (CopilotChatApp)",
|
||||
"type": "shell",
|
||||
"group": "test",
|
||||
"command": "yarn start",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"group": "copilot"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/samples/apps/copilot-chat-app/webapp"
|
||||
}
|
||||
},
|
||||
// Copilot Chat Api
|
||||
{
|
||||
"label": "build (CopilotChatWebApi)",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/samples/apps/copilot-chat-app/webapi/CopilotChatWebApi.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary",
|
||||
"/property:DebugType=portable"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "run (CopilotChatWebApi)",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/samples/apps/copilot-chat-app/webapi/CopilotChatWebApi.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "test",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"group": "copilot"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "watch (CopilotChatWebApi)",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/samples/apps/copilot-chat-app/webapi/CopilotChatWebApi.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "aiServiceSecret",
|
||||
"type": "promptString",
|
||||
"default": "",
|
||||
"description": "Enter a secret for Copilot Chat AIService:Key",
|
||||
"password": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.6.33706.43
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CopilotChatWebApi", "webapi\CopilotChatWebApi.csproj", "{5252E68F-B653-44CE-9A32-360A75C54E0E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{5252E68F-B653-44CE-9A32-360A75C54E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5252E68F-B653-44CE-9A32-360A75C54E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5252E68F-B653-44CE-9A32-360A75C54E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5252E68F-B653-44CE-9A32-360A75C54E0E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {971570D3-60EA-4EE4-980C-0BDA3E66E741}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,172 +1,3 @@
|
||||
# Copilot Chat Sample Application
|
||||
# Semantic Kernel copilot-chat-app
|
||||
|
||||
> This sample is for educational purposes only and is not recommended for production deployments.
|
||||
|
||||
# About Copilot Chat
|
||||
|
||||
This sample allows you to build your own integrated large language model chat copilot.
|
||||
This is an enriched intelligence app, with multiple dynamic components including
|
||||
command messages, user intent, and memories.
|
||||
|
||||
The chat prompt and response will evolve as the conversation between the user and the application proceeds.
|
||||
This chat experience is orchestrated with Semantic Kernel and a Copilot Chat skill containing numerous
|
||||
functions that work together to construct each response.
|
||||
|
||||

|
||||
|
||||
# Automated Setup and Local Deployment
|
||||
|
||||
Refer to [./scripts/README.md](./scripts/README.md) for local configuration and deployment.
|
||||
|
||||
Refer to [./deploy/README.md](./deploy/README.md) for Azure configuration and deployment.
|
||||
|
||||
# Manual Setup and Local Deployment
|
||||
|
||||
## Configure your environment
|
||||
|
||||
Before you get started, make sure you have the following requirements in place:
|
||||
|
||||
- [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [Yarn](https://classic.yarnpkg.com/lang/en/docs/install) - After installation, run `yarn --version` in a terminal window to ensure you are running v1.22.19.
|
||||
- [Azure OpenAI](https://aka.ms/oai/access) resource or an account with [OpenAI](https://platform.openai.com).
|
||||
- [Visual Studio Code](https://code.visualstudio.com/Download) **(Optional)**
|
||||
|
||||
## Start the WebApi Backend Server
|
||||
|
||||
The sample uses two applications, a front-end web UI, and a back-end API server.
|
||||
First, let’s set up and verify the back-end API server is running.
|
||||
|
||||
1. Generate and trust a localhost developer certificate. Open a terminal and run:
|
||||
- For Windows and Mac run `dotnet dev-certs https --trust` and select `Yes` when asked if you want to install this certificate.
|
||||
- For Linux run `dotnet dev-certs https`
|
||||
> **Note:** It is recommended you close all instances of your web browser after installing the developer certificates.
|
||||
|
||||
2. Navigate to `samples/apps/copilot-chat-app/webapi` and open `appsettings.json`
|
||||
- Update the `AIService` configuration section:
|
||||
- Update `Type` to the AI service you will be using (i.e., `AzureOpenAI` or `OpenAI`).
|
||||
- If your are using Azure OpenAI, update `Endpoint` to your Azure OpenAI resource Endpoint address (e.g.,
|
||||
`http://contoso.openai.azure.com`).
|
||||
> If you are using OpenAI, this property will be ignored.
|
||||
- Set your Azure OpenAI or OpenAI key by opening a terminal in the webapi project directory and using `dotnet user-secrets`
|
||||
```bash
|
||||
cd semantic-kernel/samples/apps/copilot-chat-app/webapi
|
||||
dotnet user-secrets set "AIService:Key" "MY_AZUREOPENAI_OR_OPENAI_KEY"
|
||||
```
|
||||
- **(Optional)** Update `Models` to the Azure OpenAI deployment or OpenAI models you want to use.
|
||||
- For `Completion` and `Planner`, CopilotChat is optimized for Chat completion models, such as gpt-3.5-turbo and gpt-4.
|
||||
> **Important:** gpt-3.5-turbo is normally labelled as "`gpt-35-turbo`" (no period) in Azure OpenAI and "`gpt-3.5-turbo`" (with a period) in OpenAI.
|
||||
- For `Embedding`, `text-embedding-ada-002` is sufficient and cost-effective for generating embeddings.
|
||||
> **Important:** If you are using Azure OpenAI, please use [deployment names](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource). If you are using OpenAI, please use [model names](https://platform.openai.com/docs/models).
|
||||
|
||||
- **(Optional)** To enable speech-to-text for chat input, update the `AzureSpeech` configuration section:
|
||||
> If you have not already, you will need to [create an Azure Speech resource](https://ms.portal.azure.com/#create/Microsoft.CognitiveServicesSpeechServices)
|
||||
(see [./webapi/appsettings.json](webapi/appsettings.json) for more details).
|
||||
- Update `Region` to whichever region is appropriate for your speech sdk instance.
|
||||
- Set your Azure speech key by opening a terminal in the webapi project directory and setting
|
||||
a dotnet user-secrets value for `AzureSpeech:Key`
|
||||
```bash
|
||||
dotnet user-secrets set "AzureSpeech:Key" "MY_AZURE_SPEECH_KEY"
|
||||
```
|
||||
|
||||
3. Build and run the back-end API server
|
||||
1. Open a terminal and navigate to `samples/apps/copilot-chat-app/webapi`
|
||||
|
||||
2. Run `dotnet build` to build the project.
|
||||
|
||||
3. Run `dotnet run` to start the server.
|
||||
|
||||
4. Verify the back-end server is responding, open a web browser and navigate to `https://localhost:40443/healthz`
|
||||
> The first time accessing the probe you may get a warning saying that there is a problem with website's certificate.
|
||||
Select the option to accept/continue - this is expected when running a service on `localhost`
|
||||
It is important to do this, as your browser may need to accept the certificate before allowing the frontend to communicate with the backend.
|
||||
|
||||
> You may also need to acknowledge the Windows Defender Firewall, and allow the app to communicate over private or public networks as appropriate.
|
||||
|
||||
## Start the WebApp FrontEnd application
|
||||
|
||||
1. Build and start the front-end application
|
||||
1. You will need an Azure Active Directory (AAD) application registration.
|
||||
> For more details on creating an application registration, go [here](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
|
||||
- Select `Single-page application (SPA)` as platform type, and set the Web redirect URI to `http://localhost:3000`
|
||||
- Select `Accounts in any organizational directory and personal Microsoft Accounts` as supported account types for this sample.
|
||||
- Make a note of the `Application (client) ID` from the Azure Portal, we will use of it later.
|
||||
|
||||
2. Open a terminal and navigate to `samples/apps/copilot-chat-app/webapp` Copy `.env.example` into a new
|
||||
file `.env` and update the `REACT_APP_AAD_CLIENT_ID` with the AAD application (Client) ID created above.
|
||||
For example:
|
||||
```bash
|
||||
REACT_APP_BACKEND_URI=https://localhost:40443/
|
||||
REACT_APP_AAD_CLIENT_ID={Your Application (client) ID}
|
||||
REACT_APP_AAD_AUTHORITY=https://login.microsoftonline.com/common
|
||||
```
|
||||
> For more detail on AAD authorities, see [Client Application Configuration Authorities](https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-client-application-configuration#authority).
|
||||
|
||||
> `REACT_APP_SK_API_KEY` is only required if you're using an Semantic Kernel service deployed to Azure. See the [Authorization section of Deploying Semantic Kernel to Azure in a web app service](./deploy/README.md#authorization) for more details and instruction on how to find your API key.
|
||||
```bash
|
||||
REACT_APP_SK_API_KEY={Your API Key, should be the same as Authorization:ApiKey from appsettings.json}
|
||||
```
|
||||
|
||||
3. To build and run the front-end application, open a terminal and navigate to `samples/apps/copilot-chat-app/webapp` if not already, then run:
|
||||
```bash
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
> To run the WebApp with HTTPs, see [How to use HTTPS for local development](./webapp/README.md#how-to-use-https-for-local-development).
|
||||
|
||||
4. With the back end and front end running, your web browser should automatically launch and navigate to `http://localhost:3000`
|
||||
> The first time running the front-end application may take a minute or so to start.
|
||||
|
||||
5. Sign in with a Microsoft personal account or a "Work or School" account.
|
||||
|
||||
6. Consent permission for the application to read your profile information (i.e., your name).
|
||||
|
||||
If you you experience any errors or issues, consult the troubleshooting section below.
|
||||
|
||||
2. Have fun!
|
||||
> **Note:** Each chat interaction will call Azure OpenAI/OpenAI which will use tokens that you may be billed for.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## 1. Unable to load chats. Details: interaction_in_progress: Interaction is currently in progress.
|
||||
|
||||
The WebApp can display this error when the application is configured for an active directory tenant,
|
||||
(e.g., personal/MSA accounts) and the browser attempts to use single sign-on with an account from
|
||||
another tenant (e.g., work or school account). Either user a private/incognito browser tab or clear
|
||||
your browser credentials/cookies.
|
||||
|
||||
## 2. Issues using text completion models, such as `text-davinci-003`
|
||||
|
||||
CopilotChat supports chat completion models, such as `gpt-3.5-*` and `gpt-4-*`.
|
||||
See [OpenAI's model compatibility](https://platform.openai.com/docs/models/model-endpoint-compatibility) for
|
||||
the complete list of current models supporting chat completions.
|
||||
|
||||
## 3. Localhost SSL certificate errors / CORS errors
|
||||
|
||||

|
||||
|
||||
If you are stopped at an error message similar to the one above, your browser may be blocking the front-end access
|
||||
to the back end while waiting for your permission to connect. To resolve this, try the following:
|
||||
|
||||
1. Confirm the backend service is running by opening a web browser, and navigating to `https://localhost:40443/healthz`
|
||||
- You should see a confirmation message: `Healthy`
|
||||
2. If your browser asks you to acknowledge the risks of visiting an insecure website, you must acknowledge the
|
||||
message before the front end will be allowed to connect to the back-end server.
|
||||
- Acknowledge, continue, and navigate until you see the message `Healthy`.
|
||||
3. Navigate to `http://localhost:3000` or refresh the page to use the Copilot Chat application.
|
||||
|
||||
## 4. Have Yarn version 2.x or 3.x
|
||||
|
||||
The webapp uses packages that are only supported by classic Yarn (v1.x). If you have Yarn v2.x+, run
|
||||
the following commands in your preferred shell to flip Yarn to the classic version.
|
||||
|
||||
```shell
|
||||
npm install -g yarn
|
||||
yarn set version classic
|
||||
```
|
||||
|
||||
You can confirm the active Yarn version by running `yarn --version`.
|
||||
|
||||
# Additional resources
|
||||
|
||||
1. [Import Document Application](./importdocument/README.md): Import a document to the memory store.
|
||||
_This project has moved to the [microsoft/chat-copilot](https://github.com/microsoft/chat-copilot) repository._
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
# Deploying Copilot Chat
|
||||
This document details how to deploy CopilotChat's required resources to your Azure subscription.
|
||||
|
||||
## Things to know
|
||||
- Access to Azure OpenAI is currently limited as we navigate high demand, upcoming product improvements, and Microsoft’s commitment to responsible AI.
|
||||
For more details and information on applying for access, go [here](https://learn.microsoft.com/azure/cognitive-services/openai/overview?ocid=AID3051475#how-do-i-get-access-to-azure-openai).
|
||||
For regional availability of Azure OpenAI, see the [availability map](https://azure.microsoft.com/explore/global-infrastructure/products-by-region/?products=cognitive-services).
|
||||
|
||||
- With the limited availability of Azure OpenAI, consider sharing an Azure OpenAI instance across multiple resources.
|
||||
|
||||
- `F1` and `D1` SKUs for the App Service Plans are not currently supported for this deployment in order to support private networking.
|
||||
|
||||
|
||||
# Configure your environment
|
||||
Before you get started, make sure you have the following requirements in place:
|
||||
- Azure CLI (i.e., az) (if you already installed Azure CLI, make sure to update your installation to the latest version)
|
||||
- Windows, go to https://aka.ms/installazurecliwindows
|
||||
- Linux, run "`curl -L https://aka.ms/InstallAzureCli | bash`"
|
||||
- Azure Static Web App CLI (i.e., swa) can be installed by running "`npm install -g @azure/static-web-apps-cli`"
|
||||
- (Linux only) `zip` can be installed by running "`sudo apt install zip`"
|
||||
|
||||
|
||||
# Deploy Azure Infrastructure
|
||||
The examples below assume you are using an existing Azure OpenAI resource. See the notes following each command for using OpenAI or creating a new Azure OpenAI resource.
|
||||
|
||||
## PowerShell
|
||||
```powershell
|
||||
./deploy-azure.ps1 -Subscription {YOUR_SUBSCRIPTION_ID} -DeploymentName {YOUR_DEPLOYMENT_NAME} -AIService {AzureOpenAI or OpenAI} -AIApiKey {YOUR_AI_KEY} -AIEndpoint {YOUR_AZURE_OPENAI_ENDPOINT}
|
||||
```
|
||||
- To use an existing Azure OpenAI resource, set `-AIService` to `AzureOpenAI` and include `-AIApiKey` and `-AIEndpoint`.
|
||||
- To deploy a new Azure OpenAI resource, set `-AIService` to `AzureOpenAI` and omit `-AIApiKey` and `-AIEndpoint`.
|
||||
- To use an an OpenAI account, set `-AIService` to `OpenAI` and include `-AIApiKey`.
|
||||
|
||||
## Bash
|
||||
```bash
|
||||
chmod +x ./deploy-azure.sh
|
||||
./deploy-azure.sh --subscription {YOUR_SUBSCRIPTION_ID} --deployment-name {YOUR_DEPLOYMENT_NAME} --ai-service {AzureOpenAI or OpenAI} --ai-service-key {YOUR_AI_KEY} --ai-endpoint {YOUR_AZURE_OPENAI_ENDPOINT}
|
||||
```
|
||||
- To use an existing Azure OpenAI resource, set `--ai-service` to `AzureOpenAI` and include `--ai-service-key` and `--ai-endpoint`.
|
||||
- To deploy a new Azure OpenAI resource, set `--ai-service` to `AzureOpenAI` and omit `--ai-service-key` and `--ai-endpoint`.
|
||||
- To use an an OpenAI account, set `--ai-service` to `OpenAI` and include `--ai-service-key`.
|
||||
|
||||
## Azure Portal
|
||||
You can also deploy the infrastructure directly from the Azure Portal by clicking the button below:
|
||||
|
||||
[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Fsemantic-kernel%2Fmain%2Fsamples%2Fapps%2Fcopilot-chat-app%2Fdeploy%2Fmain.json)
|
||||
|
||||
> This will automatically deploy the most recent release of CopilotChat backend binaries ([link](https://github.com/microsoft/semantic-kernel/releases?q=copilotchat)).
|
||||
|
||||
> To find the deployment name when using `Deploy to Azure`, look for a deployment in your resource group that starts with `Microsoft.Template`.
|
||||
|
||||
|
||||
# Deploy Backend (WebAPI)
|
||||
To deploy the backend, build the deployment package first and deploy it to the Azure resources created above.
|
||||
|
||||
## PowerShell
|
||||
```powershell
|
||||
./package-webapi.ps1
|
||||
|
||||
./deploy-webapi.ps1 -Subscription {YOUR_SUBSCRIPTION_ID} -ResourceGroupName rg-{YOUR_DEPLOYMENT_NAME} -DeploymentName {YOUR_DEPLOYMENT_NAME}
|
||||
```
|
||||
|
||||
## Bash
|
||||
```bash
|
||||
chmod +x ./package-webapi.sh
|
||||
./package-webapi.sh
|
||||
|
||||
chmod +x ./deploy-webapi.sh
|
||||
./deploy-webapi.sh --subscription {YOUR_SUBSCRIPTION_ID} --resource-group rg-{YOUR_DEPLOYMENT_NAME} --deployment-name {YOUR_DEPLOYMENT_NAME}
|
||||
```
|
||||
|
||||
|
||||
# Deploy Frontend (WebApp)
|
||||
|
||||
## Prerequisites
|
||||
### App registration (identity)
|
||||
You will need an Azure Active Directory (AAD) application registration.
|
||||
> For details on creating an application registration, go [here](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
|
||||
- Select `Single-page application (SPA)` as platform type, and set the redirect URI to `http://localhost:3000`
|
||||
- Select `Accounts in any organizational directory and personal Microsoft Accounts` as supported account types for this sample.
|
||||
- Make a note of the `Application (client) ID` from the Azure Portal for use in the `Deploy` below.
|
||||
|
||||
### Install Azure's Static Web Apps CLI
|
||||
```bash
|
||||
npm install -g @azure/static-web-apps-cli
|
||||
```
|
||||
|
||||
## PowerShell
|
||||
|
||||
```powershell
|
||||
|
||||
./deploy-webapp.ps1 -Subscription {YOUR_SUBSCRIPTION_ID} -ResourceGroupName rg-{YOUR_DEPLOYMENT_NAME} -DeploymentName {YOUR_DEPLOYMENT_NAME} -ApplicationClientId {YOUR_APPLICATION_ID}
|
||||
```
|
||||
|
||||
## Bash
|
||||
|
||||
```bash
|
||||
./deploy-webapp.sh --subscription {YOUR_SUBSCRIPTION_ID} --resource-group rg-{YOUR_DEPLOYMENT_NAME} --deployment-name {YOUR_DEPLOYMENT_NAME} --application-id {YOUR_APPLICATION_ID}
|
||||
```
|
||||
|
||||
Your CopilotChat application is now deployed!
|
||||
|
||||
|
||||
# Appendix
|
||||
## Using custom web frontends to access your deployment
|
||||
Make sure to include your frontend's URL as an allowed origin in your deployment's CORS settings. Otherwise, web browsers will refuse to let JavaScript make calls to your deployment.
|
||||
|
||||
To do this, go on the Azure portal, select your Semantic Kernel App Service, then click on "CORS" under the "API" section of the resource menu on the left of the page.
|
||||
This will get you to the CORS page where you can add your allowed hosts.
|
||||
|
||||
## Authorization
|
||||
All of endpoints (except `/healthz`) require authorization to access.
|
||||
By default, an API key is required for access which can be found in the `Authorization:ApiKey` configuration setting.
|
||||
To authorize requests with the API key, add the API key value to a `x-sk-api-key` header in your requests.
|
||||
|
||||
To view your CopilotChat API key:
|
||||
### PowerShell
|
||||
```powershell
|
||||
$webApiName = $(az deployment group show --name {DEPLOYMENT_NAME} --resource-group rg-{DEPLOYMENT_NAME} --output json | ConvertFrom-Json).properties.outputs.webapiName.value
|
||||
|
||||
($(az webapp config appsettings list --name $webapiName --resource-group rg-{YOUR_DEPLOYMENT_NAME} | ConvertFrom-JSON) | Where-Object -Property name -EQ -Value Authorization:ApiKey).value
|
||||
```
|
||||
|
||||
### Bash
|
||||
```bash
|
||||
eval WEB_API_NAME=$(az deployment group show --name $DEPLOYMENT_NAME --resource-group $RESOURCE_GROUP --output json) | jq -r '.properties.outputs.webapiName.value'
|
||||
|
||||
$(az webapp config appsettings list --name $WEB_API_NAME --resource-group rg-{YOUR_DEPLOYMENT_NAME} | jq '.[] | select(.name=="Authorization:ApiKey").value')
|
||||
```
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Deploy CopilotChat Azure resources
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]
|
||||
# Name for the deployment
|
||||
$DeploymentName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]
|
||||
# Subscription to which to make the deployment
|
||||
$Subscription,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet("AzureOpenAI","OpenAI")]
|
||||
[string]
|
||||
# AI service to use
|
||||
$AIService,
|
||||
|
||||
[string]
|
||||
# API key for existing Azure OpenAI resource or OpenAI account
|
||||
$AIApiKey,
|
||||
|
||||
# Endpoint for existing Azure OpenAI resource
|
||||
[string]
|
||||
$AIEndpoint,
|
||||
|
||||
[string]
|
||||
# Resource group to which to make the deployment
|
||||
$ResourceGroup,
|
||||
|
||||
[string]
|
||||
# Region to which to make the deployment (ignored when deploying to an existing resource group)
|
||||
$Region = "centralus",
|
||||
|
||||
[string]
|
||||
# SKU for the Azure App Service plan
|
||||
$WebAppServiceSku = "B1",
|
||||
|
||||
[switch]
|
||||
# Don't deploy Qdrant for memory storage - Use volatile memory instead
|
||||
$NoQdrant,
|
||||
|
||||
[switch]
|
||||
# Don't deploy Cosmos DB for chat storage - Use volatile memory instead
|
||||
$NoCosmosDb,
|
||||
|
||||
[switch]
|
||||
# Don't deploy Speech Services to enable speech as chat input
|
||||
$NoSpeechServices,
|
||||
|
||||
[switch]
|
||||
# Switches on verbose template deployment output
|
||||
$DebugDeployment
|
||||
)
|
||||
|
||||
# if AIService is AzureOpenAI
|
||||
if ($AIService -eq "AzureOpenAI") {
|
||||
# Both $AIEndpoint and $AIApiKey must be set
|
||||
if ((!$AIEndpoint -and $AIApiKey) -or ($AIEndpoint -and !$AIApiKey)) {
|
||||
Write-Error "When AIService is AzureOpenAI, when either AIEndpoint and AIApiKey are set then both must be set."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# If both $AIEndpoint and $AIApiKey are not set, set $DeployAzureOpenAI to true and inform the user. Otherwise set $DeployAzureOpenAI to false and inform the user.
|
||||
if (!$AIEndpoint -and !$AIApiKey) {
|
||||
$DeployAzureOpenAI = $true
|
||||
Write-Host "When AIService is AzureOpenAI and both AIEndpoint and AIApiKey are not set then a new Azure OpenAI resource will be created."
|
||||
}
|
||||
else {
|
||||
$DeployAzureOpenAI = $false
|
||||
Write-Host "When AIService is AzureOpenAI and both AIEndpoint and AIApiKey are set, use the existing Azure OpenAI resource."
|
||||
}
|
||||
}
|
||||
|
||||
# if AIService is OpenAI then $AIApiKey is mandatory.
|
||||
if ($AIService -eq "OpenAI" -and !$AIApiKey) {
|
||||
Write-Error "When AIService is OpenAI, AIApiKey must be set."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$jsonConfig = "
|
||||
{
|
||||
`\`"name`\`": { `\`"value`\`": `\`"$DeploymentName`\`" },
|
||||
`\`"webAppServiceSku`\`": { `\`"value`\`": `\`"$WebAppServiceSku`\`" },
|
||||
`\`"aiService`\`": { `\`"value`\`": `\`"$AIService`\`" },
|
||||
`\`"aiApiKey`\`": { `\`"value`\`": `\`"$AIApiKey`\`" },
|
||||
`\`"aiEndpoint`\`": { `\`"value`\`": `\`"$AIEndpoint`\`" },
|
||||
`\`"deployNewAzureOpenAI`\`": { `\`"value`\`": $(If ($DeployAzureOpenAI) {"true"} Else {"false"}) },
|
||||
`\`"deployQdrant`\`": { `\`"value`\`": $(If (!($NoQdrant)) {"true"} Else {"false"}) },
|
||||
`\`"deployCosmosDB`\`": { `\`"value`\`": $(If (!($NoCosmosDb)) {"true"} Else {"false"}) },
|
||||
`\`"deploySpeechServices`\`": { `\`"value`\`": $(If (!($NoSpeechServices)) {"true"} Else {"false"}) }
|
||||
}
|
||||
"
|
||||
|
||||
$jsonConfig = $jsonConfig -replace '\s',''
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$templateFile = "$($PSScriptRoot)/main.bicep"
|
||||
|
||||
if (!$ResourceGroup)
|
||||
{
|
||||
$ResourceGroup = "rg-" + $DeploymentName
|
||||
}
|
||||
|
||||
az account show --output none
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Log into your Azure account"
|
||||
az login --output none
|
||||
}
|
||||
|
||||
az account set -s $Subscription
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Ensuring resource group '$ResourceGroup' exists..."
|
||||
az group create --location $Region --name $ResourceGroup --tags Creator=$env:UserName
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Validating template file..."
|
||||
az deployment group validate --name $DeploymentName --resource-group $ResourceGroup --template-file $templateFile --parameters $jsonConfig
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Deploying Azure resources ($DeploymentName)..."
|
||||
if ($DebugDeployment) {
|
||||
az deployment group create --name $DeploymentName --resource-group $ResourceGroup --template-file $templateFile --debug --parameters $jsonConfig
|
||||
}
|
||||
else {
|
||||
az deployment group create --name $DeploymentName --resource-group $ResourceGroup --template-file $templateFile --parameters $jsonConfig
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Deploy CopilotChat Azure resources.
|
||||
|
||||
set -e
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 -d DEPLOYMENT_NAME -s SUBSCRIPTION -ai AI_SERVICE_TYPE -aikey AI_SERVICE_KEY [OPTIONS]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " -d, --deployment-name DEPLOYMENT_NAME Name for the deployment (mandatory)"
|
||||
echo " -s, --subscription SUBSCRIPTION Subscription to which to make the deployment (mandatory)"
|
||||
echo " -ai, --ai-service AI_SERVICE_TYPE Type of AI service to use (i.e., OpenAI or AzureOpenAI)"
|
||||
echo " -aikey, --ai-service-key AI_SERVICE_KEY API key for existing Azure OpenAI resource or OpenAI account"
|
||||
echo " -aiend, --ai-endpoint AI_ENDPOINT Endpoint for existing Azure OpenAI resource"
|
||||
echo " -rg, --resource-group RESOURCE_GROUP Resource group to which to make the deployment (default: \"rg-\$DEPLOYMENT_NAME\")"
|
||||
echo " -r, --region REGION Region to which to make the deployment (default: \"South Central US\")"
|
||||
echo " -a, --app-service-sku WEB_APP_SVC_SKU SKU for the Azure App Service plan (default: \"B1\")"
|
||||
echo " -nq, --no-qdrant Don't deploy Qdrant for memory storage - Use volatile memory instead"
|
||||
echo " -nc, --no-cosmos-db Don't deploy Cosmos DB for chat storage - Use volatile memory instead"
|
||||
echo " -ns, --no-speech-services Don't deploy Speech Services to enable speech as chat input"
|
||||
echo " -dd, --debug-deployment Switches on verbose template deployment output"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
case $key in
|
||||
-d|--deployment-name)
|
||||
DEPLOYMENT_NAME="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-s|--subscription)
|
||||
SUBSCRIPTION="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-ai|--ai-service)
|
||||
AI_SERVICE_TYPE="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-aikey|--ai-service-key)
|
||||
AI_SERVICE_KEY="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-aiend|--ai-endpoint)
|
||||
AI_ENDPOINT="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-rg|--resource-group)
|
||||
RESOURCE_GROUP="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-r|--region)
|
||||
REGION="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-a|--app-service-sku)
|
||||
WEB_APP_SVC_SKU="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-nq|--no-qdrant)
|
||||
NO_QDRANT=true
|
||||
shift
|
||||
;;
|
||||
-nc|--no-cosmos-db)
|
||||
NO_COSMOS_DB=true
|
||||
shift
|
||||
;;
|
||||
-ns|--no-speech-services)
|
||||
NO_SPEECH_SERVICES=true
|
||||
shift
|
||||
;;
|
||||
-dd|--debug-deployment)
|
||||
DEBUG_DEPLOYMENT=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check mandatory arguments
|
||||
if [[ -z "$DEPLOYMENT_NAME" ]] || [[ -z "$SUBSCRIPTION" ]] || [[ -z "$AI_SERVICE_TYPE" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if AI_SERVICE_TYPE is either OpenAI or AzureOpenAI
|
||||
if [[ "${AI_SERVICE_TYPE,,}" != "openai" ]] && [[ "${AI_SERVICE_TYPE,,}" != "azureopenai" ]]; then
|
||||
echo "--ai-service must be either OpenAI or AzureOpenAI"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# if AI_SERVICE_TYPE is AzureOpenAI
|
||||
if [[ "${AI_SERVICE_TYPE,,}" = "azureopenai" ]]; then
|
||||
# Both AI_ENDPOINT and AI_SERVICE_KEY must be set or neither of them.
|
||||
if [[ (-z "$AI_ENDPOINT" && -n "$AI_SERVICE_KEY") || (-n "$AI_ENDPOINT" && -z "$AI_SERVICE_KEY") ]]; then
|
||||
echo "When --ai is 'AzureOpenAI', if either --ai-endpoint or --ai-service-key is set, then both must be set."
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# if AI_ENDPOINT and AI_SERVICE_KEY are not set, set NO_NEW_AZURE_OPENAI to false and tell the user, else set NO_NEW_AZURE_OPENAI to true
|
||||
if [[ -z "$AI_ENDPOINT" ]] && [[ -z "$AI_SERVICE_KEY" ]]; then
|
||||
NO_NEW_AZURE_OPENAI=false
|
||||
echo "When --ai is 'AzureOpenAI', if neither --ai-endpoint nor --ai-service-key are set, then a new Azure OpenAI resource will be created."
|
||||
else
|
||||
NO_NEW_AZURE_OPENAI=true
|
||||
echo "When --ai is 'AzureOpenAI', if both --ai-endpoint and --ai-service-key are set, then an existing Azure OpenAI resource will be used."
|
||||
fi
|
||||
fi
|
||||
|
||||
# if AI_SERVICE_TYPE is OpenAI then AI_SERVICE_KEY is mandatory
|
||||
if [[ "${AI_SERVICE_TYPE,,}" = "openai" ]] && [[ -z "$AI_SERVICE_KEY" ]]; then
|
||||
echo "When --ai is 'OpenAI', --ai-service-key must be set."
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If resource group is not set, then set it to rg-DEPLOYMENT_NAME
|
||||
if [ -z "$RESOURCE_GROUP" ]; then
|
||||
RESOURCE_GROUP="rg-${DEPLOYMENT_NAME}"
|
||||
fi
|
||||
|
||||
TEMPLATE_FILE="$(dirname "$0")/main.bicep"
|
||||
|
||||
az account show --output none
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Log into your Azure account"
|
||||
az login --use-device-code
|
||||
fi
|
||||
|
||||
az account set -s "$SUBSCRIPTION"
|
||||
|
||||
# Set defaults
|
||||
: "${REGION:="centralus"}"
|
||||
: "${WEB_APP_SVC_SKU:="B1"}"
|
||||
: "${NO_QDRANT:=false}"
|
||||
: "${NO_COSMOS_DB:=false}"
|
||||
: "${NO_SPEECH_SERVICES:=false}"
|
||||
|
||||
# Create JSON config
|
||||
JSON_CONFIG=$(cat << EOF
|
||||
{
|
||||
"name": { "value": "$DEPLOYMENT_NAME" },
|
||||
"webAppServiceSku": { "value": "$WEB_APP_SVC_SKU" },
|
||||
"aiService": { "value": "$AI_SERVICE_TYPE" },
|
||||
"aiApiKey": { "value": "$AI_SERVICE_KEY" },
|
||||
"aiEndpoint": { "value": "$([ -z "$AI_ENDPOINT" ] && echo "$AI_ENDPOINT")" },
|
||||
"deployNewAzureOpenAI": { "value": $([ "$NO_NEW_AZURE_OPENAI" = true ] && echo "false" || echo "true") },
|
||||
"deployQdrant": { "value": $([ "$NO_QDRANT" = true ] && echo "false" || echo "true") },
|
||||
"deployCosmosDB": { "value": $([ "$NO_COSMOS_DB" = true ] && echo "false" || echo "true") },
|
||||
"deploySpeechServices": { "value": $([ "$NO_SPEECH_SERVICES" = true ] && echo "false" || echo "true") }
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "Ensuring resource group $RESOURCE_GROUP..."
|
||||
az group create --location "$REGION" --name "$RESOURCE_GROUP" --tags Creator="$USER"
|
||||
|
||||
echo "Validating template file..."
|
||||
az deployment group validate --name "$DEPLOYMENT_NAME" --resource-group "$RESOURCE_GROUP" --template-file "$TEMPLATE_FILE" --parameters "$JSON_CONFIG"
|
||||
|
||||
echo "Deploying Azure resources ($DEPLOYMENT_NAME)..."
|
||||
if [ "$DEBUG_DEPLOYMENT" = true ]; then
|
||||
az deployment group create --name "$DEPLOYMENT_NAME" --resource-group "$RESOURCE_GROUP" --template-file "$TEMPLATE_FILE" --debug --parameters "$JSON_CONFIG"
|
||||
else
|
||||
az deployment group create --name "$DEPLOYMENT_NAME" --resource-group "$RESOURCE_GROUP" --template-file "$TEMPLATE_FILE" --parameters "$JSON_CONFIG"
|
||||
fi
|
||||
@@ -1,63 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Deploy CopilotChat's WebAPI to Azure
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]
|
||||
# Subscription to which to make the deployment
|
||||
$Subscription,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]
|
||||
# Resource group to which to make the deployment
|
||||
$ResourceGroupName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]
|
||||
# Name of the previously deployed Azure deployment
|
||||
$DeploymentName,
|
||||
|
||||
[string]
|
||||
# CopilotChat WebApi package to deploy
|
||||
$PackageFilePath = "$PSScriptRoot/out/webapi.zip"
|
||||
)
|
||||
|
||||
# Ensure $PackageFilePath exists
|
||||
if (!(Test-Path $PackageFilePath)) {
|
||||
Write-Error "Package file '$PackageFilePath' does not exist. Have you run 'package-webapi.ps1' yet?"
|
||||
exit 1
|
||||
}
|
||||
|
||||
az account show --output none
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Log into your Azure account"
|
||||
az login --output none
|
||||
}
|
||||
|
||||
az account set -s $Subscription
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Getting Azure WebApp resource name..."
|
||||
$webappName=$(az deployment group show --name $DeploymentName --resource-group $ResourceGroupName --output json | ConvertFrom-Json).properties.outputs.webapiName.value
|
||||
if ($null -eq $webAppName) {
|
||||
Write-Error "Could not get Azure WebApp resource name from deployment output."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Azure WebApp name: $webappName"
|
||||
|
||||
Write-Host "Configuring Azure WebApp to run from package..."
|
||||
az webapp config appsettings set --resource-group $ResourceGroupName --name $webappName --settings WEBSITE_RUN_FROM_PACKAGE="1" | out-null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Deploying '$PackageFilePath' to Azure WebApp '$webappName'..."
|
||||
az webapp deployment source config-zip --resource-group $ResourceGroupName --name $webappName --src $PackageFilePath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Deploy CopilotChat's WebAPI to Azure.
|
||||
|
||||
set -e
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 -d DEPLOYMENT_NAME -s SUBSCRIPTION -rg RESOURCE_GROUP [OPTIONS]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " -d, --deployment-name DEPLOYMENT_NAME Name of the deployment from a 'deploy-azure.sh' deployment (mandatory)"
|
||||
echo " -s, --subscription SUBSCRIPTION Subscription to which to make the deployment (mandatory)"
|
||||
echo " -rg, --resource-group RESOURCE_GROUP Resource group name from a 'deploy-azure.sh' deployment (mandatory)"
|
||||
echo " -p, --package PACKAGE_FILE_PATH Path to the WebAPI package file from a 'package-webapi.sh' run"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
case $key in
|
||||
-d|--deployment-name)
|
||||
DEPLOYMENT_NAME="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-s|--subscription)
|
||||
SUBSCRIPTION="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-rg|--resource-group)
|
||||
RESOURCE_GROUP="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-p|--package)
|
||||
PACKAGE_FILE_PATH="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check mandatory arguments
|
||||
if [[ -z "$DEPLOYMENT_NAME" ]] || [[ -z "$SUBSCRIPTION" ]] || [[ -z "$RESOURCE_GROUP" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set defaults
|
||||
: "${PACKAGE_FILE_PATH:="$(dirname "$0")/out/webapi.zip"}"
|
||||
|
||||
# Ensure $PACKAGE_FILE_PATH exists
|
||||
if [[ ! -f "$PACKAGE_FILE_PATH" ]]; then
|
||||
echo "Package file '$PACKAGE_FILE_PATH' does not exist. Have you run 'package-webapi.sh' yet?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
az account show --output none
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Log into your Azure account"
|
||||
az login --use-device-code
|
||||
fi
|
||||
|
||||
az account set -s "$SUBSCRIPTION"
|
||||
|
||||
echo "Getting Azure WebApp resource name..."
|
||||
eval WEB_APP_NAME=$(az deployment group show --name $DEPLOYMENT_NAME --resource-group $RESOURCE_GROUP --output json | jq '.properties.outputs.webapiName.value')
|
||||
# Ensure $WEB_APP_NAME is set
|
||||
if [[ -z "$WEB_APP_NAME" ]]; then
|
||||
echo "Could not get Azure WebApp resource name from deployment output."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Azure WebApp name: $webappName"
|
||||
|
||||
echo "Configuring Azure WebApp to run from package..."
|
||||
az webapp config appsettings set --resource-group $RESOURCE_GROUP --name $WEB_APP_NAME --settings WEBSITE_RUN_FROM_PACKAGE="1"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Could not configure Azure WebApp to run from package."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying '$PackageFilePath' to Azure WebApp '$webappName'..."
|
||||
az webapp deployment source config-zip --resource-group $RESOURCE_GROUP --name $WEB_APP_NAME --src $PACKAGE_FILE_PATH
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Could not deploy '$PackageFilePath' to Azure WebApp '$webappName'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
eval WEB_APP_URL=$(az deployment group show --name $DEPLOYMENT_NAME --resource-group $RESOURCE_GROUP --output json | jq '.properties.outputs.webapiUrl.value')
|
||||
echo "To verify your deployment, go to 'https://$WEB_APP_URL/healthz' in your browser."
|
||||
@@ -1,122 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Deploy CopilotChat's WebApp to Azure
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]
|
||||
# Subscription to which to make the deployment
|
||||
$Subscription,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]
|
||||
# Resource group to which to make the deployment
|
||||
$ResourceGroupName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]
|
||||
# Name of the previously deployed Azure deployment
|
||||
$DeploymentName,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string]
|
||||
# Client application id
|
||||
$ApplicationClientId
|
||||
)
|
||||
|
||||
Write-Host "Setting up Azure credentials..."
|
||||
az account show --output none
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Log into your Azure account"
|
||||
az login --output none
|
||||
}
|
||||
|
||||
Write-Host "Setting subscription to '$Subscription'..."
|
||||
az account set -s $Subscription
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Getting deployment outputs..."
|
||||
$deployment = $(az deployment group show --name $DeploymentName --resource-group $ResourceGroupName --output json | ConvertFrom-Json)
|
||||
$webappUrl = $deployment.properties.outputs.webappUrl.value
|
||||
$webappName = $deployment.properties.outputs.webappName.value
|
||||
$webapiUrl = $deployment.properties.outputs.webapiUrl.value
|
||||
$webapiName = $deployment.properties.outputs.webapiName.value
|
||||
$webapiApiKey = ($(az webapp config appsettings list --name $webapiName --resource-group $ResourceGroupName | ConvertFrom-JSON) | Where-Object -Property name -EQ -Value Authorization:ApiKey).value
|
||||
Write-Host "webappUrl: $webappUrl"
|
||||
Write-Host "webappName: $webappName"
|
||||
Write-Host "webapiName: $webapiName"
|
||||
Write-Host "webapiUrl: $webapiUrl"
|
||||
|
||||
# Set UTF8 as default encoding for Out-File
|
||||
$PSDefaultParameterValues['Out-File:Encoding'] = 'ascii'
|
||||
|
||||
$envFilePath = "$PSScriptRoot/../webapp/.env"
|
||||
Write-Host "Writing environment variables to '$envFilePath'..."
|
||||
"REACT_APP_BACKEND_URI=https://$webapiUrl/" | Out-File -FilePath $envFilePath
|
||||
"REACT_APP_AAD_AUTHORITY=https://login.microsoftonline.com/common" | Out-File -FilePath $envFilePath -Append
|
||||
"REACT_APP_AAD_CLIENT_ID=$ApplicationClientId" | Out-File -FilePath $envFilePath -Append
|
||||
"REACT_APP_SK_API_KEY=$webapiApiKey" | Out-File -FilePath $envFilePath -Append
|
||||
|
||||
Write-Host "Generating SWA config..."
|
||||
$swaConfig = $(Get-Content "$PSScriptRoot/../webapp/template.swa-cli.config.json" -Raw)
|
||||
$swaConfig = $swaConfig.Replace("{{appDevserverUrl}}", "https://$webappUrl")
|
||||
$swaConfig = $swaConfig.Replace("{{appName}}", "$webappName")
|
||||
$swaConfig = $swaConfig.Replace("{{resourceGroup}}", "$ResourceGroupName")
|
||||
$swaConfig = $swaConfig.Replace("{{subscription-id}}", "$Subscription")
|
||||
|
||||
$swaConfig | Out-File -FilePath "$PSScriptRoot/../webapp/swa-cli.config.json"
|
||||
Write-Host $(Get-Content "$PSScriptRoot/../webapp/swa-cli.config.json" -Raw)
|
||||
|
||||
Push-Location -Path "$PSScriptRoot/../webapp"
|
||||
Write-Host "Installing yarn dependencies..."
|
||||
yarn install
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Building webapp..."
|
||||
swa build
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Deploying webapp..."
|
||||
swa deploy
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
$origin = "https://$webappUrl"
|
||||
Write-Host "Ensuring CORS origin '$origin' to webapi '$webapiName'..."
|
||||
if (-not ((az webapp cors show --name $webapiName --resource-group $ResourceGroupName --subscription $Subscription | ConvertFrom-Json).allowedOrigins -contains $origin)) {
|
||||
az webapp cors add --name $webapiName --resource-group $ResourceGroupName --subscription $Subscription --allowed-origins $origin
|
||||
}
|
||||
|
||||
Write-Host "Ensuring '$origin' is included in AAD app registration's redirect URIs..."
|
||||
$objectId = (az ad app show --id $ApplicationClientId | ConvertFrom-Json).id
|
||||
$redirectUris = (az rest --method GET --uri "https://graph.microsoft.com/v1.0/applications/$objectId" --headers 'Content-Type=application/json' | ConvertFrom-Json).spa.redirectUris
|
||||
if ($redirectUris -notcontains "$origin") {
|
||||
$redirectUris += "$origin"
|
||||
|
||||
$body = "{spa:{redirectUris:["
|
||||
foreach ($uri in $redirectUris) {
|
||||
$body += "'$uri',"
|
||||
}
|
||||
$body += "]}}"
|
||||
|
||||
az rest `
|
||||
--method PATCH `
|
||||
--uri "https://graph.microsoft.com/v1.0/applications/$objectId" `
|
||||
--headers 'Content-Type=application/json' `
|
||||
--body $body
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Pop-Location
|
||||
|
||||
Write-Host "To verify your deployment, go to 'https://$webappUrl' in your browser."
|
||||
@@ -1,144 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Deploy CopilotChat's WebApp to Azure
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 -d DEPLOYMENT_NAME -s SUBSCRIPTION --ai AI_SERVICE_TYPE -aikey AI_SERVICE_KEY [OPTIONS]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " -s, --subscription SUBSCRIPTION Subscription to which to make the deployment (mandatory)"
|
||||
echo " -rg, --resource-group RESOURCE_GROUP Resource group name from a 'deploy-azure.sh' deployment (mandatory)"
|
||||
echo " -d, --deployment-name DEPLOYMENT_NAME Name of the deployment from a 'deploy-azure.sh' deployment (mandatory)"
|
||||
echo " -a, --application-id Client application ID (mandatory)"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
case $key in
|
||||
-d|--deployment-name)
|
||||
DEPLOYMENT_NAME="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-s|--subscription)
|
||||
SUBSCRIPTION="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-rg|--resource-group)
|
||||
RESOURCE_GROUP="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-a|--application-id)
|
||||
APPLICATION_ID="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check mandatory arguments
|
||||
if [[ -z "$DEPLOYMENT_NAME" ]] || [[ -z "$SUBSCRIPTION" ]] || [[ -z "$RESOURCE_GROUP" ]] || [[ -z "$APPLICATION_ID" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
az account show --output none
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Log into your Azure account"
|
||||
az login --use-device-code
|
||||
fi
|
||||
|
||||
az account set -s "$SUBSCRIPTION"
|
||||
|
||||
echo "Getting deployment outputs..."
|
||||
DEPLOYMENT_JSON=$(az deployment group show --name $DEPLOYMENT_NAME --resource-group $RESOURCE_GROUP --output json)
|
||||
# get the webapiUrl from the deployment outputs
|
||||
eval WEB_APP_URL=$(echo $DEPLOYMENT_JSON | jq -r '.properties.outputs.webappUrl.value')
|
||||
echo "WEB_APP_URL: $WEB_APP_URL"
|
||||
eval WEB_APP_NAME=$(echo $DEPLOYMENT_JSON | jq -r '.properties.outputs.webappName.value')
|
||||
echo "WEB_APP_NAME: $WEB_APP_NAME"
|
||||
eval WEB_API_URL=$(echo $DEPLOYMENT_JSON | jq -r '.properties.outputs.webapiUrl.value')
|
||||
echo "WEB_API_URL: $WEB_API_URL"
|
||||
eval WEB_API_NAME=$(echo $DEPLOYMENT_JSON | jq -r '.properties.outputs.webapiName.value')
|
||||
echo "WEB_API_NAME: $WEB_API_NAME"
|
||||
echo "Getting webapi key..."
|
||||
eval WEB_API_KEY=$(az webapp config appsettings list --name $WEB_API_NAME --resource-group $RESOURCE_GROUP | jq '.[] | select(.name=="Authorization:ApiKey").value')
|
||||
|
||||
ENV_FILE_PATH="$SCRIPT_ROOT/../webapp/.env"
|
||||
echo "Writing environment variables to '$ENV_FILE_PATH'..."
|
||||
echo "REACT_APP_BACKEND_URI=https://$WEB_API_URL/" > $ENV_FILE_PATH
|
||||
echo "REACT_APP_AAD_AUTHORITY=https://login.microsoftonline.com/common" >> $ENV_FILE_PATH
|
||||
echo "REACT_APP_AAD_CLIENT_ID=$APPLICATION_ID" >> $ENV_FILE_PATH
|
||||
echo "REACT_APP_SK_API_KEY=$WEB_API_KEY" >> $ENV_FILE_PATH
|
||||
|
||||
echo "Writing swa-cli.config.json..."
|
||||
SWA_CONFIG_FILE_PATH="$SCRIPT_ROOT/../webapp/swa-cli.config.json"
|
||||
sed "s/{{appDevserverUrl}}/https:\/\/${WEB_APP_URL}/g" $SCRIPT_ROOT/../webapp/template.swa-cli.config.json > $SWA_CONFIG_FILE_PATH
|
||||
cat $SWA_CONFIG_FILE_PATH
|
||||
|
||||
pushd "$SCRIPT_ROOT/../webapp"
|
||||
|
||||
echo "Installing yarn dependencies..."
|
||||
yarn install
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install yarn dependencies"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building webapp..."
|
||||
swa build
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to build webapp"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deploying webapp..."
|
||||
swa deploy --subscription-id $SUBSCRIPTION --app-name $WEB_APP_NAME --env production
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to deploy webapp"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ORIGIN="https://$WEB_APP_URL"
|
||||
echo "Ensuring CORS origin '$ORIGIN' to webapi '$WEB_API_NAME'..."
|
||||
CORS_RESULT=$(az webapp cors show --name $WEB_API_NAME --resource-group $RESOURCE_GROUP --subscription $SUBSCRIPTION | jq '.allowedOrigins | index("$ORIGIN")')
|
||||
if [[ "$CORS_RESULT" == "null" ]]; then
|
||||
echo "Adding CORS origin '$ORIGIN' to webapi '$WEB_API_NAME'..."
|
||||
az webapp cors add --name $WEB_API_NAME --resource-group $RESOURCE_GROUP --subscription $SUBSCRIPTION --allowed-origins $ORIGIN
|
||||
fi
|
||||
|
||||
echo "Ensuring '$ORIGIN' is included in AAD app registration's redirect URIs..."
|
||||
eval OBJECT_ID=$(az ad app show --id $APPLICATION_ID | jq -r '.id')
|
||||
|
||||
REDIRECT_URIS=$(az rest --method GET --uri "https://graph.microsoft.com/v1.0/applications/$OBJECT_ID" --headers 'Content-Type=application/json' | jq -r '.spa.redirectUris')
|
||||
if [[ ! "$REDIRECT_URIS" =~ "$ORIGIN" ]]; then
|
||||
BODY="{spa:{redirectUris:['"
|
||||
eval BODY+=$(echo $REDIRECT_URIS | jq $'join("\',\'")')
|
||||
BODY+="','$ORIGIN']}}"
|
||||
|
||||
az rest \
|
||||
--method PATCH \
|
||||
--uri "https://graph.microsoft.com/v1.0/applications/$OBJECT_ID" \
|
||||
--headers 'Content-Type=application/json' \
|
||||
--body $BODY
|
||||
fi
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to update app registration"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
popd
|
||||
|
||||
echo "To verify your deployment, go to 'https://$WEB_APP_URL' in your browser."
|
||||
@@ -1,711 +0,0 @@
|
||||
/*
|
||||
Copyright (c) Microsoft. All rights reserved.
|
||||
Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
Bicep template for deploying CopilotChat Azure resources.
|
||||
*/
|
||||
|
||||
@description('Name for the deployment consisting of alphanumeric characters or dashes (\'-\')')
|
||||
param name string = 'copichat'
|
||||
|
||||
@description('SKU for the Azure App Service plan')
|
||||
@allowed(['B1', 'S1', 'S2', 'S3', 'P1V3', 'P2V3', 'I1V2', 'I2V2' ])
|
||||
param webAppServiceSku string = 'B1'
|
||||
|
||||
@description('Location of package to deploy as the web service')
|
||||
#disable-next-line no-hardcoded-env-urls
|
||||
param packageUri string = 'https://aka.ms/copilotchat/webapi/latest'
|
||||
|
||||
@description('Underlying AI service')
|
||||
@allowed([
|
||||
'AzureOpenAI'
|
||||
'OpenAI'
|
||||
])
|
||||
param aiService string = 'AzureOpenAI'
|
||||
|
||||
@description('Model to use for chat completions')
|
||||
param completionModel string = 'gpt-35-turbo'
|
||||
|
||||
@description('Model to use for text embeddings')
|
||||
param embeddingModel string = 'text-embedding-ada-002'
|
||||
|
||||
@description('Completion model the task planner should use')
|
||||
param plannerModel string = 'gpt-35-turbo'
|
||||
|
||||
@description('Azure OpenAI endpoint to use (Azure OpenAI only)')
|
||||
param aiEndpoint string = ''
|
||||
|
||||
@secure()
|
||||
@description('Azure OpenAI or OpenAI API key')
|
||||
param aiApiKey string = ''
|
||||
|
||||
@secure()
|
||||
@description('WebAPI key to use for authorization')
|
||||
param webApiKey string = newGuid()
|
||||
|
||||
@description('Whether to deploy a new Azure OpenAI instance')
|
||||
param deployNewAzureOpenAI bool = false
|
||||
|
||||
@description('Whether to deploy Cosmos DB for persistent chat storage')
|
||||
param deployCosmosDB bool = true
|
||||
|
||||
@description('Whether to deploy Qdrant (in a container) for persistent memory storage')
|
||||
param deployQdrant bool = true
|
||||
|
||||
@description('Whether to deploy Azure Speech Services to enable input by voice')
|
||||
param deploySpeechServices bool = true
|
||||
|
||||
@description('Region for the resources')
|
||||
param location string = resourceGroup().location
|
||||
|
||||
@description('Region for the webapp frontend')
|
||||
param webappLocation string = 'westus2'
|
||||
|
||||
@description('Hash of the resource group ID')
|
||||
var rgIdHash = uniqueString(resourceGroup().id)
|
||||
|
||||
@description('Deployment name unique to resource group')
|
||||
var uniqueName = '${name}-${rgIdHash}'
|
||||
|
||||
@description('Name of the Azure Storage file share to create')
|
||||
var storageFileShareName = 'aciqdrantshare'
|
||||
|
||||
|
||||
resource openAI 'Microsoft.CognitiveServices/accounts@2022-12-01' = if(deployNewAzureOpenAI) {
|
||||
name: 'ai-${uniqueName}'
|
||||
location: location
|
||||
kind: 'OpenAI'
|
||||
sku: {
|
||||
name: 'S0'
|
||||
}
|
||||
properties: {
|
||||
customSubDomainName: toLower(uniqueName)
|
||||
}
|
||||
}
|
||||
|
||||
resource openAI_completionModel 'Microsoft.CognitiveServices/accounts/deployments@2022-12-01' = if(deployNewAzureOpenAI) {
|
||||
parent: openAI
|
||||
name: completionModel
|
||||
properties: {
|
||||
model: {
|
||||
format: 'OpenAI'
|
||||
name: completionModel
|
||||
}
|
||||
scaleSettings: {
|
||||
scaleType: 'Standard'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource openAI_embeddingModel 'Microsoft.CognitiveServices/accounts/deployments@2022-12-01' = if(deployNewAzureOpenAI) {
|
||||
parent: openAI
|
||||
name: embeddingModel
|
||||
properties: {
|
||||
model: {
|
||||
format: 'OpenAI'
|
||||
name: embeddingModel
|
||||
}
|
||||
scaleSettings: {
|
||||
scaleType: 'Standard'
|
||||
}
|
||||
}
|
||||
dependsOn: [ // This "dependency" is to create models sequentially because the resource
|
||||
openAI_completionModel // provider does not support parallel creation of models properly.
|
||||
]
|
||||
}
|
||||
|
||||
resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
|
||||
name: 'asp-${uniqueName}-webapi'
|
||||
location: location
|
||||
kind: 'app'
|
||||
sku: {
|
||||
name: webAppServiceSku
|
||||
}
|
||||
}
|
||||
|
||||
resource appServiceWeb 'Microsoft.Web/sites@2022-09-01' = {
|
||||
name: 'app-${uniqueName}-webapi'
|
||||
location: location
|
||||
kind: 'app'
|
||||
tags: {
|
||||
skweb: '1'
|
||||
}
|
||||
properties: {
|
||||
serverFarmId: appServicePlan.id
|
||||
httpsOnly: true
|
||||
virtualNetworkSubnetId: virtualNetwork.properties.subnets[0].id
|
||||
siteConfig: {
|
||||
healthCheckPath: '/healthz'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource appServiceWebConfig 'Microsoft.Web/sites/config@2022-09-01' = {
|
||||
parent: appServiceWeb
|
||||
name: 'web'
|
||||
properties: {
|
||||
alwaysOn: false
|
||||
cors: {
|
||||
allowedOrigins: [
|
||||
'http://localhost:3000'
|
||||
'https://localhost:3000'
|
||||
]
|
||||
supportCredentials: true
|
||||
}
|
||||
detailedErrorLoggingEnabled: true
|
||||
minTlsVersion: '1.2'
|
||||
netFrameworkVersion: 'v6.0'
|
||||
use32BitWorkerProcess: false
|
||||
vnetRouteAllEnabled: true
|
||||
webSocketsEnabled: true
|
||||
appSettings: [
|
||||
{
|
||||
name: 'AIService:Type'
|
||||
value: aiService
|
||||
}
|
||||
{
|
||||
name: 'AIService:Endpoint'
|
||||
value: deployNewAzureOpenAI ? openAI.properties.endpoint : aiEndpoint
|
||||
}
|
||||
{
|
||||
name: 'AIService:Key'
|
||||
value: deployNewAzureOpenAI ? openAI.listKeys().key1 : aiApiKey
|
||||
}
|
||||
{
|
||||
name: 'AIService:Models:Completion'
|
||||
value: completionModel
|
||||
}
|
||||
{
|
||||
name: 'AIService:Models:Embedding'
|
||||
value: embeddingModel
|
||||
}
|
||||
{
|
||||
name: 'AIService:Models:Planner'
|
||||
value: plannerModel
|
||||
}
|
||||
{
|
||||
name: 'Authorization:Type'
|
||||
value: empty(webApiKey) ? 'None' : 'ApiKey'
|
||||
}
|
||||
{
|
||||
name: 'Authorization:ApiKey'
|
||||
value: webApiKey
|
||||
}
|
||||
{
|
||||
name: 'ChatStore:Type'
|
||||
value: deployCosmosDB ? 'cosmos' : 'volatile'
|
||||
}
|
||||
{
|
||||
name: 'ChatStore:Cosmos:Database'
|
||||
value: 'CopilotChat'
|
||||
}
|
||||
{
|
||||
name: 'ChatStore:Cosmos:ChatSessionsContainer'
|
||||
value: 'chatsessions'
|
||||
}
|
||||
{
|
||||
name: 'ChatStore:Cosmos:ChatMessagesContainer'
|
||||
value: 'chatmessages'
|
||||
}
|
||||
{
|
||||
name: 'ChatStore:Cosmos:ChatMemorySourcesContainer'
|
||||
value: 'chatmemorysources'
|
||||
}
|
||||
{
|
||||
name: 'ChatStore:Cosmos:ChatParticipantsContainer'
|
||||
value: 'chatparticipants'
|
||||
}
|
||||
{
|
||||
name: 'ChatStore:Cosmos:ConnectionString'
|
||||
value: deployCosmosDB ? cosmosAccount.listConnectionStrings().connectionStrings[0].connectionString : ''
|
||||
}
|
||||
{
|
||||
name: 'MemoriesStore:Type'
|
||||
value: deployQdrant ? 'Qdrant' : 'Volatile'
|
||||
}
|
||||
{
|
||||
name: 'MemoriesStore:Qdrant:Host'
|
||||
value: deployQdrant ? 'https://${appServiceQdrant.properties.defaultHostName}' : ''
|
||||
}
|
||||
{
|
||||
name: 'MemoriesStore:Qdrant:Port'
|
||||
value: '443'
|
||||
}
|
||||
{
|
||||
name: 'AzureSpeech:Region'
|
||||
value: location
|
||||
}
|
||||
{
|
||||
name: 'AzureSpeech:Key'
|
||||
value: deploySpeechServices ? speechAccount.listKeys().key1 : ''
|
||||
}
|
||||
{
|
||||
name: 'AllowedOrigins'
|
||||
value: '[*]' // Defer list of allowed origins to the Azure service app's CORS configuration
|
||||
}
|
||||
{
|
||||
name: 'Kestrel:Endpoints:Https:Url'
|
||||
value: 'https://localhost:443'
|
||||
}
|
||||
{
|
||||
name: 'Logging:LogLevel:Default'
|
||||
value: 'Warning'
|
||||
}
|
||||
{
|
||||
name: 'Logging:LogLevel:SemanticKernel.Service'
|
||||
value: 'Warning'
|
||||
}
|
||||
{
|
||||
name: 'Logging:LogLevel:Microsoft.SemanticKernel'
|
||||
value: 'Warning'
|
||||
}
|
||||
{
|
||||
name: 'Logging:LogLevel:Microsoft.AspNetCore.Hosting'
|
||||
value: 'Warning'
|
||||
}
|
||||
{
|
||||
name: 'Logging:LogLevel:Microsoft.Hosting.Lifetimel'
|
||||
value: 'Warning'
|
||||
}
|
||||
{
|
||||
name: 'ApplicationInsights:ConnectionString'
|
||||
value: appInsights.properties.ConnectionString
|
||||
}
|
||||
{
|
||||
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
|
||||
value: appInsights.properties.ConnectionString
|
||||
}
|
||||
{
|
||||
name: 'ApplicationInsightsAgent_EXTENSION_VERSION'
|
||||
value: '~2'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource appServiceWebDeploy 'Microsoft.Web/sites/extensions@2022-09-01' = {
|
||||
name: 'MSDeploy'
|
||||
kind: 'string'
|
||||
parent: appServiceWeb
|
||||
properties: {
|
||||
packageUri: packageUri
|
||||
}
|
||||
dependsOn: [
|
||||
appServiceWebConfig
|
||||
]
|
||||
}
|
||||
|
||||
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
|
||||
name: 'appins-${uniqueName}'
|
||||
location: location
|
||||
kind: 'string'
|
||||
tags: {
|
||||
displayName: 'AppInsight'
|
||||
}
|
||||
properties: {
|
||||
Application_Type: 'web'
|
||||
WorkspaceResourceId: logAnalyticsWorkspace.id
|
||||
}
|
||||
}
|
||||
|
||||
resource appInsightExtension 'Microsoft.Web/sites/siteextensions@2022-09-01' = {
|
||||
parent: appServiceWeb
|
||||
name: 'Microsoft.ApplicationInsights.AzureWebSites'
|
||||
dependsOn: [
|
||||
appServiceWebDeploy
|
||||
]
|
||||
}
|
||||
|
||||
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
|
||||
name: 'la-${uniqueName}'
|
||||
location: location
|
||||
tags: {
|
||||
displayName: 'Log Analytics'
|
||||
}
|
||||
properties: {
|
||||
sku: {
|
||||
name: 'PerGB2018'
|
||||
}
|
||||
retentionInDays: 90
|
||||
features: {
|
||||
searchVersion: 1
|
||||
legacy: 0
|
||||
enableLogAccessUsingOnlyResourcePermissions: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource storage 'Microsoft.Storage/storageAccounts@2022-09-01' = if (deployQdrant) {
|
||||
name: 'st${rgIdHash}' // Not using full unique name to avoid hitting 24 char limit
|
||||
location: location
|
||||
kind: 'StorageV2'
|
||||
sku: {
|
||||
name: 'Standard_LRS'
|
||||
}
|
||||
properties: {
|
||||
supportsHttpsTrafficOnly: true
|
||||
allowBlobPublicAccess: false
|
||||
}
|
||||
resource fileservices 'fileServices' = {
|
||||
name: 'default'
|
||||
resource share 'shares' = {
|
||||
name: storageFileShareName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource appServicePlanQdrant 'Microsoft.Web/serverfarms@2022-03-01' = if (deployQdrant) {
|
||||
name: 'asp-${uniqueName}-qdrant'
|
||||
location: location
|
||||
kind: 'linux'
|
||||
sku: {
|
||||
name: 'P1v3'
|
||||
}
|
||||
properties: {
|
||||
reserved: true
|
||||
}
|
||||
}
|
||||
|
||||
resource appServiceQdrant 'Microsoft.Web/sites@2022-09-01' = if (deployQdrant) {
|
||||
name: 'app-${uniqueName}-qdrant'
|
||||
location: location
|
||||
kind: 'app,linux,container'
|
||||
properties: {
|
||||
serverFarmId: appServicePlanQdrant.id
|
||||
httpsOnly: true
|
||||
reserved: true
|
||||
clientCertMode: 'Required'
|
||||
virtualNetworkSubnetId: virtualNetwork.properties.subnets[1].id
|
||||
siteConfig: {
|
||||
numberOfWorkers: 1
|
||||
linuxFxVersion: 'DOCKER|qdrant/qdrant:latest'
|
||||
alwaysOn: true
|
||||
vnetRouteAllEnabled: true
|
||||
ipSecurityRestrictions: [
|
||||
{
|
||||
vnetSubnetResourceId: virtualNetwork.properties.subnets[0].id
|
||||
action: 'Allow'
|
||||
priority: 300
|
||||
name: 'Allow front vnet'
|
||||
}
|
||||
{
|
||||
ipAddress: 'Any'
|
||||
action: 'Deny'
|
||||
priority: 2147483647
|
||||
name: 'Deny all'
|
||||
}
|
||||
]
|
||||
azureStorageAccounts: {
|
||||
aciqdrantshare: {
|
||||
type: 'AzureFiles'
|
||||
accountName: deployQdrant ? storage.name : 'notdeployed'
|
||||
shareName: storageFileShareName
|
||||
mountPath: '/qdrant/storage'
|
||||
accessKey: deployQdrant ? storage.listKeys().keys[0].value : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2021-05-01' = {
|
||||
name: 'vnet-${uniqueName}'
|
||||
location: location
|
||||
properties: {
|
||||
addressSpace: {
|
||||
addressPrefixes: [
|
||||
'10.0.0.0/16'
|
||||
]
|
||||
}
|
||||
subnets: [
|
||||
{
|
||||
name: 'webSubnet'
|
||||
properties: {
|
||||
addressPrefix: '10.0.1.0/24'
|
||||
networkSecurityGroup: {
|
||||
id: webNsg.id
|
||||
}
|
||||
serviceEndpoints: [
|
||||
{
|
||||
service: 'Microsoft.Web'
|
||||
locations: [
|
||||
'*'
|
||||
]
|
||||
}
|
||||
]
|
||||
delegations: [
|
||||
{
|
||||
name: 'delegation'
|
||||
properties: {
|
||||
serviceName: 'Microsoft.Web/serverfarms'
|
||||
}
|
||||
}
|
||||
]
|
||||
privateEndpointNetworkPolicies: 'Disabled'
|
||||
privateLinkServiceNetworkPolicies: 'Enabled'
|
||||
}
|
||||
}
|
||||
{
|
||||
name: 'qdrantSubnet'
|
||||
properties: {
|
||||
addressPrefix: '10.0.2.0/24'
|
||||
networkSecurityGroup: {
|
||||
id: qdrantNsg.id
|
||||
}
|
||||
serviceEndpoints: [
|
||||
{
|
||||
service: 'Microsoft.Web'
|
||||
locations: [
|
||||
'*'
|
||||
]
|
||||
}
|
||||
]
|
||||
delegations: [
|
||||
{
|
||||
name: 'delegation'
|
||||
properties: {
|
||||
serviceName: 'Microsoft.Web/serverfarms'
|
||||
}
|
||||
}
|
||||
]
|
||||
privateEndpointNetworkPolicies: 'Disabled'
|
||||
privateLinkServiceNetworkPolicies: 'Enabled'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource webNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01' = {
|
||||
name: 'nsg-${uniqueName}-webapi'
|
||||
location: location
|
||||
properties: {
|
||||
securityRules: [
|
||||
{
|
||||
name: 'AllowAnyHTTPSInbound'
|
||||
properties: {
|
||||
protocol: 'TCP'
|
||||
sourcePortRange: '*'
|
||||
destinationPortRange: '443'
|
||||
sourceAddressPrefix: '*'
|
||||
destinationAddressPrefix: '*'
|
||||
access: 'Allow'
|
||||
priority: 100
|
||||
direction: 'Inbound'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource qdrantNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01' = {
|
||||
name: 'nsg-${uniqueName}-qdrant'
|
||||
location: location
|
||||
properties: {
|
||||
securityRules: []
|
||||
}
|
||||
}
|
||||
|
||||
resource webSubnetConnection 'Microsoft.Web/sites/virtualNetworkConnections@2022-09-01' = {
|
||||
parent: appServiceWeb
|
||||
name: 'webSubnetConnection'
|
||||
properties: {
|
||||
vnetResourceId: virtualNetwork.properties.subnets[0].id
|
||||
isSwift: true
|
||||
}
|
||||
}
|
||||
|
||||
resource qdrantSubnetConnection 'Microsoft.Web/sites/virtualNetworkConnections@2022-09-01' = if (deployQdrant) {
|
||||
parent: appServiceQdrant
|
||||
name: 'qdrantSubnetConnection'
|
||||
properties: {
|
||||
vnetResourceId: virtualNetwork.properties.subnets[1].id
|
||||
isSwift: true
|
||||
}
|
||||
}
|
||||
|
||||
resource cosmosAccount 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = if (deployCosmosDB) {
|
||||
name: toLower('cosmos-${uniqueName}')
|
||||
location: location
|
||||
kind: 'GlobalDocumentDB'
|
||||
properties: {
|
||||
consistencyPolicy: { defaultConsistencyLevel: 'Session' }
|
||||
locations: [ {
|
||||
locationName: location
|
||||
failoverPriority: 0
|
||||
isZoneRedundant: false
|
||||
}
|
||||
]
|
||||
databaseAccountOfferType: 'Standard'
|
||||
}
|
||||
}
|
||||
|
||||
resource cosmosDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2023-04-15' = if (deployCosmosDB) {
|
||||
parent: cosmosAccount
|
||||
name: 'CopilotChat'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'CopilotChat'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource messageContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = if (deployCosmosDB) {
|
||||
parent: cosmosDatabase
|
||||
name: 'chatmessages'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'chatmessages'
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent'
|
||||
automatic: true
|
||||
includedPaths: [
|
||||
{
|
||||
path: '/*'
|
||||
}
|
||||
]
|
||||
excludedPaths: [
|
||||
{
|
||||
path: '/"_etag"/?'
|
||||
}
|
||||
]
|
||||
}
|
||||
partitionKey: {
|
||||
paths: [
|
||||
'/id'
|
||||
]
|
||||
kind: 'Hash'
|
||||
version: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource sessionContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = if (deployCosmosDB) {
|
||||
parent: cosmosDatabase
|
||||
name: 'chatsessions'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'chatsessions'
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent'
|
||||
automatic: true
|
||||
includedPaths: [
|
||||
{
|
||||
path: '/*'
|
||||
}
|
||||
]
|
||||
excludedPaths: [
|
||||
{
|
||||
path: '/"_etag"/?'
|
||||
}
|
||||
]
|
||||
}
|
||||
partitionKey: {
|
||||
paths: [
|
||||
'/id'
|
||||
]
|
||||
kind: 'Hash'
|
||||
version: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource participantContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = if (deployCosmosDB) {
|
||||
parent: cosmosDatabase
|
||||
name: 'chatparticipants'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'chatparticipants'
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent'
|
||||
automatic: true
|
||||
includedPaths: [
|
||||
{
|
||||
path: '/*'
|
||||
}
|
||||
]
|
||||
excludedPaths: [
|
||||
{
|
||||
path: '/"_etag"/?'
|
||||
}
|
||||
]
|
||||
}
|
||||
partitionKey: {
|
||||
paths: [
|
||||
'/id'
|
||||
]
|
||||
kind: 'Hash'
|
||||
version: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource memorySourcesContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = if (deployCosmosDB) {
|
||||
parent: cosmosDatabase
|
||||
name: 'chatmemorysources'
|
||||
properties: {
|
||||
resource: {
|
||||
id: 'chatmemorysources'
|
||||
indexingPolicy: {
|
||||
indexingMode: 'consistent'
|
||||
automatic: true
|
||||
includedPaths: [
|
||||
{
|
||||
path: '/*'
|
||||
}
|
||||
]
|
||||
excludedPaths: [
|
||||
{
|
||||
path: '/"_etag"/?'
|
||||
}
|
||||
]
|
||||
}
|
||||
partitionKey: {
|
||||
paths: [
|
||||
'/id'
|
||||
]
|
||||
kind: 'Hash'
|
||||
version: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource speechAccount 'Microsoft.CognitiveServices/accounts@2022-12-01' = if (deploySpeechServices) {
|
||||
name: 'cog-${uniqueName}'
|
||||
location: location
|
||||
sku: {
|
||||
name: 'S0'
|
||||
}
|
||||
kind: 'SpeechServices'
|
||||
identity: {
|
||||
type: 'None'
|
||||
}
|
||||
properties: {
|
||||
customSubDomainName: 'cog-${uniqueName}'
|
||||
networkAcls: {
|
||||
defaultAction: 'Allow'
|
||||
}
|
||||
publicNetworkAccess: 'Enabled'
|
||||
}
|
||||
}
|
||||
|
||||
resource staticWebApp 'Microsoft.Web/staticSites@2022-09-01' = {
|
||||
name: 'swa-${uniqueName}'
|
||||
location: webappLocation
|
||||
properties: {
|
||||
provider: 'None'
|
||||
}
|
||||
sku: {
|
||||
name: 'Free'
|
||||
tier: 'Free'
|
||||
}
|
||||
}
|
||||
|
||||
output webappUrl string = staticWebApp.properties.defaultHostname
|
||||
output webappName string = staticWebApp.name
|
||||
output webapiUrl string = appServiceWeb.properties.defaultHostName
|
||||
output webapiName string = appServiceWeb.name
|
||||
@@ -1,891 +0,0 @@
|
||||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"metadata": {
|
||||
"_generator": {
|
||||
"name": "bicep",
|
||||
"version": "0.16.2.56959",
|
||||
"templateHash": "18037485528098010448"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"defaultValue": "copichat",
|
||||
"metadata": {
|
||||
"description": "Name for the deployment consisting of alphanumeric characters or dashes ('-')"
|
||||
}
|
||||
},
|
||||
"webAppServiceSku": {
|
||||
"type": "string",
|
||||
"defaultValue": "B1",
|
||||
"allowedValues": [
|
||||
"B1",
|
||||
"S1",
|
||||
"S2",
|
||||
"S3",
|
||||
"P1V3",
|
||||
"P2V3",
|
||||
"I1V2",
|
||||
"I2V2"
|
||||
],
|
||||
"metadata": {
|
||||
"description": "SKU for the Azure App Service plan"
|
||||
}
|
||||
},
|
||||
"packageUri": {
|
||||
"type": "string",
|
||||
"defaultValue": "https://aka.ms/copilotchat/webapi/latest",
|
||||
"metadata": {
|
||||
"description": "Location of package to deploy as the web service"
|
||||
}
|
||||
},
|
||||
"aiService": {
|
||||
"type": "string",
|
||||
"defaultValue": "AzureOpenAI",
|
||||
"allowedValues": [
|
||||
"AzureOpenAI",
|
||||
"OpenAI"
|
||||
],
|
||||
"metadata": {
|
||||
"description": "Underlying AI service"
|
||||
}
|
||||
},
|
||||
"completionModel": {
|
||||
"type": "string",
|
||||
"defaultValue": "gpt-35-turbo",
|
||||
"metadata": {
|
||||
"description": "Model to use for chat completions"
|
||||
}
|
||||
},
|
||||
"embeddingModel": {
|
||||
"type": "string",
|
||||
"defaultValue": "text-embedding-ada-002",
|
||||
"metadata": {
|
||||
"description": "Model to use for text embeddings"
|
||||
}
|
||||
},
|
||||
"plannerModel": {
|
||||
"type": "string",
|
||||
"defaultValue": "gpt-35-turbo",
|
||||
"metadata": {
|
||||
"description": "Completion model the task planner should use"
|
||||
}
|
||||
},
|
||||
"aiEndpoint": {
|
||||
"type": "string",
|
||||
"defaultValue": "",
|
||||
"metadata": {
|
||||
"description": "Azure OpenAI endpoint to use (Azure OpenAI only)"
|
||||
}
|
||||
},
|
||||
"aiApiKey": {
|
||||
"type": "securestring",
|
||||
"defaultValue": "",
|
||||
"metadata": {
|
||||
"description": "Azure OpenAI or OpenAI API key"
|
||||
}
|
||||
},
|
||||
"webApiKey": {
|
||||
"type": "securestring",
|
||||
"defaultValue": "[newGuid()]",
|
||||
"metadata": {
|
||||
"description": "WebAPI key to use for authorization"
|
||||
}
|
||||
},
|
||||
"deployNewAzureOpenAI": {
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"metadata": {
|
||||
"description": "Whether to deploy a new Azure OpenAI instance"
|
||||
}
|
||||
},
|
||||
"deployCosmosDB": {
|
||||
"type": "bool",
|
||||
"defaultValue": true,
|
||||
"metadata": {
|
||||
"description": "Whether to deploy Cosmos DB for persistent chat storage"
|
||||
}
|
||||
},
|
||||
"deployQdrant": {
|
||||
"type": "bool",
|
||||
"defaultValue": true,
|
||||
"metadata": {
|
||||
"description": "Whether to deploy Qdrant (in a container) for persistent memory storage"
|
||||
}
|
||||
},
|
||||
"deploySpeechServices": {
|
||||
"type": "bool",
|
||||
"defaultValue": true,
|
||||
"metadata": {
|
||||
"description": "Whether to deploy Azure Speech Services to enable input by voice"
|
||||
}
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"defaultValue": "[resourceGroup().location]",
|
||||
"metadata": {
|
||||
"description": "Region for the resources"
|
||||
}
|
||||
},
|
||||
"webappLocation": {
|
||||
"type": "string",
|
||||
"defaultValue": "westus2",
|
||||
"metadata": {
|
||||
"description": "Region for the webapp frontend"
|
||||
}
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"rgIdHash": "[uniqueString(resourceGroup().id)]",
|
||||
"uniqueName": "[format('{0}-{1}', parameters('name'), variables('rgIdHash'))]",
|
||||
"storageFileShareName": "aciqdrantshare"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"condition": "[parameters('deployQdrant')]",
|
||||
"type": "Microsoft.Storage/storageAccounts/fileServices/shares",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('{0}/{1}/{2}', format('st{0}', variables('rgIdHash')), 'default', variables('storageFileShareName'))]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts/fileServices', format('st{0}', variables('rgIdHash')), 'default')]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployQdrant')]",
|
||||
"type": "Microsoft.Storage/storageAccounts/fileServices",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('{0}/{1}', format('st{0}', variables('rgIdHash')), 'default')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployNewAzureOpenAI')]",
|
||||
"type": "Microsoft.CognitiveServices/accounts",
|
||||
"apiVersion": "2022-12-01",
|
||||
"name": "[format('ai-{0}', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"kind": "OpenAI",
|
||||
"sku": {
|
||||
"name": "S0"
|
||||
},
|
||||
"properties": {
|
||||
"customSubDomainName": "[toLower(variables('uniqueName'))]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployNewAzureOpenAI')]",
|
||||
"type": "Microsoft.CognitiveServices/accounts/deployments",
|
||||
"apiVersion": "2022-12-01",
|
||||
"name": "[format('{0}/{1}', format('ai-{0}', variables('uniqueName')), parameters('completionModel'))]",
|
||||
"properties": {
|
||||
"model": {
|
||||
"format": "OpenAI",
|
||||
"name": "[parameters('completionModel')]"
|
||||
},
|
||||
"scaleSettings": {
|
||||
"scaleType": "Standard"
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployNewAzureOpenAI')]",
|
||||
"type": "Microsoft.CognitiveServices/accounts/deployments",
|
||||
"apiVersion": "2022-12-01",
|
||||
"name": "[format('{0}/{1}', format('ai-{0}', variables('uniqueName')), parameters('embeddingModel'))]",
|
||||
"properties": {
|
||||
"model": {
|
||||
"format": "OpenAI",
|
||||
"name": "[parameters('embeddingModel')]"
|
||||
},
|
||||
"scaleSettings": {
|
||||
"scaleType": "Standard"
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.CognitiveServices/accounts/deployments', format('ai-{0}', variables('uniqueName')), parameters('completionModel'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Web/serverfarms",
|
||||
"apiVersion": "2022-03-01",
|
||||
"name": "[format('asp-{0}-webapi', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"kind": "app",
|
||||
"sku": {
|
||||
"name": "[parameters('webAppServiceSku')]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Web/sites",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('app-{0}-webapi', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"kind": "app",
|
||||
"tags": {
|
||||
"skweb": "1"
|
||||
},
|
||||
"properties": {
|
||||
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format('asp-{0}-webapi', variables('uniqueName')))]",
|
||||
"httpsOnly": true,
|
||||
"virtualNetworkSubnetId": "[reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[0].id]"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/serverfarms', format('asp-{0}-webapi', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Web/sites/config",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('{0}/{1}', format('app-{0}-webapi', variables('uniqueName')), 'web')]",
|
||||
"properties": {
|
||||
"alwaysOn": true,
|
||||
"cors": {
|
||||
"allowedOrigins": [
|
||||
"http://localhost:3000",
|
||||
"https://localhost:3000"
|
||||
],
|
||||
"supportCredentials": true
|
||||
},
|
||||
"detailedErrorLoggingEnabled": true,
|
||||
"minTlsVersion": "1.2",
|
||||
"netFrameworkVersion": "v6.0",
|
||||
"use32BitWorkerProcess": false,
|
||||
"vnetRouteAllEnabled": true,
|
||||
"webSocketsEnabled": true,
|
||||
"appSettings": [
|
||||
{
|
||||
"name": "AIService:Type",
|
||||
"value": "[parameters('aiService')]"
|
||||
},
|
||||
{
|
||||
"name": "AIService:Endpoint",
|
||||
"value": "[if(parameters('deployNewAzureOpenAI'), reference(resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName'))), '2022-12-01').endpoint, parameters('aiEndpoint'))]"
|
||||
},
|
||||
{
|
||||
"name": "AIService:Key",
|
||||
"value": "[if(parameters('deployNewAzureOpenAI'), listKeys(resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName'))), '2022-12-01').key1, parameters('aiApiKey'))]"
|
||||
},
|
||||
{
|
||||
"name": "AIService:Models:Completion",
|
||||
"value": "[parameters('completionModel')]"
|
||||
},
|
||||
{
|
||||
"name": "AIService:Models:Embedding",
|
||||
"value": "[parameters('embeddingModel')]"
|
||||
},
|
||||
{
|
||||
"name": "AIService:Models:Planner",
|
||||
"value": "[parameters('plannerModel')]"
|
||||
},
|
||||
{
|
||||
"name": "Authorization:Type",
|
||||
"value": "[if(empty(parameters('webApiKey')), 'None', 'ApiKey')]"
|
||||
},
|
||||
{
|
||||
"name": "Authorization:ApiKey",
|
||||
"value": "[parameters('webApiKey')]"
|
||||
},
|
||||
{
|
||||
"name": "ChatStore:Type",
|
||||
"value": "[if(parameters('deployCosmosDB'), 'cosmos', 'volatile')]"
|
||||
},
|
||||
{
|
||||
"name": "ChatStore:Cosmos:Database",
|
||||
"value": "CopilotChat"
|
||||
},
|
||||
{
|
||||
"name": "ChatStore:Cosmos:ChatSessionsContainer",
|
||||
"value": "chatsessions"
|
||||
},
|
||||
{
|
||||
"name": "ChatStore:Cosmos:ChatMessagesContainer",
|
||||
"value": "chatmessages"
|
||||
},
|
||||
{
|
||||
"name": "ChatStore:Cosmos:ChatMemorySourcesContainer",
|
||||
"value": "chatmemorysources"
|
||||
},
|
||||
{
|
||||
"name": "ChatStore:Cosmos:ChatParticipantsContainer",
|
||||
"value": "chatparticipants"
|
||||
},
|
||||
{
|
||||
"name": "ChatStore:Cosmos:ConnectionString",
|
||||
"value": "[if(parameters('deployCosmosDB'), listConnectionStrings(resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('cosmos-{0}', variables('uniqueName')))), '2023-04-15').connectionStrings[0].connectionString, '')]"
|
||||
},
|
||||
{
|
||||
"name": "MemoriesStore:Type",
|
||||
"value": "[if(parameters('deployQdrant'), 'Qdrant', 'Volatile')]"
|
||||
},
|
||||
{
|
||||
"name": "MemoriesStore:Qdrant:Host",
|
||||
"value": "[if(parameters('deployQdrant'), format('https://{0}', reference(resourceId('Microsoft.Web/sites', format('app-{0}-qdrant', variables('uniqueName'))), '2022-09-01').defaultHostName), '')]"
|
||||
},
|
||||
{
|
||||
"name": "MemoriesStore:Qdrant:Port",
|
||||
"value": "443"
|
||||
},
|
||||
{
|
||||
"name": "AzureSpeech:Region",
|
||||
"value": "[parameters('location')]"
|
||||
},
|
||||
{
|
||||
"name": "AzureSpeech:Key",
|
||||
"value": "[if(parameters('deploySpeechServices'), listKeys(resourceId('Microsoft.CognitiveServices/accounts', format('cog-{0}', variables('uniqueName'))), '2022-12-01').key1, '')]"
|
||||
},
|
||||
{
|
||||
"name": "AllowedOrigins",
|
||||
"value": "[[*]"
|
||||
},
|
||||
{
|
||||
"name": "Kestrel:Endpoints:Https:Url",
|
||||
"value": "https://localhost:443"
|
||||
},
|
||||
{
|
||||
"name": "Logging:LogLevel:Default",
|
||||
"value": "Warning"
|
||||
},
|
||||
{
|
||||
"name": "Logging:LogLevel:SemanticKernel.Service",
|
||||
"value": "Warning"
|
||||
},
|
||||
{
|
||||
"name": "Logging:LogLevel:Microsoft.SemanticKernel",
|
||||
"value": "Warning"
|
||||
},
|
||||
{
|
||||
"name": "Logging:LogLevel:Microsoft.AspNetCore.Hosting",
|
||||
"value": "Warning"
|
||||
},
|
||||
{
|
||||
"name": "Logging:LogLevel:Microsoft.Hosting.Lifetimel",
|
||||
"value": "Warning"
|
||||
},
|
||||
{
|
||||
"name": "ApplicationInsights:ConnectionString",
|
||||
"value": "[reference(resourceId('Microsoft.Insights/components', format('appins-{0}', variables('uniqueName'))), '2020-02-02').ConnectionString]"
|
||||
},
|
||||
{
|
||||
"name": "APPLICATIONINSIGHTS_CONNECTION_STRING",
|
||||
"value": "[reference(resourceId('Microsoft.Insights/components', format('appins-{0}', variables('uniqueName'))), '2020-02-02').ConnectionString]"
|
||||
},
|
||||
{
|
||||
"name": "ApplicationInsightsAgent_EXTENSION_VERSION",
|
||||
"value": "~2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Insights/components', format('appins-{0}', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.Web/sites', format('app-{0}-qdrant', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.Web/sites', format('app-{0}-webapi', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('cosmos-{0}', variables('uniqueName'))))]",
|
||||
"[resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.CognitiveServices/accounts', format('cog-{0}', variables('uniqueName')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Web/sites/extensions",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('{0}/{1}', format('app-{0}-webapi', variables('uniqueName')), 'MSDeploy')]",
|
||||
"kind": "string",
|
||||
"properties": {
|
||||
"packageUri": "[parameters('packageUri')]"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', format('app-{0}-webapi', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.Web/sites/config', format('app-{0}-webapi', variables('uniqueName')), 'web')]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Insights/components",
|
||||
"apiVersion": "2020-02-02",
|
||||
"name": "[format('appins-{0}', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"kind": "string",
|
||||
"tags": {
|
||||
"displayName": "AppInsight"
|
||||
},
|
||||
"properties": {
|
||||
"Application_Type": "web",
|
||||
"WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', format('la-{0}', variables('uniqueName')))]"
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.OperationalInsights/workspaces', format('la-{0}', variables('uniqueName')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Web/sites/siteextensions",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('{0}/{1}', format('app-{0}-webapi', variables('uniqueName')), 'Microsoft.ApplicationInsights.AzureWebSites')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', format('app-{0}-webapi', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.Web/sites/extensions', format('app-{0}-webapi', variables('uniqueName')), 'MSDeploy')]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.OperationalInsights/workspaces",
|
||||
"apiVersion": "2022-10-01",
|
||||
"name": "[format('la-{0}', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"tags": {
|
||||
"displayName": "Log Analytics"
|
||||
},
|
||||
"properties": {
|
||||
"sku": {
|
||||
"name": "PerGB2018"
|
||||
},
|
||||
"retentionInDays": 90,
|
||||
"features": {
|
||||
"searchVersion": 1,
|
||||
"legacy": 0,
|
||||
"enableLogAccessUsingOnlyResourcePermissions": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployQdrant')]",
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('st{0}', variables('rgIdHash'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"kind": "StorageV2",
|
||||
"sku": {
|
||||
"name": "Standard_LRS"
|
||||
},
|
||||
"properties": {
|
||||
"supportsHttpsTrafficOnly": true,
|
||||
"allowBlobPublicAccess": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployQdrant')]",
|
||||
"type": "Microsoft.Web/serverfarms",
|
||||
"apiVersion": "2022-03-01",
|
||||
"name": "[format('asp-{0}-qdrant', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"kind": "linux",
|
||||
"sku": {
|
||||
"name": "P1v3"
|
||||
},
|
||||
"properties": {
|
||||
"reserved": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployQdrant')]",
|
||||
"type": "Microsoft.Web/sites",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('app-{0}-qdrant', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"kind": "app,linux,container",
|
||||
"properties": {
|
||||
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format('asp-{0}-qdrant', variables('uniqueName')))]",
|
||||
"httpsOnly": true,
|
||||
"reserved": true,
|
||||
"clientCertMode": "Required",
|
||||
"virtualNetworkSubnetId": "[reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[1].id]",
|
||||
"siteConfig": {
|
||||
"numberOfWorkers": 1,
|
||||
"linuxFxVersion": "DOCKER|qdrant/qdrant:latest",
|
||||
"alwaysOn": true,
|
||||
"vnetRouteAllEnabled": true,
|
||||
"ipSecurityRestrictions": [
|
||||
{
|
||||
"vnetSubnetResourceId": "[reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[0].id]",
|
||||
"action": "Allow",
|
||||
"priority": 300,
|
||||
"name": "Allow front vnet"
|
||||
},
|
||||
{
|
||||
"ipAddress": "Any",
|
||||
"action": "Deny",
|
||||
"priority": 2147483647,
|
||||
"name": "Deny all"
|
||||
}
|
||||
],
|
||||
"azureStorageAccounts": {
|
||||
"aciqdrantshare": {
|
||||
"type": "AzureFiles",
|
||||
"accountName": "[if(parameters('deployQdrant'), format('st{0}', variables('rgIdHash')), 'notdeployed')]",
|
||||
"shareName": "[variables('storageFileShareName')]",
|
||||
"mountPath": "/qdrant/storage",
|
||||
"accessKey": "[if(parameters('deployQdrant'), listKeys(resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash'))), '2022-09-01').keys[0].value, '')]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/serverfarms', format('asp-{0}-qdrant', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash')))]",
|
||||
"[resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Network/virtualNetworks",
|
||||
"apiVersion": "2021-05-01",
|
||||
"name": "[format('vnet-{0}', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"properties": {
|
||||
"addressSpace": {
|
||||
"addressPrefixes": [
|
||||
"10.0.0.0/16"
|
||||
]
|
||||
},
|
||||
"subnets": [
|
||||
{
|
||||
"name": "webSubnet",
|
||||
"properties": {
|
||||
"addressPrefix": "10.0.1.0/24",
|
||||
"networkSecurityGroup": {
|
||||
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('nsg-{0}-webapi', variables('uniqueName')))]"
|
||||
},
|
||||
"serviceEndpoints": [
|
||||
{
|
||||
"service": "Microsoft.Web",
|
||||
"locations": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"delegations": [
|
||||
{
|
||||
"name": "delegation",
|
||||
"properties": {
|
||||
"serviceName": "Microsoft.Web/serverfarms"
|
||||
}
|
||||
}
|
||||
],
|
||||
"privateEndpointNetworkPolicies": "Disabled",
|
||||
"privateLinkServiceNetworkPolicies": "Enabled"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "qdrantSubnet",
|
||||
"properties": {
|
||||
"addressPrefix": "10.0.2.0/24",
|
||||
"networkSecurityGroup": {
|
||||
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('nsg-{0}-qdrant', variables('uniqueName')))]"
|
||||
},
|
||||
"serviceEndpoints": [
|
||||
{
|
||||
"service": "Microsoft.Web",
|
||||
"locations": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"delegations": [
|
||||
{
|
||||
"name": "delegation",
|
||||
"properties": {
|
||||
"serviceName": "Microsoft.Web/serverfarms"
|
||||
}
|
||||
}
|
||||
],
|
||||
"privateEndpointNetworkPolicies": "Disabled",
|
||||
"privateLinkServiceNetworkPolicies": "Enabled"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Network/networkSecurityGroups', format('nsg-{0}-qdrant', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.Network/networkSecurityGroups', format('nsg-{0}-webapi', variables('uniqueName')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Network/networkSecurityGroups",
|
||||
"apiVersion": "2022-11-01",
|
||||
"name": "[format('nsg-{0}-webapi', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"properties": {
|
||||
"securityRules": [
|
||||
{
|
||||
"name": "AllowAnyHTTPSInbound",
|
||||
"properties": {
|
||||
"protocol": "TCP",
|
||||
"sourcePortRange": "*",
|
||||
"destinationPortRange": "443",
|
||||
"sourceAddressPrefix": "*",
|
||||
"destinationAddressPrefix": "*",
|
||||
"access": "Allow",
|
||||
"priority": 100,
|
||||
"direction": "Inbound"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Network/networkSecurityGroups",
|
||||
"apiVersion": "2022-11-01",
|
||||
"name": "[format('nsg-{0}-qdrant', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"properties": {
|
||||
"securityRules": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Web/sites/virtualNetworkConnections",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('{0}/{1}', format('app-{0}-webapi', variables('uniqueName')), 'webSubnetConnection')]",
|
||||
"properties": {
|
||||
"vnetResourceId": "[reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[0].id]",
|
||||
"isSwift": true
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', format('app-{0}-webapi', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployQdrant')]",
|
||||
"type": "Microsoft.Web/sites/virtualNetworkConnections",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('{0}/{1}', format('app-{0}-qdrant', variables('uniqueName')), 'qdrantSubnetConnection')]",
|
||||
"properties": {
|
||||
"vnetResourceId": "[reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[1].id]",
|
||||
"isSwift": true
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Web/sites', format('app-{0}-qdrant', variables('uniqueName')))]",
|
||||
"[resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName')))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployCosmosDB')]",
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts",
|
||||
"apiVersion": "2023-04-15",
|
||||
"name": "[toLower(format('cosmos-{0}', variables('uniqueName')))]",
|
||||
"location": "[parameters('location')]",
|
||||
"kind": "GlobalDocumentDB",
|
||||
"properties": {
|
||||
"consistencyPolicy": {
|
||||
"defaultConsistencyLevel": "Session"
|
||||
},
|
||||
"locations": [
|
||||
{
|
||||
"locationName": "[parameters('location')]",
|
||||
"failoverPriority": 0,
|
||||
"isZoneRedundant": false
|
||||
}
|
||||
],
|
||||
"databaseAccountOfferType": "Standard"
|
||||
}
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployCosmosDB')]",
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases",
|
||||
"apiVersion": "2023-04-15",
|
||||
"name": "[format('{0}/{1}', toLower(format('cosmos-{0}', variables('uniqueName'))), 'CopilotChat')]",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "CopilotChat"
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('cosmos-{0}', variables('uniqueName'))))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployCosmosDB')]",
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"apiVersion": "2023-04-15",
|
||||
"name": "[format('{0}/{1}/{2}', toLower(format('cosmos-{0}', variables('uniqueName'))), 'CopilotChat', 'chatmessages')]",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "chatmessages",
|
||||
"indexingPolicy": {
|
||||
"indexingMode": "consistent",
|
||||
"automatic": true,
|
||||
"includedPaths": [
|
||||
{
|
||||
"path": "/*"
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
{
|
||||
"path": "/\"_etag\"/?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
],
|
||||
"kind": "Hash",
|
||||
"version": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', toLower(format('cosmos-{0}', variables('uniqueName'))), 'CopilotChat')]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployCosmosDB')]",
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"apiVersion": "2023-04-15",
|
||||
"name": "[format('{0}/{1}/{2}', toLower(format('cosmos-{0}', variables('uniqueName'))), 'CopilotChat', 'chatsessions')]",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "chatsessions",
|
||||
"indexingPolicy": {
|
||||
"indexingMode": "consistent",
|
||||
"automatic": true,
|
||||
"includedPaths": [
|
||||
{
|
||||
"path": "/*"
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
{
|
||||
"path": "/\"_etag\"/?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
],
|
||||
"kind": "Hash",
|
||||
"version": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', toLower(format('cosmos-{0}', variables('uniqueName'))), 'CopilotChat')]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployCosmosDB')]",
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"apiVersion": "2023-04-15",
|
||||
"name": "[format('{0}/{1}/{2}', toLower(format('cosmos-{0}', variables('uniqueName'))), 'CopilotChat', 'chatparticipants')]",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "chatparticipants",
|
||||
"indexingPolicy": {
|
||||
"indexingMode": "consistent",
|
||||
"automatic": true,
|
||||
"includedPaths": [
|
||||
{
|
||||
"path": "/*"
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
{
|
||||
"path": "/\"_etag\"/?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
],
|
||||
"kind": "Hash",
|
||||
"version": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', toLower(format('cosmos-{0}', variables('uniqueName'))), 'CopilotChat')]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deployCosmosDB')]",
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"apiVersion": "2023-04-15",
|
||||
"name": "[format('{0}/{1}/{2}', toLower(format('cosmos-{0}', variables('uniqueName'))), 'CopilotChat', 'chatmemorysources')]",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "chatmemorysources",
|
||||
"indexingPolicy": {
|
||||
"indexingMode": "consistent",
|
||||
"automatic": true,
|
||||
"includedPaths": [
|
||||
{
|
||||
"path": "/*"
|
||||
}
|
||||
],
|
||||
"excludedPaths": [
|
||||
{
|
||||
"path": "/\"_etag\"/?"
|
||||
}
|
||||
]
|
||||
},
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
],
|
||||
"kind": "Hash",
|
||||
"version": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', toLower(format('cosmos-{0}', variables('uniqueName'))), 'CopilotChat')]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"condition": "[parameters('deploySpeechServices')]",
|
||||
"type": "Microsoft.CognitiveServices/accounts",
|
||||
"apiVersion": "2022-12-01",
|
||||
"name": "[format('cog-{0}', variables('uniqueName'))]",
|
||||
"location": "[parameters('location')]",
|
||||
"sku": {
|
||||
"name": "S0"
|
||||
},
|
||||
"kind": "SpeechServices",
|
||||
"identity": {
|
||||
"type": "None"
|
||||
},
|
||||
"properties": {
|
||||
"customSubDomainName": "[format('cog-{0}', variables('uniqueName'))]",
|
||||
"networkAcls": {
|
||||
"defaultAction": "Allow"
|
||||
},
|
||||
"publicNetworkAccess": "Enabled"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Web/staticSites",
|
||||
"apiVersion": "2022-09-01",
|
||||
"name": "[format('swa-{0}', variables('uniqueName'))]",
|
||||
"location": "[parameters('webappLocation')]",
|
||||
"properties": {
|
||||
"provider": "None"
|
||||
},
|
||||
"sku": {
|
||||
"name": "Free",
|
||||
"tier": "Free"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": {
|
||||
"webappUrl": {
|
||||
"type": "string",
|
||||
"value": "[reference(resourceId('Microsoft.Web/staticSites', format('swa-{0}', variables('uniqueName'))), '2022-09-01').defaultHostname]"
|
||||
},
|
||||
"webappName": {
|
||||
"type": "string",
|
||||
"value": "[format('swa-{0}', variables('uniqueName'))]"
|
||||
},
|
||||
"webapiUrl": {
|
||||
"type": "string",
|
||||
"value": "[reference(resourceId('Microsoft.Web/sites', format('app-{0}-webapi', variables('uniqueName'))), '2022-09-01').defaultHostName]"
|
||||
},
|
||||
"webapiName": {
|
||||
"type": "string",
|
||||
"value": "[format('app-{0}-webapi', variables('uniqueName'))]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Package CopilotChat's WebAPI for deployment to Azure
|
||||
#>
|
||||
|
||||
param(
|
||||
[string]
|
||||
# Build configuration to publish.
|
||||
$BuildConfiguration = "Release",
|
||||
|
||||
[string]
|
||||
# .NET framework to publish.
|
||||
$DotNetFramework = "net6.0",
|
||||
|
||||
[string]
|
||||
# Target runtime to publish.
|
||||
$TargetRuntime = "win-x64",
|
||||
|
||||
[string]
|
||||
# Output directory for published assets.
|
||||
$OutputDirectory = "$PSScriptRoot"
|
||||
)
|
||||
|
||||
Write-Host "BuildConfiguration: $BuildConfiguration"
|
||||
Write-Host "DotNetFramework: $DotNetFramework"
|
||||
Write-Host "TargetRuntime: $TargetRuntime"
|
||||
Write-Host "OutputDirectory: $OutputDirectory"
|
||||
|
||||
$publishOutputDirectory = "$OutputDirectory/publish"
|
||||
$publishedZipDirectory = "$OutputDirectory/out"
|
||||
$publishedZipFilePath = "$publishedZipDirectory/webapi.zip"
|
||||
if (!(Test-Path $publishedZipDirectory)) {
|
||||
New-Item -ItemType Directory -Force -Path $publishedZipDirectory | Out-Null
|
||||
}
|
||||
if (!(Test-Path $publishOutputDirectory)) {
|
||||
New-Item -ItemType Directory -Force -Path $publishOutputDirectory | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "Build configuration: $BuildConfiguration"
|
||||
dotnet publish "$PSScriptRoot/../webapi/CopilotChatWebApi.csproj" --configuration $BuildConfiguration --framework $DotNetFramework --runtime $TargetRuntime --self-contained --output "$publishOutputDirectory"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host "Compressing to $publishedZipFilePath"
|
||||
Compress-Archive -Path $publishOutputDirectory\* -DestinationPath $publishedZipFilePath -Force
|
||||
|
||||
Write-Host "Published webapi package to '$publishedZipFilePath'"
|
||||
@@ -1,88 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Package CiopilotChat's WebAPI for deployment to Azure
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
OUTPUT_DIRECTORY="$SCRIPT_ROOT"
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 -d DEPLOYMENT_NAME -s SUBSCRIPTION --ai AI_SERVICE_TYPE -aikey AI_SERVICE_KEY [OPTIONS]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " -c, --configuration CONFIGURATION Build configuration (default: Release)"
|
||||
echo " -d, --dotnet DOTNET_FRAMEWORK_VERSION Target dotnet framework (default: net6.0)"
|
||||
echo " -r, --runtime TARGET_RUNTIME Runtime identifier (default: linux-x64)"
|
||||
echo " -p, --output OUTPUT_DIRECTORY Output directory (default: $SCRIPT_ROOT)"
|
||||
echo " -nz, --no-zip Do not zip package (default: false)"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
case $key in
|
||||
-c|--configuration)
|
||||
CONFIGURATION="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-d|--dotnet)
|
||||
DOTNET="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-r|--runtime)
|
||||
RUNTIME="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-o|--output)
|
||||
OUTPUT_DIRECTORY="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-nz|--no-zip)
|
||||
NO_ZIP=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Set defaults
|
||||
: "${CONFIGURATION:="Release"}"
|
||||
: "${DOTNET:="net6.0"}"
|
||||
: "${RUNTIME:="win-x64"}"
|
||||
: "${OUTPUT_DIRECTORY:="$SCRIPT_ROOT"}"
|
||||
|
||||
PUBLISH_OUTPUT_DIRECTORY="$OUTPUT_DIRECTORY/publish"
|
||||
PUBLISH_ZIP_DIRECTORY="$OUTPUT_DIRECTORY/out"
|
||||
PACKAGE_FILE_PATH="$PUBLISH_ZIP_DIRECTORY/webapi.zip"
|
||||
|
||||
if [[ ! -d "$PUBLISH_OUTPUT_DIRECTORY" ]]; then
|
||||
mkdir -p "$PUBLISH_OUTPUT_DIRECTORY"
|
||||
fi
|
||||
if [[ ! -d "$PUBLISH_ZIP_DIRECTORY" ]]; then
|
||||
mkdir -p "$PUBLISH_ZIP_DIRECTORY"
|
||||
fi
|
||||
|
||||
echo "Build configuration: $CONFIGURATION"
|
||||
dotnet publish "$SCRIPT_ROOT/../webapi/CopilotChatWebApi.csproj" --configuration $CONFIGURATION --framework $DOTNET --runtime $RUNTIME --self-contained --output "$PUBLISH_OUTPUT_DIRECTORY"
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# if not NO_ZIP then zip the package
|
||||
if [[ -z "$NO_ZIP" ]]; then
|
||||
pushd "$PUBLISH_OUTPUT_DIRECTORY"
|
||||
echo "Compressing to $PACKAGE_FILE_PATH"
|
||||
zip -r $PACKAGE_FILE_PATH .
|
||||
popd
|
||||
fi
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace ImportDocument;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the app.
|
||||
/// </summary>
|
||||
public sealed class Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Client ID for the app as registered in Azure AD.
|
||||
/// </summary>
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Redirect URI for the app as registered in Azure AD.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1056 // URI-like properties should not be strings
|
||||
public string RedirectUri { get; set; } = string.Empty;
|
||||
#pragma warning restore CA1056 // URI-like properties should not be strings
|
||||
|
||||
/// <summary>
|
||||
/// Uri for the service that is running the chat.
|
||||
/// </summary>
|
||||
#pragma warning disable CA1056 // URI-like properties should not be strings
|
||||
public string ServiceUri { get; set; } = string.Empty;
|
||||
#pragma warning restore CA1056 // URI-like properties should not be strings
|
||||
|
||||
/// <summary>
|
||||
/// Api key for the service that is running the chat.
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets configuration from appsettings.json.
|
||||
/// </summary>
|
||||
/// <returns>An Config instance</returns>
|
||||
public static Config? GetConfig()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
return config.GetRequiredSection("Config").Get<Config>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a Config object.
|
||||
/// </summary>
|
||||
/// <param name="config"></param>
|
||||
/// <returns>True is the config object is not null.</returns>
|
||||
public static bool Validate(Config? config)
|
||||
{
|
||||
return config != null;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RollForward>LatestMajor</RollForward>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>10</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0-preview.3.23174.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0-preview.3.23174.8" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.54.1" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.CLSCompliantAttribute">
|
||||
<_Parameter1>false</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,216 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.CommandLine;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Identity.Client;
|
||||
|
||||
namespace ImportDocument;
|
||||
|
||||
/// <summary>
|
||||
/// This console app imports a list of files to the CopilotChat WebAPI document memory store.
|
||||
/// </summary>
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var config = Config.GetConfig();
|
||||
if (!Config.Validate(config))
|
||||
{
|
||||
Console.WriteLine("Error: Failed to read appsettings.json.");
|
||||
return;
|
||||
}
|
||||
|
||||
var filesOption = new Option<IEnumerable<FileInfo>>(name: "--files", description: "The files to import to document memory store.")
|
||||
{
|
||||
IsRequired = true,
|
||||
AllowMultipleArgumentsPerToken = true,
|
||||
};
|
||||
|
||||
var chatCollectionOption = new Option<Guid>(
|
||||
name: "--chat-id",
|
||||
description: "Save the extracted context to an isolated chat collection.",
|
||||
getDefaultValue: () => Guid.Empty
|
||||
);
|
||||
|
||||
var rootCommand = new RootCommand(
|
||||
"This console app imports files to the CopilotChat WebAPI's document memory store."
|
||||
)
|
||||
{
|
||||
filesOption, chatCollectionOption
|
||||
};
|
||||
|
||||
rootCommand.SetHandler(async (files, chatCollectionId) =>
|
||||
{
|
||||
await ImportFilesAsync(files, config!, chatCollectionId);
|
||||
},
|
||||
filesOption, chatCollectionOption
|
||||
);
|
||||
|
||||
rootCommand.Invoke(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Acquires a user account from Azure AD.
|
||||
/// </summary>
|
||||
/// <param name="config">The App configuration.</param>
|
||||
/// <param name="setAccount">Sets the account to the first account found.</param>
|
||||
/// <param name="setAccessToken">Sets the access token to the first account found.</param>
|
||||
/// <returns>True if the user account was acquired.</returns>
|
||||
private static async Task<bool> AcquireUserAccountAsync(
|
||||
Config config,
|
||||
Action<IAccount> setAccount,
|
||||
Action<string> setAccessToken)
|
||||
{
|
||||
Console.WriteLine("Requesting User Account ID...");
|
||||
|
||||
string[] scopes = { "User.Read" };
|
||||
try
|
||||
{
|
||||
var app = PublicClientApplicationBuilder.Create(config.ClientId)
|
||||
.WithRedirectUri(config.RedirectUri)
|
||||
.Build();
|
||||
var result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();
|
||||
IEnumerable<IAccount>? accounts = await app.GetAccountsAsync();
|
||||
IAccount? first = accounts.FirstOrDefault();
|
||||
|
||||
if (first is null)
|
||||
{
|
||||
Console.WriteLine("Error: No accounts found");
|
||||
return false;
|
||||
}
|
||||
|
||||
setAccount(first);
|
||||
setAccessToken(result.AccessToken);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is MsalServiceException or MsalClientException)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Conditionally imports a list of files to the Document Store.
|
||||
/// </summary>
|
||||
/// <param name="files">A list of files to import.</param>
|
||||
/// <param name="config">Configuration.</param>
|
||||
/// <param name="chatCollectionId">Save the extracted context to an isolated chat collection.</param>
|
||||
private static async Task ImportFilesAsync(IEnumerable<FileInfo> files, Config config, Guid chatCollectionId)
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (!file.Exists)
|
||||
{
|
||||
Console.WriteLine($"File {file.FullName} does not exist.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
IAccount? userAccount = null;
|
||||
string? accessToken = null;
|
||||
|
||||
if (await AcquireUserAccountAsync(config, v => { userAccount = v; }, v => { accessToken = v; }) == false)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to acquire user account.");
|
||||
return;
|
||||
}
|
||||
Console.WriteLine($"Successfully acquired User ID. Continuing...");
|
||||
|
||||
using var formContent = new MultipartFormDataContent();
|
||||
List<StreamContent> filesContent = files.Select(file => new StreamContent(file.OpenRead())).ToList();
|
||||
for (int i = 0; i < filesContent.Count; i++)
|
||||
{
|
||||
formContent.Add(filesContent[i], "formFiles", files.ElementAt(i).Name);
|
||||
}
|
||||
|
||||
var userId = userAccount!.HomeAccountId.Identifier;
|
||||
var userName = userAccount.Username;
|
||||
using var userIdContent = new StringContent(userId);
|
||||
using var userNameContent = new StringContent(userName);
|
||||
formContent.Add(userIdContent, "userId");
|
||||
formContent.Add(userNameContent, "userName");
|
||||
|
||||
if (chatCollectionId != Guid.Empty)
|
||||
{
|
||||
Console.WriteLine($"Uploading and parsing file to chat {chatCollectionId}...");
|
||||
using var chatScopeContent = new StringContent("Chat");
|
||||
using var chatCollectionIdContent = new StringContent(chatCollectionId.ToString());
|
||||
formContent.Add(chatScopeContent, "documentScope");
|
||||
formContent.Add(chatCollectionIdContent, "chatId");
|
||||
|
||||
// Calling UploadAsync here to make sure disposable objects are still in scope.
|
||||
await UploadAsync(formContent, accessToken!, config);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Uploading and parsing file to global collection...");
|
||||
using var globalScopeContent = new StringContent("Global");
|
||||
formContent.Add(globalScopeContent, "documentScope");
|
||||
|
||||
// Calling UploadAsync here to make sure disposable objects are still in scope.
|
||||
await UploadAsync(formContent, accessToken!, config);
|
||||
}
|
||||
|
||||
// Dispose of all the file streams.
|
||||
foreach (var fileContent in filesContent)
|
||||
{
|
||||
fileContent.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a POST request to the Document Store to upload a file for parsing.
|
||||
/// </summary>
|
||||
/// <param name="multipartFormDataContent">The multipart form data content to send.</param>
|
||||
/// <param name="config">Configuration.</param>
|
||||
private static async Task UploadAsync(
|
||||
MultipartFormDataContent multipartFormDataContent,
|
||||
string accessToken,
|
||||
Config config)
|
||||
{
|
||||
// Create a HttpClient instance and set the timeout to infinite since
|
||||
// large documents will take a while to parse.
|
||||
using HttpClientHandler clientHandler = new()
|
||||
{
|
||||
CheckCertificateRevocationList = true
|
||||
};
|
||||
using HttpClient httpClient = new(clientHandler)
|
||||
{
|
||||
Timeout = Timeout.InfiniteTimeSpan
|
||||
};
|
||||
// Add required properties to the request header.
|
||||
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken}");
|
||||
if (!string.IsNullOrEmpty(config.ApiKey))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Add("x-sk-api-key", config.ApiKey);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using HttpResponseMessage response = await httpClient.PostAsync(
|
||||
new Uri(new Uri(config.ServiceUri), "importDocuments"),
|
||||
multipartFormDataContent
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Error: {response.StatusCode} {response.ReasonPhrase}");
|
||||
Console.WriteLine(await response.Content.ReadAsStringAsync());
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Uploading and parsing successful.");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
# Copilot Chat Import Document App
|
||||
|
||||
> **!IMPORTANT**
|
||||
> This sample is for educational purposes only and is not recommended for production deployments.
|
||||
|
||||
One of the exciting features of the Copilot Chat App is its ability to store contextual information
|
||||
to [memories](https://github.com/microsoft/semantic-kernel/blob/main/docs/EMBEDDINGS.md) and retrieve
|
||||
relevant information from memories to provide more meaningful answers to users through out the conversations.
|
||||
|
||||
Memories can be generated from conversations as well as imported from external sources, such as documents.
|
||||
Importing documents enables Copilot Chat to have up-to-date knowledge of specific contexts, such as enterprise and personal data.
|
||||
|
||||
## Configure your environment
|
||||
1. A registered App in Azure Portal (https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app)
|
||||
- Select Mobile and desktop applications as platform type, and the Redirect URI will be `http://localhost`
|
||||
- Select **`Accounts in any organizational directory (Any Azure AD directory - Multitenant)
|
||||
and personal Microsoft accounts (e.g. Skype, Xbox)`** as the supported account
|
||||
type for this sample.
|
||||
- Note the **`Application (client) ID`** from your app registration.
|
||||
2. Make sure the service is running. To start the service, see [here](../webapi/README.md).
|
||||
|
||||
## Running the app
|
||||
1. Ensure the web api is running at `https://localhost:40443/`.
|
||||
2. Configure the appsettings.json file under this folder root with the following variables and fill
|
||||
in with your information, where
|
||||
`ClientId` is the GUID copied from the **Application (client) ID** from your app registration in the Azure Portal,
|
||||
`RedirectUri` is the Redirect URI also from the app registration in the Azure Portal, and
|
||||
`ServiceUri` is the address the web api is running at.
|
||||
`ApiKey` is the API key to the service if there is one.
|
||||
|
||||
3. Change directory to this folder root.
|
||||
4. **Run** the following command to import a document to the app under the global document collection where
|
||||
all users will have access to:
|
||||
|
||||
`dotnet run --files .\sample-docs\ms10k.txt`
|
||||
|
||||
Or **Run** the following command to import a document to the app under a chat isolated document collection where
|
||||
only the chat session will have access to:
|
||||
|
||||
`dotnet run --files .\sample-docs\ms10k.txt --chat-id [chatId]`
|
||||
|
||||
> Note that this will open a browser window for you to sign in to retrieve your user id to make sure you have access to the chat session.
|
||||
|
||||
> Currently only supports txt and pdf files. A sample file is provided under ./sample-docs.
|
||||
|
||||
Importing may take some time to generate embeddings for each piece/chunk of a document.
|
||||
|
||||
To import multiple files, specify multiple files. For example:
|
||||
|
||||
`dotnet run --files .\sample-docs\ms10k.txt .\sample-docs\Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf`
|
||||
|
||||
5. Chat with the bot.
|
||||
|
||||
Examples:
|
||||
|
||||
With [ms10k.txt](./sample-docs/ms10k.txt):
|
||||
|
||||

|
||||
|
||||
With [Microsoft Responsible AI Standard v2 General Requirements.pdf](./sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf):
|
||||
|
||||

|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Config": {
|
||||
"ClientId": "",
|
||||
"RedirectUri": "",
|
||||
"ServiceUri": "https://localhost:40443",
|
||||
"ApiKey": ""
|
||||
}
|
||||
}
|
||||
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,136 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Configure user secrets, appsettings.Development.json, and .env for Copilot Chat.
|
||||
|
||||
.PARAMETER OpenAI
|
||||
Switch to configure for OpenAI.
|
||||
|
||||
.PARAMETER AzureOpenAI
|
||||
Switch to configure for Azure OpenAI.
|
||||
|
||||
.PARAMETER Endpoint
|
||||
Set when using Azure OpenAI.
|
||||
|
||||
.PARAMETER ApiKey
|
||||
The API key for the AI service.
|
||||
|
||||
.PARAMETER CompletionModel
|
||||
The chat completion model to use (e.g., gpt-3.5-turbo or gpt-4).
|
||||
|
||||
.PARAMETER EmbeddingModel
|
||||
The embedding model to use (e.g., text-embedding-ada-002).
|
||||
|
||||
.PARAMETER PlannerModel
|
||||
The chat completion model to use for planning (e.g., gpt-3.5-turbo or gpt-4).
|
||||
|
||||
.PARAMETER ClientID
|
||||
The client (application) ID associated with your AAD app registration.
|
||||
|
||||
.PARAMETER Tenant
|
||||
The tenant (directory) associated with your AAD app registration.
|
||||
Defaults to 'common'.
|
||||
See https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-client-application-configuration#authority.
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(ParameterSetName='OpenAI',Mandatory=$false)]
|
||||
[switch]$OpenAI,
|
||||
|
||||
[Parameter(ParameterSetName='AzureOpenAI',Mandatory=$false)]
|
||||
[switch]$AzureOpenAI,
|
||||
|
||||
[Parameter(ParameterSetName='AzureOpenAI',Mandatory=$true)]
|
||||
[string]$Endpoint,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$ApiKey,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$CompletionModel = "gpt-3.5-turbo",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$EmbeddingModel = "text-embedding-ada-002",
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$PlannerModel = "gpt-3.5-turbo",
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $ClientId,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string] $Tenant = 'common'
|
||||
)
|
||||
|
||||
Write-Host "#########################"
|
||||
Write-Host "# Backend configuration #"
|
||||
Write-Host "#########################"
|
||||
|
||||
# Install dev certificate
|
||||
if ($IsLinux)
|
||||
{
|
||||
dotnet dev-certs https
|
||||
if ($LASTEXITCODE -ne 0) { exit(1) }
|
||||
}
|
||||
else # Windows/MacOS
|
||||
{
|
||||
dotnet dev-certs https --trust
|
||||
if ($LASTEXITCODE -ne 0) { exit(1) }
|
||||
}
|
||||
|
||||
if ($OpenAI)
|
||||
{
|
||||
$aiServiceType = "OpenAI"
|
||||
$Endpoint = ""
|
||||
}
|
||||
elseif ($AzureOpenAI)
|
||||
{
|
||||
$aiServiceType = "AzureOpenAI"
|
||||
|
||||
# Azure OpenAI has a different model name for gpt-3.5-turbo (no decimal).
|
||||
$CompletionModel = $CompletionModel.Replace("3.5", "35")
|
||||
$EmbeddingModel = $EmbeddingModel.Replace("3.5", "35")
|
||||
$PlannerModel = $PlannerModel.Replace("3.5", "35")
|
||||
}
|
||||
else {
|
||||
Write-Error "Please specify either -OpenAI or -AzureOpenAI"
|
||||
exit(1)
|
||||
}
|
||||
|
||||
$appsettingsOverrides = @{ AIService = @{ Type = $aiServiceType; Endpoint = $Endpoint; Models = @{ Completion = $CompletionModel; Embedding = $EmbeddingModel; Planner = $PlannerModel } } }
|
||||
|
||||
$webapiProjectPath = Join-Path "$PSScriptRoot" '../webapi'
|
||||
$appsettingsOverridesFilePath = Join-Path $webapiProjectPath 'appsettings.Development.json'
|
||||
|
||||
Write-Host "Setting 'AIService:Key' user secret for $aiServiceType..."
|
||||
dotnet user-secrets set --project $webapiProjectPath AIService:Key $ApiKey
|
||||
if ($LASTEXITCODE -ne 0) { exit(1) }
|
||||
|
||||
Write-Host "Setting up 'appsettings.Development.json' for $aiServiceType..."
|
||||
ConvertTo-Json $appsettingsOverrides | Out-File -Encoding utf8 $appsettingsOverridesFilePath
|
||||
|
||||
Write-Host "($appsettingsOverridesFilePath)"
|
||||
Write-Host "========"
|
||||
Get-Content $appsettingsOverridesFilePath | Write-Host
|
||||
Write-Host "========"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "##########################"
|
||||
Write-Host "# Frontend configuration #"
|
||||
Write-Host "##########################"
|
||||
|
||||
$envFilePath = Join-Path "$PSScriptRoot" '../webapp/.env'
|
||||
|
||||
Write-Host "Setting up '.env'..."
|
||||
Set-Content -Path $envFilePath -Value "REACT_APP_BACKEND_URI=https://localhost:40443/"
|
||||
Add-Content -Path $envFilePath -Value "REACT_APP_AAD_AUTHORITY=https://login.microsoftonline.com/$Tenant"
|
||||
Add-Content -Path $envFilePath -Value "REACT_APP_AAD_CLIENT_ID=$ClientId"
|
||||
Add-Content -Path $envFilePath -Value ""
|
||||
Add-Content -Path $envFilePath -Value "# Web Service API key (not required when running locally)"
|
||||
Add-Content -Path $envFilePath -Value "REACT_APP_SK_API_KEY="
|
||||
|
||||
Write-Host "($envFilePath)"
|
||||
Write-Host "========"
|
||||
Get-Content $envFilePath | Write-Host
|
||||
Write-Host "========"
|
||||
|
||||
Write-Host "Done!"
|
||||
@@ -1,154 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Configure user secrets, appsettings.Development.json, and .env for Copilot Chat.
|
||||
|
||||
set -e
|
||||
|
||||
# Defaults
|
||||
COMPLETION_MODEL="gpt-3.5-turbo"
|
||||
EMBEDDING_MODEL="text-embedding-ada-002"
|
||||
PLANNER_MODEL="gpt-3.5-turbo"
|
||||
TENANT_ID="common"
|
||||
|
||||
# Argument parsing
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--openai)
|
||||
OPENAI=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--azureopenai)
|
||||
AZURE_OPENAI=YES
|
||||
shift
|
||||
;;
|
||||
-e|--endpoint)
|
||||
ENDPOINT="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
-a|--apikey)
|
||||
API_KEY="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--completion)
|
||||
COMPLETION_MODEL="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--embedding)
|
||||
EMBEDDING_MODEL="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--planner)
|
||||
PLANNER_MODEL="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-c|--clientid)
|
||||
CLIENT_ID="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-t|--tenantid)
|
||||
TENANT_ID="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
-*|--*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") # save positional arg
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||
|
||||
SCRIPT_DIRECTORY="$(dirname $0)"
|
||||
|
||||
# Validate arguments
|
||||
if [ -z "$API_KEY" ]; then
|
||||
echo "Please specify an API key with -a or --apikey."; exit 1;
|
||||
fi
|
||||
if [ -z "$CLIENT_ID" ]; then
|
||||
echo "Please specify a client (application) ID with -c or --clientid."; exit 1;
|
||||
fi
|
||||
if [ "$AZURE_OPENAI" = "YES" ] && [ -z "$ENDPOINT" ]; then
|
||||
echo "When using --azureopenti, please specify an endpoint with -e or --endpoint."; exit 1;
|
||||
fi
|
||||
|
||||
echo "#########################"
|
||||
echo "# Backend configuration #"
|
||||
echo "#########################"
|
||||
|
||||
# Install dev certificate
|
||||
case "$OSTYPE" in
|
||||
darwin*)
|
||||
dotnet dev-certs https --trust
|
||||
if [ $? -ne 0 ]; then `exit` 1; fi ;;
|
||||
msys*)
|
||||
dotnet dev-certs https --trust
|
||||
if [ $? -ne 0 ]; then exit 1; fi ;;
|
||||
cygwin*)
|
||||
dotnet dev-certs https --trust
|
||||
if [ $? -ne 0 ]; then exit 1; fi ;;
|
||||
linux*)
|
||||
dotnet dev-certs https
|
||||
if [ $? -ne 0 ]; then exit 1; fi ;;
|
||||
esac
|
||||
|
||||
if [ "$OPENAI" = "YES" ]; then
|
||||
AI_SERVICE_TYPE="OpenAI"
|
||||
elif [ "$AZURE_OPENAI" = "YES" ]; then
|
||||
# Azure OpenAI has a different model name for gpt-3.5-turbo (no decimal).
|
||||
AI_SERVICE_TYPE="AzureOpenAI"
|
||||
COMPLETION_MODEL="${COMPLETION_MODEL/3.5/"35"}"
|
||||
EMBEDDING_MODEL="${EMBEDDING_MODEL/3.5/"35"}"
|
||||
PLANNER_MODEL="${PLANNER_MODEL/3.5/"35"}"
|
||||
else
|
||||
echo "Please specify either --openai or --azureopenai."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APPSETTINGS_JSON="{ \"AIService\": { \"Type\": \"${AI_SERVICE_TYPE}\", \"Endpoint\": \"${ENDPOINT}\", \"Models\": { \"Completion\": \"${COMPLETION_MODEL}\", \"Embedding\": \"${EMBEDDING_MODEL}\", \"Planner\": \"${PLANNER_MODEL}\" } } }"
|
||||
WEBAPI_PROJECT_PATH="${SCRIPT_DIRECTORY}/../webapi"
|
||||
APPSETTINGS_OVERRIDES_FILEPATH="${WEBAPI_PROJECT_PATH}/appsettings.Development.json"
|
||||
|
||||
echo "Setting 'AIService:Key' user secret for $AI_SERVICE_TYPE..."
|
||||
dotnet user-secrets set --project $WEBAPI_PROJECT_PATH AIService:Key $API_KEY
|
||||
if [ $? -ne 0 ]; then exit 1; fi
|
||||
|
||||
echo "Setting up 'appsettings.Development.json' for $AI_SERVICE_TYPE..."
|
||||
echo $APPSETTINGS_JSON > $APPSETTINGS_OVERRIDES_FILEPATH
|
||||
|
||||
echo "($APPSETTINGS_OVERRIDES_FILEPATH)"
|
||||
echo "========"
|
||||
cat $APPSETTINGS_OVERRIDES_FILEPATH
|
||||
echo "========"
|
||||
|
||||
echo ""
|
||||
echo "##########################"
|
||||
echo "# Frontend configuration #"
|
||||
echo "##########################"
|
||||
|
||||
ENV_FILEPATH="${SCRIPT_DIRECTORY}/../webapp/.env"
|
||||
|
||||
echo "Setting up '.env'..."
|
||||
echo "REACT_APP_BACKEND_URI=https://localhost:40443/" > $ENV_FILEPATH
|
||||
echo "REACT_APP_AAD_AUTHORITY=https://login.microsoftonline.com/$TENANT_ID" >> $ENV_FILEPATH
|
||||
echo "REACT_APP_AAD_CLIENT_ID=$CLIENT_ID" >> $ENV_FILEPATH
|
||||
echo "# Web Service API key (not required when running locally)" >> $ENV_FILEPATH
|
||||
echo "REACT_APP_SK_API_KEY=" >> $ENV_FILEPATH
|
||||
|
||||
echo "($ENV_FILEPATH)"
|
||||
echo "========"
|
||||
cat $ENV_FILEPATH
|
||||
echo "========"
|
||||
|
||||
echo "Done!"
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Installs the requirements for running Copilot Chat.
|
||||
|
||||
set -e
|
||||
|
||||
# Add Yarn's package repository to the system
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
# Add .NET's package repository to the system
|
||||
wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
|
||||
sudo dpkg -i packages-microsoft-prod.deb
|
||||
rm packages-microsoft-prod.deb
|
||||
|
||||
# Add NodeJS's package repository to the system
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
|
||||
# Install the requirements
|
||||
sudo apt update;
|
||||
sudo apt install yarn -y;
|
||||
sudo apt install dotnet-sdk-6.0 -y;
|
||||
sudo apt install nodejs -y;
|
||||
|
||||
echo ""
|
||||
echo "YARN $(yarn --version) installed."
|
||||
echo "NODEJS $(node --version) installed."
|
||||
echo "DOTNET $(dotnet --version) installed."
|
||||
@@ -1,33 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Installs the requirements for running Copilot Chat.
|
||||
#>
|
||||
|
||||
if ($IsLinux)
|
||||
{
|
||||
Write-Host "ERROR: This script is not supported for your operating system."
|
||||
exit 1;
|
||||
}
|
||||
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
[System.Net.ServicePointManager]::SecurityProtocol = 3072
|
||||
|
||||
# Install chocolatey if not already installed
|
||||
$ChocoInstalled = $false
|
||||
if (Get-Command choco.exe -ErrorAction SilentlyContinue) {
|
||||
$ChocoInstalled = $true
|
||||
}
|
||||
if (!$ChocoInstalled)
|
||||
{
|
||||
Write-Host "Installing Chocolatey..."
|
||||
Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')
|
||||
$env:PATH += ";%ALLUSERSPROFILE%\chocolatey\bin"
|
||||
refreshenv
|
||||
}
|
||||
|
||||
# Ensure required packages are installed
|
||||
$Packages = 'dotnet-6.0-sdk', 'nodejs', 'yarn'
|
||||
foreach ($PackageName in $Packages)
|
||||
{
|
||||
choco install $PackageName -y
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
# Copilot Chat Setup Scripts (local deployment)
|
||||
|
||||
## Before You Begin
|
||||
To run Copilot Chat, you will need the following:
|
||||
- *Application ID*
|
||||
- This is the Client ID (i.e., Application ID) associated with your Azure Active Directory (AAD) application registration, which you can find in the Azure portal.
|
||||
- *Azure OpenAI or OpenAI API Key*
|
||||
- This is your API key for Azure OpenAI or OpenAI
|
||||
|
||||
## 1. Configure your environment
|
||||
### Windows
|
||||
Open a PowerShell terminal as an administrator, navigate to this directory, and run the following command:
|
||||
```powershell
|
||||
./Install-Requirements.ps1
|
||||
```
|
||||
> This script uses the Chocolatey package manager install .NET 6.0 SDK, latest Node.js, and Yarn package manager.
|
||||
|
||||
### Ubuntu/Debian Linux
|
||||
Open a bash terminal as an administrator, navigate to this directory, and run the following command:
|
||||
```bash
|
||||
./Install-Requirements-UbuntuDebian.ps1
|
||||
```
|
||||
|
||||
### Other Linux/MacOS
|
||||
For all other operating systems, ensure NET 6.0 SDK (or newer), Node.js 14 (or newer), and Yarn classic ([v1.22.19](https://classic.yarnpkg.com/)) package manager are installed before proceeding.
|
||||
|
||||
## 2. Configure CopilotChat
|
||||
Configure the projects with your AI service and application registration information from above.
|
||||
|
||||
**Powershell**
|
||||
```powershell
|
||||
./Configure.ps1 -AzureOpenAI -Endpoint {AZURE_OPENAI_ENDPOINT} -ApiKey {AZURE_OPENAI_API_KEY} -ClientId {CLIENT_ID}
|
||||
```
|
||||
> For OpenAI, replace `-AzureOpenAI` with `-OpenAI` and omit `-Endpoint`.
|
||||
|
||||
**Bash**
|
||||
```bash
|
||||
./Configure.sh --azureopenai --endpoint {AZURE_OPENAI_ENDPOINT} --apikey {AZURE_OPENAI_API_KEY} --clientid {CLIENT_ID}
|
||||
```
|
||||
> For OpenAI, replace `--azureopenai` with `--openai` and omit `--endpoint`.
|
||||
|
||||
> **Note:** `Configure.ps1`/`Configure.sh` scripts also have parameters for setting additional options, such as AI models and Azure Active Directory tenant IDs.
|
||||
|
||||
## 3. Run Copilot Chat
|
||||
The `Start` script initializes and runs the WebApp (frontend) and WebApi (backend) for Copilot Chat on your local machine.
|
||||
|
||||
### PowerShell
|
||||
Open a PowerShell window, navigate to this directory, and run the following command:
|
||||
```powershell
|
||||
./Start.ps1
|
||||
```
|
||||
|
||||
### Bash
|
||||
Open a Bash window, navigate to this directory, and run the following commands:
|
||||
```bash
|
||||
# Ensure ./Start.sh is executable
|
||||
chmod +x Start.sh
|
||||
# Start CopilotChat
|
||||
./Start.sh
|
||||
```
|
||||
> **Note:** The first time you run this may take a few minutes for Yarn packages to install.
|
||||
> **Note:** This script starts `CopilotChatWebApi.exe` as a background process. Be sure to terminate it when you are finished.
|
||||
|
||||
# Troubleshooting
|
||||
## 1. "A fatal error occurred. The folder [/usr/share/dotnet/host/fxr] does not exist" when running dotnet commands on Linux.
|
||||
> From https://stackoverflow.com/questions/73753672/a-fatal-error-occurred-the-folder-usr-share-dotnet-host-fxr-does-not-exist
|
||||
|
||||
When .NET (Core) was first released for Linux, it was not yet available in the official Ubuntu repo. So instead, many of us added the Microsoft APT repo in order to install it. Now, the packages are part of the Ubuntu repo, and they are conflicting with the Microsoft packages. This error is a result of mixed packages.
|
||||
```bash
|
||||
# Remove all existing packages to get to a clean state:
|
||||
sudo apt remove --assume-yes dotnet*;
|
||||
sudo apt remove --assume-yes aspnetcore*;
|
||||
sudo apt remove --assume-yes netstandard*;
|
||||
# Set the Microsoft package provider priority
|
||||
echo -e "Package: *\nPin: origin \"packages.microsoft.com\"\nPin-Priority: 1001" | sudo tee /etc/apt/preferences.d/99microsoft-dotnet.pref;
|
||||
# Update and install dotnet
|
||||
sudo apt update;
|
||||
sudo apt install --assume-yes dotnet-sdk-6.0;
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds and runs the Copilot Chat backend.
|
||||
#>
|
||||
|
||||
Join-Path "$PSScriptRoot" '../webapi' | Set-Location
|
||||
dotnet build
|
||||
dotnet run
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Builds and runs the Copilot Chat backend.
|
||||
|
||||
set -e
|
||||
|
||||
ScriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ScriptDir/../WebApi"
|
||||
|
||||
# Build and run the backend API server
|
||||
dotnet build && dotnet run
|
||||
@@ -1,8 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds and runs the Copilot Chat frontend.
|
||||
#>
|
||||
|
||||
Join-Path "$PSScriptRoot" '../webapp' | Set-Location
|
||||
yarn install
|
||||
yarn start
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Initializes and runs the Copilot Chat frontend.
|
||||
|
||||
set -e
|
||||
|
||||
ScriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ScriptDir/../webapp"
|
||||
|
||||
# Build and run the frontend application
|
||||
yarn install && yarn start
|
||||
@@ -1,13 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds and runs both the backend and frontend for Copilot Chat.
|
||||
#>
|
||||
|
||||
$BackendScript = Join-Path "$PSScriptRoot" 'Start-Backend.ps1'
|
||||
$FrontendScript = Join-Path "$PSScriptRoot" 'Start-Frontend.ps1'
|
||||
|
||||
# Start backend (in new PS process)
|
||||
Start-Process pwsh -ArgumentList "-noexit", "-command $BackendScript"
|
||||
|
||||
# Start frontend (in current PS process)
|
||||
& $FrontendScript
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Initializes and runs both the backend and frontend for Copilot Chat.
|
||||
|
||||
set -e
|
||||
|
||||
ScriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ScriptDir"
|
||||
|
||||
# Start backend (in background)
|
||||
./Start-Backend.sh &
|
||||
|
||||
# Start frontend
|
||||
./Start-Frontend.sh
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace SemanticKernel.Service.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Class implementing API key authentication.
|
||||
/// </summary>
|
||||
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationSchemeOptions>
|
||||
{
|
||||
public const string AuthenticationScheme = "ApiKey";
|
||||
public const string ApiKeyHeaderName = "x-sk-api-key";
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public ApiKeyAuthenticationHandler(
|
||||
IOptionsMonitor<ApiKeyAuthenticationSchemeOptions> options,
|
||||
ILoggerFactory loggerFactory,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock) : base(options, loggerFactory, encoder, clock)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
this.Logger.LogInformation("Checking API key");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(this.Options.ApiKey))
|
||||
{
|
||||
const string ErrorMessage = "API key not configured on server";
|
||||
|
||||
this.Logger.LogError(ErrorMessage);
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Fail(ErrorMessage));
|
||||
}
|
||||
|
||||
if (!this.Request.Headers.TryGetValue(ApiKeyHeaderName, out StringValues apiKeyFromHeader))
|
||||
{
|
||||
const string WarningMessage = "No API key provided";
|
||||
|
||||
this.Logger.LogWarning(WarningMessage);
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Fail(WarningMessage));
|
||||
}
|
||||
|
||||
if (!string.Equals(apiKeyFromHeader, this.Options.ApiKey, StringComparison.Ordinal))
|
||||
{
|
||||
const string WarningMessage = "Incorrect API key";
|
||||
|
||||
this.Logger.LogWarning(WarningMessage);
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Fail(WarningMessage));
|
||||
}
|
||||
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(AuthenticationScheme));
|
||||
var ticket = new AuthenticationTicket(principal, this.Scheme.Name);
|
||||
|
||||
this.Logger.LogInformation("Request authorized by API key");
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace SemanticKernel.Service.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Options for API key authentication.
|
||||
/// </summary>
|
||||
public class ApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The API key against which to authenticate.
|
||||
/// </summary>
|
||||
public string? ApiKey { get; set; }
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace SemanticKernel.Service.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Class implementing "authentication" that lets all requests pass through.
|
||||
/// </summary>
|
||||
public class PassThroughAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
public const string AuthenticationScheme = "PassThrough";
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public PassThroughAuthenticationHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory loggerFactory,
|
||||
UrlEncoder encoder,
|
||||
ISystemClock clock) : base(options, loggerFactory, encoder, clock)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
this.Logger.LogInformation("Allowing request to pass through");
|
||||
|
||||
var principal = new ClaimsPrincipal(new ClaimsIdentity(AuthenticationScheme));
|
||||
var ticket = new AuthenticationTicket(principal, this.Scheme.Name);
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace SemanticKernel.Service;
|
||||
|
||||
internal static class ConfigExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Build the configuration for the service.
|
||||
/// </summary>
|
||||
public static IHostBuilder AddConfiguration(this IHostBuilder host)
|
||||
{
|
||||
string? environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||
|
||||
host.ConfigureAppConfiguration((builderContext, configBuilder) =>
|
||||
{
|
||||
configBuilder.AddJsonFile(
|
||||
path: "appsettings.json",
|
||||
optional: false,
|
||||
reloadOnChange: true);
|
||||
|
||||
configBuilder.AddJsonFile(
|
||||
path: $"appsettings.{environment}.json",
|
||||
optional: true,
|
||||
reloadOnChange: true);
|
||||
|
||||
configBuilder.AddEnvironmentVariables();
|
||||
|
||||
configBuilder.AddUserSecrets(
|
||||
assembly: Assembly.GetExecutingAssembly(),
|
||||
optional: true,
|
||||
reloadOnChange: true);
|
||||
|
||||
// For settings from Key Vault, see https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-8.0
|
||||
string? keyVaultUri = builderContext.Configuration["Service:KeyVault"];
|
||||
if (!string.IsNullOrWhiteSpace(keyVaultUri))
|
||||
{
|
||||
configBuilder.AddAzureKeyVault(
|
||||
new Uri(keyVaultUri),
|
||||
new DefaultAzureCredential());
|
||||
|
||||
// for more information on how to use DefaultAzureCredential, see https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet
|
||||
}
|
||||
});
|
||||
|
||||
return host;
|
||||
}
|
||||
}
|
||||
@@ -1,342 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Memory;
|
||||
using SemanticKernel.Service.CopilotChat.Extensions;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Controllers;
|
||||
|
||||
[ApiController]
|
||||
public class BotController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<BotController> _logger;
|
||||
private readonly IMemoryStore? _memoryStore;
|
||||
private readonly ISemanticTextMemory _semanticMemory;
|
||||
private readonly ChatSessionRepository _chatRepository;
|
||||
private readonly ChatMessageRepository _chatMessageRepository;
|
||||
private readonly ChatParticipantRepository _chatParticipantRepository;
|
||||
|
||||
private readonly BotSchemaOptions _botSchemaOptions;
|
||||
private readonly AIServiceOptions _embeddingOptions;
|
||||
private readonly DocumentMemoryOptions _documentMemoryOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The constructor of BotController.
|
||||
/// </summary>
|
||||
/// <param name="optionalIMemoryStore">Optional memory store.
|
||||
/// High level semantic memory implementations, such as Azure Cognitive Search, do not allow for providing embeddings when storing memories.
|
||||
/// We wrap the memory store in an optional memory store to allow controllers to pass dependency injection validation and potentially optimize
|
||||
/// for a lower-level memory implementation (e.g. Qdrant). Lower level memory implementations (i.e., IMemoryStore) allow for reusing embeddings,
|
||||
/// whereas high level memory implementation (i.e., ISemanticTextMemory) assume embeddings get recalculated on every write.
|
||||
/// </param>
|
||||
/// <param name="chatRepository">The chat session repository.</param>
|
||||
/// <param name="chatMessageRepository">The chat message repository.</param>
|
||||
/// <param name="chatParticipantRepository">The chat participant repository.</param>
|
||||
/// <param name="aiServiceOptions">The AI service options where we need the embedding settings from.</param>
|
||||
/// <param name="botSchemaOptions">The bot schema options.</param>
|
||||
/// <param name="documentMemoryOptions">The document memory options.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public BotController(
|
||||
OptionalIMemoryStore optionalIMemoryStore,
|
||||
ISemanticTextMemory semanticMemory,
|
||||
ChatSessionRepository chatRepository,
|
||||
ChatMessageRepository chatMessageRepository,
|
||||
ChatParticipantRepository chatParticipantRepository,
|
||||
IOptions<AIServiceOptions> aiServiceOptions,
|
||||
IOptions<BotSchemaOptions> botSchemaOptions,
|
||||
IOptions<DocumentMemoryOptions> documentMemoryOptions,
|
||||
ILogger<BotController> logger)
|
||||
{
|
||||
this._memoryStore = optionalIMemoryStore.MemoryStore;
|
||||
this._logger = logger;
|
||||
this._semanticMemory = semanticMemory;
|
||||
this._chatRepository = chatRepository;
|
||||
this._chatMessageRepository = chatMessageRepository;
|
||||
this._chatParticipantRepository = chatParticipantRepository;
|
||||
this._botSchemaOptions = botSchemaOptions.Value;
|
||||
this._embeddingOptions = aiServiceOptions.Value;
|
||||
this._documentMemoryOptions = documentMemoryOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload a bot.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The Semantic Kernel instance.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="bot">The bot object from the message body</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The HTTP action result with new chat session object.</returns>
|
||||
[Authorize]
|
||||
[HttpPost]
|
||||
[Route("bot/upload")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<ChatSession>> UploadAsync(
|
||||
[FromServices] IKernel kernel,
|
||||
[FromQuery] string userId,
|
||||
[FromBody] Bot bot,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: We should get userId from server context instead of from request for privacy/security reasons when support multiple users.
|
||||
this._logger.LogDebug("Received call to upload a bot");
|
||||
|
||||
if (!IsBotCompatible(
|
||||
externalBotSchema: bot.Schema,
|
||||
externalBotEmbeddingConfig: bot.EmbeddingConfigurations,
|
||||
embeddingOptions: this._embeddingOptions,
|
||||
botSchemaOptions: this._botSchemaOptions))
|
||||
{
|
||||
return this.BadRequest("Incompatible schema. " +
|
||||
$"The supported bot schema is {this._botSchemaOptions.Name}/{this._botSchemaOptions.Version} " +
|
||||
$"for the {this._embeddingOptions.Models.Embedding} model from {this._embeddingOptions.Type}. " +
|
||||
$"But the uploaded file is with schema {bot.Schema.Name}/{bot.Schema.Version} " +
|
||||
$"for the {bot.EmbeddingConfigurations.DeploymentOrModelId} model from {bot.EmbeddingConfigurations.AIService}.");
|
||||
}
|
||||
|
||||
string chatTitle = $"{bot.ChatTitle} - Clone";
|
||||
string chatId = string.Empty;
|
||||
ChatSession newChat;
|
||||
|
||||
// Upload chat history into chat repository and embeddings into memory.
|
||||
|
||||
// 1. Create a new chat and get the chat id.
|
||||
newChat = new ChatSession(chatTitle);
|
||||
await this._chatRepository.CreateAsync(newChat);
|
||||
await this._chatParticipantRepository.CreateAsync(new ChatParticipant(userId, newChat.Id));
|
||||
chatId = newChat.Id;
|
||||
|
||||
string oldChatId = bot.ChatHistory.First().ChatId;
|
||||
|
||||
// 2. Update the app's chat storage.
|
||||
foreach (var message in bot.ChatHistory)
|
||||
{
|
||||
var chatMessage = new ChatMessage(
|
||||
message.UserId,
|
||||
message.UserName,
|
||||
chatId,
|
||||
message.Content,
|
||||
message.Prompt,
|
||||
ChatMessage.AuthorRoles.Participant)
|
||||
{
|
||||
Timestamp = message.Timestamp
|
||||
};
|
||||
await this._chatMessageRepository.CreateAsync(chatMessage);
|
||||
}
|
||||
|
||||
// 3. Update the memory.
|
||||
await this.BulkUpsertMemoryRecordsAsync(oldChatId, chatId, bot.Embeddings, cancellationToken);
|
||||
|
||||
// TODO: Revert changes if any of the actions failed
|
||||
|
||||
return this.CreatedAtAction(
|
||||
nameof(ChatHistoryController.GetChatSessionByIdAsync),
|
||||
nameof(ChatHistoryController).Replace("Controller", "", StringComparison.OrdinalIgnoreCase),
|
||||
new { chatId },
|
||||
newChat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Download a bot.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The Semantic Kernel instance.</param>
|
||||
/// <param name="chatId">The chat id to be downloaded.</param>
|
||||
/// <returns>The serialized Bot object of the chat id.</returns>
|
||||
[Authorize]
|
||||
[HttpGet]
|
||||
[ActionName("DownloadAsync")]
|
||||
[Route("bot/download/{chatId:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<string>> DownloadAsync(
|
||||
[FromServices] IKernel kernel,
|
||||
Guid chatId)
|
||||
{
|
||||
this._logger.LogDebug("Received call to download a bot");
|
||||
var memory = await this.CreateBotAsync(kernel: kernel, chatId: chatId);
|
||||
|
||||
return JsonSerializer.Serialize(memory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if an external bot file is compatible with the application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the embeddings are not generated from the same model, the bot file is not compatible.
|
||||
/// </remarks>
|
||||
/// <param name="externalBotSchema">The external bot schema.</param>
|
||||
/// <param name="externalBotEmbeddingConfig">The external bot embedding configuration.</param>
|
||||
/// <param name="embeddingOptions">The embedding options.</param>
|
||||
/// <param name="botSchemaOptions">The bot schema options.</param>
|
||||
/// <returns>True if the bot file is compatible with the app; otherwise false.</returns>
|
||||
private static bool IsBotCompatible(
|
||||
BotSchemaOptions externalBotSchema,
|
||||
BotEmbeddingConfig externalBotEmbeddingConfig,
|
||||
AIServiceOptions embeddingOptions,
|
||||
BotSchemaOptions botSchemaOptions)
|
||||
{
|
||||
// The app can define what schema/version it supports before the community comes out with an open schema.
|
||||
return externalBotSchema.Name.Equals(botSchemaOptions.Name, StringComparison.OrdinalIgnoreCase)
|
||||
&& externalBotSchema.Version == botSchemaOptions.Version
|
||||
&& externalBotEmbeddingConfig.AIService == embeddingOptions.Type
|
||||
&& externalBotEmbeddingConfig.DeploymentOrModelId.Equals(embeddingOptions.Models.Embedding, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get memory from memory store and append the memory records to a given list.
|
||||
/// It will update the memory collection name in the new list if the newCollectionName is provided.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The Semantic Kernel instance.</param>
|
||||
/// <param name="collectionName">The current collection name. Used to query the memory storage.</param>
|
||||
/// <param name="embeddings">The embeddings list where we will append the fetched memory records.</param>
|
||||
/// <param name="newCollectionName">
|
||||
/// The new collection name when appends to the embeddings list. Will use the old collection name if not provided.
|
||||
/// </param>
|
||||
private static async Task GetMemoryRecordsAndAppendToEmbeddingsAsync(
|
||||
IKernel kernel,
|
||||
string collectionName,
|
||||
List<KeyValuePair<string, List<MemoryQueryResult>>> embeddings,
|
||||
string newCollectionName = "")
|
||||
{
|
||||
List<MemoryQueryResult> collectionMemoryRecords = await kernel.Memory.SearchAsync(
|
||||
collectionName,
|
||||
"abc", // dummy query since we don't care about relevance. An empty string will cause exception.
|
||||
limit: 999999999, // temp solution to get as much as record as a workaround.
|
||||
minRelevanceScore: -1, // no relevance required since the collection only has one entry
|
||||
withEmbeddings: true,
|
||||
cancellationToken: default
|
||||
).ToListAsync();
|
||||
|
||||
embeddings.Add(new KeyValuePair<string, List<MemoryQueryResult>>(
|
||||
string.IsNullOrEmpty(newCollectionName) ? collectionName : newCollectionName,
|
||||
collectionMemoryRecords));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepare the bot information of a given chat.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The semantic kernel object.</param>
|
||||
/// <param name="chatId">The chat id of the bot</param>
|
||||
/// <returns>A Bot object that represents the chat session.</returns>
|
||||
private async Task<Bot> CreateBotAsync(IKernel kernel, Guid chatId)
|
||||
{
|
||||
var chatIdString = chatId.ToString();
|
||||
var bot = new Bot
|
||||
{
|
||||
// get the bot schema version
|
||||
Schema = this._botSchemaOptions,
|
||||
|
||||
// get the embedding configuration
|
||||
EmbeddingConfigurations = new BotEmbeddingConfig
|
||||
{
|
||||
AIService = this._embeddingOptions.Type,
|
||||
DeploymentOrModelId = this._embeddingOptions.Models.Embedding
|
||||
}
|
||||
};
|
||||
|
||||
// get the chat title
|
||||
ChatSession chat = await this._chatRepository.FindByIdAsync(chatIdString);
|
||||
bot.ChatTitle = chat.Title;
|
||||
|
||||
// get the chat history
|
||||
bot.ChatHistory = await this.GetAllChatMessagesAsync(chatIdString);
|
||||
|
||||
// get the memory collections associated with this chat
|
||||
// TODO: filtering memory collections by name might be fragile.
|
||||
var chatCollections = (await kernel.Memory.GetCollectionsAsync())
|
||||
.Where(collection => collection.StartsWith(chatIdString, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
foreach (var collection in chatCollections)
|
||||
{
|
||||
await GetMemoryRecordsAndAppendToEmbeddingsAsync(kernel: kernel, collectionName: collection, embeddings: bot.Embeddings);
|
||||
}
|
||||
|
||||
// get the document memory collection names (global scope)
|
||||
await GetMemoryRecordsAndAppendToEmbeddingsAsync(
|
||||
kernel: kernel,
|
||||
collectionName: this._documentMemoryOptions.GlobalDocumentCollectionName,
|
||||
embeddings: bot.DocumentEmbeddings);
|
||||
|
||||
// get the document memory collection names (user scope)
|
||||
await GetMemoryRecordsAndAppendToEmbeddingsAsync(
|
||||
kernel: kernel,
|
||||
collectionName: this._documentMemoryOptions.ChatDocumentCollectionNamePrefix + chatIdString,
|
||||
embeddings: bot.DocumentEmbeddings);
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get chat messages of a given chat id.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The chat id</param>
|
||||
/// <returns>The list of chat messages in descending order of the timestamp</returns>
|
||||
private async Task<List<ChatMessage>> GetAllChatMessagesAsync(string chatId)
|
||||
{
|
||||
// TODO: We might want to set limitation on the number of messages that are pulled from the storage.
|
||||
return (await this._chatMessageRepository.FindByChatIdAsync(chatId))
|
||||
.OrderByDescending(m => m.Timestamp).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulk upsert memory records into memory store.
|
||||
/// </summary>
|
||||
/// <param name="oldChatId">The original chat id of the memory records.</param>
|
||||
/// <param name="chatId">The new chat id that will replace the original chat id.</param>
|
||||
/// <param name="embeddings">The list of embeddings of the chat id.</param>
|
||||
/// <returns>The function doesn't return anything.</returns>
|
||||
private async Task BulkUpsertMemoryRecordsAsync(string oldChatId, string chatId, List<KeyValuePair<string, List<MemoryQueryResult>>> embeddings, CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var collection in embeddings)
|
||||
{
|
||||
foreach (var record in collection.Value)
|
||||
{
|
||||
if (record != null && record.Embedding != null)
|
||||
{
|
||||
var newCollectionName = collection.Key.Replace(oldChatId, chatId, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (this._memoryStore == null)
|
||||
{
|
||||
await this._semanticMemory.SaveInformationAsync(
|
||||
collection: newCollectionName,
|
||||
text: record.Metadata.Text,
|
||||
id: record.Metadata.Id,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
MemoryRecord data = MemoryRecord.LocalRecord(
|
||||
id: record.Metadata.Id,
|
||||
text: record.Metadata.Text,
|
||||
embedding: record.Embedding.Value,
|
||||
description: null,
|
||||
additionalMetadata: null);
|
||||
|
||||
if (!(await this._memoryStore.DoesCollectionExistAsync(newCollectionName, default)))
|
||||
{
|
||||
await this._memoryStore.CreateCollectionAsync(newCollectionName, default);
|
||||
}
|
||||
|
||||
await this._memoryStore.UpsertAsync(newCollectionName, data, default);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Graph;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.AI;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
using Microsoft.SemanticKernel.SkillDefinition;
|
||||
using Microsoft.SemanticKernel.Skills.MsGraph;
|
||||
using Microsoft.SemanticKernel.Skills.MsGraph.Connectors;
|
||||
using Microsoft.SemanticKernel.Skills.MsGraph.Connectors.Client;
|
||||
using Microsoft.SemanticKernel.Skills.OpenAPI.Authentication;
|
||||
using Microsoft.SemanticKernel.Skills.OpenAPI.Extensions;
|
||||
using SemanticKernel.Service.CopilotChat.Hubs;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
using SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
|
||||
using SemanticKernel.Service.Diagnostics;
|
||||
using SemanticKernel.Service.Models;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller responsible for handling chat messages and responses.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
public class ChatController : ControllerBase, IDisposable
|
||||
{
|
||||
private readonly ILogger<ChatController> _logger;
|
||||
private readonly List<IDisposable> _disposables;
|
||||
private readonly ITelemetryService _telemetryService;
|
||||
private const string ChatSkillName = "ChatSkill";
|
||||
private const string ChatFunctionName = "Chat";
|
||||
private const string ReceiveResponseClientCall = "ReceiveResponse";
|
||||
private const string GeneratingResponseClientCall = "ReceiveBotTypingState";
|
||||
|
||||
public ChatController(ILogger<ChatController> logger, ITelemetryService telemetryService)
|
||||
{
|
||||
this._logger = logger;
|
||||
this._telemetryService = telemetryService;
|
||||
this._disposables = new List<IDisposable>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the chat skill to get a response from the bot.
|
||||
/// </summary>
|
||||
/// <param name="kernel">Semantic kernel obtained through dependency injection.</param>
|
||||
/// <param name="messageRelayHubContext">Message Hub that performs the real time relay service.</param>
|
||||
/// <param name="planner">Planner to use to create function sequences.</param>
|
||||
/// <param name="ask">Prompt along with its parameters.</param>
|
||||
/// <param name="openApiSkillsAuthHeaders">Authentication headers to connect to OpenAPI Skills.</param>
|
||||
/// <returns>Results containing the response from the model.</returns>
|
||||
[Authorize]
|
||||
[Route("chat")]
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ChatAsync(
|
||||
[FromServices] IKernel kernel,
|
||||
[FromServices] IHubContext<MessageRelayHub> messageRelayHubContext,
|
||||
[FromServices] CopilotChatPlanner planner,
|
||||
[FromBody] Ask ask,
|
||||
[FromHeader] OpenApiSkillsAuthHeaders openApiSkillsAuthHeaders)
|
||||
{
|
||||
this._logger.LogDebug("Chat request received.");
|
||||
|
||||
// Put ask's variables in the context we will use.
|
||||
var contextVariables = new ContextVariables(ask.Input);
|
||||
foreach (var input in ask.Variables)
|
||||
{
|
||||
contextVariables.Set(input.Key, input.Value);
|
||||
}
|
||||
|
||||
// Register plugins that have been enabled
|
||||
await this.RegisterPlannerSkillsAsync(planner, openApiSkillsAuthHeaders, contextVariables);
|
||||
|
||||
// Get the function to invoke
|
||||
ISKFunction? function = null;
|
||||
try
|
||||
{
|
||||
function = kernel.Skills.GetFunction(ChatSkillName, ChatFunctionName);
|
||||
}
|
||||
catch (KernelException ke)
|
||||
{
|
||||
this._logger.LogError("Failed to find {0}/{1} on server: {2}", ChatSkillName, ChatFunctionName, ke);
|
||||
|
||||
return this.NotFound($"Failed to find {ChatSkillName}/{ChatFunctionName} on server");
|
||||
}
|
||||
|
||||
// Broadcast bot typing state to all users
|
||||
if (ask.Variables.Where(v => v.Key == "chatId").Any())
|
||||
{
|
||||
var chatId = ask.Variables.Where(v => v.Key == "chatId").First().Value;
|
||||
await messageRelayHubContext.Clients.Group(chatId).SendAsync(GeneratingResponseClientCall, chatId, true);
|
||||
}
|
||||
|
||||
// Run the function.
|
||||
SKContext? result = null;
|
||||
try
|
||||
{
|
||||
result = await kernel.RunAsync(contextVariables, function!);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this._telemetryService.TrackSkillFunction(ChatSkillName, ChatFunctionName, (!result?.ErrorOccurred) ?? false);
|
||||
}
|
||||
|
||||
if (result.ErrorOccurred)
|
||||
{
|
||||
if (result.LastException is AIException aiException && aiException.Detail is not null)
|
||||
{
|
||||
return this.BadRequest(string.Concat(aiException.Message, " - Detail: " + aiException.Detail));
|
||||
}
|
||||
|
||||
return this.BadRequest(result.LastErrorDescription);
|
||||
}
|
||||
|
||||
AskResult chatSkillAskResult = new()
|
||||
{
|
||||
Value = result.Result,
|
||||
Variables = result.Variables.Select(
|
||||
v => new KeyValuePair<string, string>(v.Key, v.Value))
|
||||
};
|
||||
|
||||
// Broadcast AskResult to all users
|
||||
if (ask.Variables.Where(v => v.Key == "chatId").Any())
|
||||
{
|
||||
var chatId = ask.Variables.Where(v => v.Key == "chatId").First().Value;
|
||||
await messageRelayHubContext.Clients.Group(chatId).SendAsync(ReceiveResponseClientCall, chatSkillAskResult, chatId);
|
||||
await messageRelayHubContext.Clients.Group(chatId).SendAsync(GeneratingResponseClientCall, chatId, false);
|
||||
}
|
||||
|
||||
return this.Ok(chatSkillAskResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register skills with the planner's kernel.
|
||||
/// </summary>
|
||||
private async Task RegisterPlannerSkillsAsync(CopilotChatPlanner planner, OpenApiSkillsAuthHeaders openApiSkillsAuthHeaders, ContextVariables variables)
|
||||
{
|
||||
// Register authenticated skills with the planner's kernel only if the request includes an auth header for the skill.
|
||||
|
||||
// Klarna Shopping
|
||||
if (openApiSkillsAuthHeaders.KlarnaAuthentication != null)
|
||||
{
|
||||
// Register the Klarna shopping ChatGPT plugin with the planner's kernel. There is no authentication required for this plugin.
|
||||
await planner.Kernel.ImportChatGptPluginSkillFromUrlAsync("KlarnaShoppingSkill", new Uri("https://www.klarna.com/.well-known/ai-plugin.json"), new OpenApiSkillExecutionParameters());
|
||||
}
|
||||
|
||||
// GitHub
|
||||
if (!string.IsNullOrWhiteSpace(openApiSkillsAuthHeaders.GithubAuthentication))
|
||||
{
|
||||
this._logger.LogInformation("Enabling GitHub skill.");
|
||||
BearerAuthenticationProvider authenticationProvider = new(() => Task.FromResult(openApiSkillsAuthHeaders.GithubAuthentication));
|
||||
await planner.Kernel.ImportOpenApiSkillFromFileAsync(
|
||||
skillName: "GitHubSkill",
|
||||
filePath: Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CopilotChat", "Skills", "OpenApiSkills/GitHubSkill/openapi.json"),
|
||||
new OpenApiSkillExecutionParameters
|
||||
{
|
||||
AuthCallback = authenticationProvider.AuthenticateRequestAsync,
|
||||
});
|
||||
}
|
||||
|
||||
// Jira
|
||||
if (!string.IsNullOrWhiteSpace(openApiSkillsAuthHeaders.JiraAuthentication))
|
||||
{
|
||||
this._logger.LogInformation("Registering Jira Skill");
|
||||
var authenticationProvider = new BasicAuthenticationProvider(() => { return Task.FromResult(openApiSkillsAuthHeaders.JiraAuthentication); });
|
||||
var hasServerUrlOverride = variables.TryGetValue("jira-server-url", out string? serverUrlOverride);
|
||||
|
||||
await planner.Kernel.ImportOpenApiSkillFromFileAsync(
|
||||
skillName: "JiraSkill",
|
||||
filePath: Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "CopilotChat", "Skills", "OpenApiSkills/JiraSkill/openapi.json"),
|
||||
new OpenApiSkillExecutionParameters
|
||||
{
|
||||
AuthCallback = authenticationProvider.AuthenticateRequestAsync,
|
||||
ServerUrlOverride = hasServerUrlOverride ? new Uri(serverUrlOverride!) : null,
|
||||
});
|
||||
}
|
||||
|
||||
// Microsoft Graph
|
||||
if (!string.IsNullOrWhiteSpace(openApiSkillsAuthHeaders.GraphAuthentication))
|
||||
{
|
||||
this._logger.LogInformation("Enabling Microsoft Graph skill(s).");
|
||||
BearerAuthenticationProvider authenticationProvider = new(() => Task.FromResult(openApiSkillsAuthHeaders.GraphAuthentication));
|
||||
GraphServiceClient graphServiceClient = this.CreateGraphServiceClient(authenticationProvider.AuthenticateRequestAsync);
|
||||
|
||||
planner.Kernel.ImportSkill(new TaskListSkill(new MicrosoftToDoConnector(graphServiceClient)), "todo");
|
||||
planner.Kernel.ImportSkill(new CalendarSkill(new OutlookCalendarConnector(graphServiceClient)), "calendar");
|
||||
planner.Kernel.ImportSkill(new EmailSkill(new OutlookMailConnector(graphServiceClient)), "email");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Microsoft Graph service client.
|
||||
/// </summary>
|
||||
/// <param name="authenticateRequestAsyncDelegate">The delegate to authenticate the request.</param>
|
||||
private GraphServiceClient CreateGraphServiceClient(AuthenticateRequestAsyncDelegate authenticateRequestAsyncDelegate)
|
||||
{
|
||||
MsGraphClientLoggingHandler graphLoggingHandler = new(this._logger);
|
||||
this._disposables.Add(graphLoggingHandler);
|
||||
|
||||
IList<DelegatingHandler> graphMiddlewareHandlers =
|
||||
GraphClientFactory.CreateDefaultHandlers(new DelegateAuthenticationProvider(authenticateRequestAsyncDelegate));
|
||||
graphMiddlewareHandlers.Add(graphLoggingHandler);
|
||||
|
||||
HttpClient graphHttpClient = GraphClientFactory.Create(graphMiddlewareHandlers);
|
||||
this._disposables.Add(graphHttpClient);
|
||||
|
||||
GraphServiceClient graphServiceClient = new(graphHttpClient);
|
||||
return graphServiceClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of the object.
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach (IDisposable disposable in this._disposables)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
this.Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
-235
@@ -1,235 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.SemanticKernel;
|
||||
using SemanticKernel.Service.CopilotChat.Hubs;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for chat history.
|
||||
/// This controller is responsible for creating new chat sessions, retrieving chat sessions,
|
||||
/// retrieving chat messages, and editing chat sessions.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class ChatHistoryController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<ChatHistoryController> _logger;
|
||||
private readonly ChatSessionRepository _sessionRepository;
|
||||
private readonly ChatMessageRepository _messageRepository;
|
||||
private readonly ChatParticipantRepository _participantRepository;
|
||||
private readonly ChatMemorySourceRepository _sourceRepository;
|
||||
private readonly PromptsOptions _promptOptions;
|
||||
private const string ChatEditedClientCall = "ChatEdited";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatHistoryController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="sessionRepository">The chat session repository.</param>
|
||||
/// <param name="messageRepository">The chat message repository.</param>
|
||||
/// <param name="participantRepository">The chat participant repository.</param>
|
||||
/// <param name="sourceRepository">The chat memory resource repository.</param>
|
||||
/// <param name="promptsOptions">The prompts options.</param>
|
||||
public ChatHistoryController(
|
||||
ILogger<ChatHistoryController> logger,
|
||||
ChatSessionRepository sessionRepository,
|
||||
ChatMessageRepository messageRepository,
|
||||
ChatParticipantRepository participantRepository,
|
||||
ChatMemorySourceRepository sourceRepository,
|
||||
IOptions<PromptsOptions> promptsOptions)
|
||||
{
|
||||
this._logger = logger;
|
||||
this._sessionRepository = sessionRepository;
|
||||
this._messageRepository = messageRepository;
|
||||
this._participantRepository = participantRepository;
|
||||
this._sourceRepository = sourceRepository;
|
||||
this._promptOptions = promptsOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new chat session and populate the session with the initial bot message.
|
||||
/// </summary>
|
||||
/// <param name="chatParameter">Contains the title of the chat.</param>
|
||||
/// <returns>The HTTP action result.</returns>
|
||||
[HttpPost]
|
||||
[Route("chatSession/create")]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> CreateChatSessionAsync([FromBody] CreateChatParameters chatParameter)
|
||||
{
|
||||
if (chatParameter.UserId == null || chatParameter.Title == null)
|
||||
{
|
||||
return this.BadRequest("Chat session parameters cannot be null.");
|
||||
}
|
||||
|
||||
// Create a new chat session
|
||||
var newChat = new ChatSession(chatParameter.Title);
|
||||
await this._sessionRepository.CreateAsync(newChat);
|
||||
|
||||
var initialBotMessage = this._promptOptions.InitialBotMessage;
|
||||
// The initial bot message doesn't need a prompt.
|
||||
var chatMessage = ChatMessage.CreateBotResponseMessage(
|
||||
newChat.Id,
|
||||
initialBotMessage,
|
||||
string.Empty);
|
||||
await this._messageRepository.CreateAsync(chatMessage);
|
||||
|
||||
// Add the user to the chat session
|
||||
await this._participantRepository.CreateAsync(new ChatParticipant(chatParameter.UserId, newChat.Id));
|
||||
|
||||
this._logger.LogDebug("Created chat session with id {0}.", newChat.Id);
|
||||
return this.CreatedAtAction(nameof(this.GetChatSessionByIdAsync), new { chatId = newChat.Id }, newChat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a chat session by id.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The chat id.</param>
|
||||
[HttpGet]
|
||||
[ActionName("GetChatSessionByIdAsync")]
|
||||
[Route("chatSession/getChat/{chatId:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetChatSessionByIdAsync(Guid chatId)
|
||||
{
|
||||
ChatSession? chat = null;
|
||||
if (await this._sessionRepository.TryFindByIdAsync(chatId.ToString(), v => chat = v))
|
||||
{
|
||||
return this.Ok(chat);
|
||||
}
|
||||
|
||||
return this.NotFound($"No chat session found for chat id '{chatId}'.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all chat sessions associated with a user. Return an empty list if no chats are found.
|
||||
/// The regex pattern that is used to match the user id will match the following format:
|
||||
/// - 2 period separated groups of one or more hyphen-delimited alphanumeric strings.
|
||||
/// The pattern matches two GUIDs in canonical textual representation separated by a period.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <returns>A list of chat sessions. An empty list if the user is not in any chat session.</returns>
|
||||
[HttpGet]
|
||||
[Route("chatSession/getAllChats/{userId:regex(([[a-z0-9]]+-)+[[a-z0-9]]+\\.([[a-z0-9]]+-)+[[a-z0-9]]+)}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetAllChatSessionsAsync(string userId)
|
||||
{
|
||||
// Get all participants that belong to the user.
|
||||
// Then get all the chats from the list of participants.
|
||||
var chatParticipants = await this._participantRepository.FindByUserIdAsync(userId);
|
||||
|
||||
var chats = new List<ChatSession>();
|
||||
foreach (var chatParticipant in chatParticipants)
|
||||
{
|
||||
ChatSession? chat = null;
|
||||
if (await this._sessionRepository.TryFindByIdAsync(chatParticipant.ChatId, v => chat = v))
|
||||
{
|
||||
chats.Add(chat!);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogDebug(
|
||||
"Failed to find chat session with id {0} for participant {1}", chatParticipant.ChatId, chatParticipant.Id);
|
||||
return this.NotFound(
|
||||
$"Failed to find chat session with id {chatParticipant.ChatId} for participant {chatParticipant.Id}");
|
||||
}
|
||||
}
|
||||
|
||||
return this.Ok(chats);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all chat messages for a chat session.
|
||||
/// The list will be ordered with the first entry being the most recent message.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The chat id.</param>
|
||||
/// <param name="startIdx">The start index at which the first message will be returned.</param>
|
||||
/// <param name="count">The number of messages to return. -1 will return all messages starting from startIdx.</param>
|
||||
/// [Authorize]
|
||||
[HttpGet]
|
||||
[Route("chatSession/getChatMessages/{chatId:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetChatMessagesAsync(
|
||||
Guid chatId,
|
||||
[FromQuery] int startIdx = 0,
|
||||
[FromQuery] int count = -1)
|
||||
{
|
||||
// TODO: the code mixes strings and Guid without being explicit about the serialization format
|
||||
var chatMessages = await this._messageRepository.FindByChatIdAsync(chatId.ToString());
|
||||
if (!chatMessages.Any())
|
||||
{
|
||||
return this.NotFound($"No messages found for chat id '{chatId}'.");
|
||||
}
|
||||
|
||||
chatMessages = chatMessages.OrderByDescending(m => m.Timestamp).Skip(startIdx);
|
||||
if (count >= 0) { chatMessages = chatMessages.Take(count); }
|
||||
|
||||
return this.Ok(chatMessages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Edit a chat session.
|
||||
/// </summary>
|
||||
/// <param name="chatParameters">Object that contains the parameters to edit the chat.</param>
|
||||
[HttpPost]
|
||||
[Route("chatSession/edit")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> EditChatSessionAsync(
|
||||
[FromServices] IHubContext<MessageRelayHub> messageRelayHubContext,
|
||||
[FromBody] ChatSession chatParameters)
|
||||
{
|
||||
string chatId = chatParameters.Id;
|
||||
|
||||
ChatSession? chat = null;
|
||||
if (await this._sessionRepository.TryFindByIdAsync(chatId, v => chat = v))
|
||||
{
|
||||
chat!.Title = chatParameters.Title;
|
||||
await this._sessionRepository.UpsertAsync(chat);
|
||||
await messageRelayHubContext.Clients.Group(chatId).SendAsync(ChatEditedClientCall, chat);
|
||||
return this.Ok(chat);
|
||||
}
|
||||
|
||||
return this.NotFound($"No chat session found for chat id '{chatId}'.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service API to get a list of imported sources.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[Route("chatSession/{chatId:guid}/sources")]
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<IEnumerable<MemorySource>>> GetSourcesAsync(
|
||||
[FromServices] IKernel kernel,
|
||||
Guid chatId)
|
||||
{
|
||||
this._logger.LogInformation("Get imported sources of chat session {0}", chatId);
|
||||
|
||||
if (await this._sessionRepository.TryFindByIdAsync(chatId.ToString(), v => _ = v))
|
||||
{
|
||||
var sources = await this._sourceRepository.FindByChatIdAsync(chatId.ToString());
|
||||
return this.Ok(sources);
|
||||
}
|
||||
|
||||
return this.NotFound($"No chat session found for chat id '{chatId}'.");
|
||||
}
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SemanticKernel.Service.CopilotChat.Hubs;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for managing invitations and participants in a chat session.
|
||||
/// This controller is responsible for:
|
||||
/// 1. Creating invitation links.
|
||||
/// 2. Accepting/rejecting invitation links.
|
||||
/// 3. Managing participants in a chat session.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class ChatParticipantController : ControllerBase
|
||||
{
|
||||
private const string UserJoinedClientCall = "UserJoined";
|
||||
private readonly ILogger<ChatParticipantController> _logger;
|
||||
private readonly ChatParticipantRepository _chatParticipantRepository;
|
||||
private readonly ChatSessionRepository _chatSessionRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatParticipantController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="chatParticipantRepository">The chat participant repository.</param>
|
||||
/// <param name="chatSessionRepository">The chat session repository.</param>
|
||||
public ChatParticipantController(
|
||||
ILogger<ChatParticipantController> logger,
|
||||
ChatParticipantRepository chatParticipantRepository,
|
||||
ChatSessionRepository chatSessionRepository)
|
||||
{
|
||||
this._logger = logger;
|
||||
this._chatParticipantRepository = chatParticipantRepository;
|
||||
this._chatSessionRepository = chatSessionRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Join a use to a chat session given a chat id and a user id.
|
||||
/// </summary>
|
||||
/// <param name="messageRelayHubContext">Message Hub that performs the real time relay service.</param>
|
||||
/// <param name="chatParticipantParam">Contains the user id and chat id.</param>
|
||||
[HttpPost]
|
||||
[Route("chatParticipant/join")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> JoinChatAsync(
|
||||
[FromServices] IHubContext<MessageRelayHub> messageRelayHubContext,
|
||||
[FromBody] ChatParticipant chatParticipantParam)
|
||||
{
|
||||
string userId = chatParticipantParam.UserId;
|
||||
string chatId = chatParticipantParam.ChatId;
|
||||
|
||||
// Make sure the chat session exists.
|
||||
if (!await this._chatSessionRepository.TryFindByIdAsync(chatId, v => _ = v))
|
||||
{
|
||||
return this.BadRequest("Chat session does not exist.");
|
||||
}
|
||||
|
||||
// Make sure the user is not already in the chat session.
|
||||
if (await this._chatParticipantRepository.IsUserInChatAsync(userId, chatId))
|
||||
{
|
||||
return this.BadRequest("User is already in the chat session.");
|
||||
}
|
||||
|
||||
var chatParticipant = new ChatParticipant(userId, chatId);
|
||||
await this._chatParticipantRepository.CreateAsync(chatParticipant);
|
||||
|
||||
// Broadcast the user joined event to all the connected clients.
|
||||
// Note that the client who initiated the request may not have joined the group.
|
||||
await messageRelayHubContext.Clients.Group(chatId).SendAsync(UserJoinedClientCall, chatId, userId);
|
||||
|
||||
return this.Ok(chatParticipant);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of chat participants that have the same chat id.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The Id of the chat to get all the participants from.</param>
|
||||
[HttpGet]
|
||||
[Route("chatParticipant/getAllParticipants/{chatId:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetAllParticipantsAsync(Guid chatId)
|
||||
{
|
||||
// Make sure the chat session exists.
|
||||
if (!await this._chatSessionRepository.TryFindByIdAsync(chatId.ToString(), v => _ = v))
|
||||
{
|
||||
return this.NotFound("Chat session does not exist.");
|
||||
}
|
||||
|
||||
var chatParticipants = await this._chatParticipantRepository.FindByChatIdAsync(chatId.ToString());
|
||||
return this.Ok(chatParticipants);
|
||||
}
|
||||
}
|
||||
-562
@@ -1,562 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Text;
|
||||
using SemanticKernel.Service.CopilotChat.Hubs;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
using SemanticKernel.Service.Services;
|
||||
using Tesseract;
|
||||
using UglyToad.PdfPig;
|
||||
using UglyToad.PdfPig.DocumentLayoutAnalysis.TextExtractor;
|
||||
using static SemanticKernel.Service.CopilotChat.Models.MemorySource;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for importing documents.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
public class DocumentImportController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Supported file types for import.
|
||||
/// </summary>
|
||||
private enum SupportedFileType
|
||||
{
|
||||
/// <summary>
|
||||
/// .txt
|
||||
/// </summary>
|
||||
Txt,
|
||||
|
||||
/// <summary>
|
||||
/// .pdf
|
||||
/// </summary>
|
||||
Pdf,
|
||||
|
||||
/// <summary>
|
||||
/// .jpg
|
||||
/// </summary>
|
||||
Jpg,
|
||||
|
||||
/// <summary>
|
||||
/// .png
|
||||
/// </summary>
|
||||
Png,
|
||||
|
||||
/// <summary>
|
||||
/// .tif or .tiff
|
||||
/// </summary>
|
||||
Tiff
|
||||
};
|
||||
|
||||
private readonly ILogger<DocumentImportController> _logger;
|
||||
private readonly DocumentMemoryOptions _options;
|
||||
private readonly ChatSessionRepository _sessionRepository;
|
||||
private readonly ChatMemorySourceRepository _sourceRepository;
|
||||
private readonly ChatMessageRepository _messageRepository;
|
||||
private readonly ChatParticipantRepository _participantRepository;
|
||||
private const string GlobalDocumentUploadedClientCall = "GlobalDocumentUploaded";
|
||||
private const string ChatDocumentUploadedClientCall = "ChatDocumentUploaded";
|
||||
private readonly ITesseractEngine _tesseractEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DocumentImportController"/> class.
|
||||
/// </summary>
|
||||
public DocumentImportController(
|
||||
ILogger<DocumentImportController> logger,
|
||||
IOptions<DocumentMemoryOptions> documentMemoryOptions,
|
||||
ChatSessionRepository sessionRepository,
|
||||
ChatMemorySourceRepository sourceRepository,
|
||||
ChatMessageRepository messageRepository,
|
||||
ChatParticipantRepository participantRepository,
|
||||
ITesseractEngine tesseractEngine)
|
||||
{
|
||||
this._logger = logger;
|
||||
this._options = documentMemoryOptions.Value;
|
||||
this._sessionRepository = sessionRepository;
|
||||
this._sourceRepository = sourceRepository;
|
||||
this._messageRepository = messageRepository;
|
||||
this._participantRepository = participantRepository;
|
||||
this._tesseractEngine = tesseractEngine;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service API for importing a document.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[Route("importDocuments")]
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> ImportDocumentsAsync(
|
||||
[FromServices] IKernel kernel,
|
||||
[FromServices] IHubContext<MessageRelayHub> messageRelayHubContext,
|
||||
[FromForm] DocumentImportForm documentImportForm)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this.ValidateDocumentImportFormAsync(documentImportForm);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return this.BadRequest(ex.Message);
|
||||
}
|
||||
|
||||
this._logger.LogInformation("Importing {0} document(s)...", documentImportForm.FormFiles.Count());
|
||||
|
||||
// TODO: Perform the import in parallel.
|
||||
DocumentMessageContent documentMessageContent = new();
|
||||
IEnumerable<ImportResult> importResults = new List<ImportResult>();
|
||||
foreach (var formFile in documentImportForm.FormFiles)
|
||||
{
|
||||
var importResult = await this.ImportDocumentHelperAsync(kernel, formFile, documentImportForm);
|
||||
documentMessageContent.AddDocument(
|
||||
formFile.FileName,
|
||||
this.GetReadableByteString(formFile.Length),
|
||||
importResult.IsSuccessful);
|
||||
importResults = importResults.Append(importResult);
|
||||
}
|
||||
|
||||
// Broadcast the document uploaded event to other users.
|
||||
if (documentImportForm.DocumentScope == DocumentImportForm.DocumentScopes.Chat)
|
||||
{
|
||||
var chatMessage = await this.TryCreateDocumentUploadMessage(
|
||||
documentMessageContent,
|
||||
documentImportForm);
|
||||
if (chatMessage == null)
|
||||
{
|
||||
foreach (var importResult in importResults)
|
||||
{
|
||||
await this.RemoveMemoriesAsync(kernel, importResult);
|
||||
}
|
||||
return this.BadRequest("Failed to create chat message. All documents are removed.");
|
||||
}
|
||||
|
||||
var chatId = documentImportForm.ChatId.ToString();
|
||||
await messageRelayHubContext.Clients.Group(chatId)
|
||||
.SendAsync(ChatDocumentUploadedClientCall, chatMessage, chatId);
|
||||
|
||||
return this.Ok(chatMessage);
|
||||
}
|
||||
|
||||
await messageRelayHubContext.Clients.All.SendAsync(
|
||||
GlobalDocumentUploadedClientCall,
|
||||
documentMessageContent.ToFormattedStringNamesOnly(),
|
||||
documentImportForm.UserName
|
||||
);
|
||||
|
||||
return this.Ok("Documents imported successfully to global scope.");
|
||||
}
|
||||
|
||||
#region Private
|
||||
|
||||
/// <summary>
|
||||
/// A class to store a document import results.
|
||||
/// </summary>
|
||||
private sealed class ImportResult
|
||||
{
|
||||
/// <summary>
|
||||
/// A boolean indicating whether the import is successful.
|
||||
/// </summary>
|
||||
public bool IsSuccessful => this.Keys.Any();
|
||||
|
||||
/// <summary>
|
||||
/// The name of the collection that the document is inserted to.
|
||||
/// </summary>
|
||||
public string CollectionName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The keys of the inserted document chunks.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Keys { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of the <see cref="ImportResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="collectionName">The name of the collection that the document is inserted to.</param>
|
||||
public ImportResult(string collectionName)
|
||||
{
|
||||
this.CollectionName = collectionName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of the <see cref="ImportResult"/> class representing a failed import.
|
||||
/// </summary>
|
||||
public static ImportResult Fail() => new(string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Add a key to the list of keys.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to be added.</param>
|
||||
public void AddKey(string key)
|
||||
{
|
||||
this.Keys = this.Keys.Append(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the document import form.
|
||||
/// </summary>
|
||||
/// <param name="documentImportForm">The document import form.</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException">Throws ArgumentException if validation fails.</exception>
|
||||
private async Task ValidateDocumentImportFormAsync(DocumentImportForm documentImportForm)
|
||||
{
|
||||
// Make sure the user has access to the chat session if the document is uploaded to a chat session.
|
||||
if (documentImportForm.DocumentScope == DocumentImportForm.DocumentScopes.Chat
|
||||
&& !(await this.UserHasAccessToChatAsync(documentImportForm.UserId, documentImportForm.ChatId)))
|
||||
{
|
||||
throw new ArgumentException("User does not have access to the chat session.");
|
||||
}
|
||||
|
||||
var formFiles = documentImportForm.FormFiles;
|
||||
|
||||
if (!formFiles.Any())
|
||||
{
|
||||
throw new ArgumentException("No files were uploaded.");
|
||||
}
|
||||
else if (formFiles.Count() > this._options.FileCountLimit)
|
||||
{
|
||||
throw new ArgumentException($"Too many files uploaded. Max file count is {this._options.FileCountLimit}.");
|
||||
}
|
||||
|
||||
// Loop through the uploaded files and validate them before importing.
|
||||
foreach (var formFile in formFiles)
|
||||
{
|
||||
if (formFile.Length == 0)
|
||||
{
|
||||
throw new ArgumentException($"File {formFile.FileName} is empty.");
|
||||
}
|
||||
|
||||
if (formFile.Length > this._options.FileSizeLimit)
|
||||
{
|
||||
throw new ArgumentException($"File {formFile.FileName} size exceeds the limit.");
|
||||
}
|
||||
|
||||
// Make sure the file type is supported.
|
||||
var fileType = this.GetFileType(Path.GetFileName(formFile.FileName));
|
||||
switch (fileType)
|
||||
{
|
||||
case SupportedFileType.Txt:
|
||||
case SupportedFileType.Pdf:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported file type: {fileType}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import a single document.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The kernel.</param>
|
||||
/// <param name="formFile">The form file.</param>
|
||||
/// <param name="documentImportForm">The document import form.</param>
|
||||
/// <returns>Import result.</returns>
|
||||
private async Task<ImportResult> ImportDocumentHelperAsync(IKernel kernel, IFormFile formFile, DocumentImportForm documentImportForm)
|
||||
{
|
||||
var fileType = this.GetFileType(Path.GetFileName(formFile.FileName));
|
||||
var documentContent = string.Empty;
|
||||
switch (fileType)
|
||||
{
|
||||
case SupportedFileType.Txt:
|
||||
documentContent = await this.ReadTxtFileAsync(formFile);
|
||||
break;
|
||||
case SupportedFileType.Pdf:
|
||||
documentContent = this.ReadPdfFile(formFile);
|
||||
break;
|
||||
case SupportedFileType.Jpg:
|
||||
case SupportedFileType.Png:
|
||||
case SupportedFileType.Tiff:
|
||||
{
|
||||
documentContent = await this.ReadTextFromImageFileAsync(formFile);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// This should never happen. Validation should have already caught this.
|
||||
return ImportResult.Fail();
|
||||
}
|
||||
|
||||
this._logger.LogInformation("Importing document {0}", formFile.FileName);
|
||||
|
||||
// Create memory source
|
||||
var memorySource = await this.TryCreateAndUpsertMemorySourceAsync(formFile, documentImportForm);
|
||||
if (memorySource == null)
|
||||
{
|
||||
return ImportResult.Fail();
|
||||
}
|
||||
|
||||
// Parse document content to memory
|
||||
ImportResult importResult = ImportResult.Fail();
|
||||
try
|
||||
{
|
||||
importResult = await this.ParseDocumentContentToMemoryAsync(
|
||||
kernel,
|
||||
formFile.FileName,
|
||||
documentContent,
|
||||
documentImportForm,
|
||||
memorySource.Id
|
||||
);
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsCriticalException())
|
||||
{
|
||||
await this._sourceRepository.DeleteAsync(memorySource);
|
||||
await this.RemoveMemoriesAsync(kernel, importResult);
|
||||
return ImportResult.Fail();
|
||||
}
|
||||
|
||||
return importResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to create and upsert a memory source.
|
||||
/// </summary>
|
||||
/// <param name="formFile">The file to be uploaded</param>
|
||||
/// <param name="documentImportForm">The document upload form that contains additional necessary info</param>
|
||||
/// <returns>A MemorySource object if successful, null otherwise</returns>
|
||||
private async Task<MemorySource?> TryCreateAndUpsertMemorySourceAsync(
|
||||
IFormFile formFile,
|
||||
DocumentImportForm documentImportForm)
|
||||
{
|
||||
var chatId = documentImportForm.ChatId.ToString();
|
||||
var userId = documentImportForm.UserId;
|
||||
var memorySource = new MemorySource(
|
||||
chatId,
|
||||
formFile.FileName,
|
||||
userId,
|
||||
MemorySourceType.File,
|
||||
formFile.Length,
|
||||
null);
|
||||
|
||||
try
|
||||
{
|
||||
await this._sourceRepository.UpsertAsync(memorySource);
|
||||
return memorySource;
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentOutOfRangeException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to create a chat message that represents document upload.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The chat id</param>
|
||||
/// <param name="userName">The user id</param>
|
||||
/// <param name="documentMessageContent">The document message content</param>
|
||||
/// <param name="documentImportForm">The document upload form that contains additional necessary info</param>
|
||||
/// <returns>A ChatMessage object if successful, null otherwise</returns>
|
||||
private async Task<ChatMessage?> TryCreateDocumentUploadMessage(
|
||||
DocumentMessageContent documentMessageContent,
|
||||
DocumentImportForm documentImportForm)
|
||||
{
|
||||
var chatId = documentImportForm.ChatId.ToString();
|
||||
var userId = documentImportForm.UserId;
|
||||
var userName = documentImportForm.UserName;
|
||||
|
||||
var chatMessage = ChatMessage.CreateDocumentMessage(
|
||||
userId,
|
||||
userName,
|
||||
chatId,
|
||||
documentMessageContent
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
await this._messageRepository.CreateAsync(chatMessage);
|
||||
return chatMessage;
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentOutOfRangeException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a `long` byte count to a human-readable string.
|
||||
/// </summary>
|
||||
/// <param name="bytes">Byte count</param>
|
||||
/// <returns>Human-readable string of bytes</returns>
|
||||
private string GetReadableByteString(long bytes)
|
||||
{
|
||||
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
|
||||
int i;
|
||||
double dblsBytes = bytes;
|
||||
for (i = 0; i < sizes.Length && bytes >= 1024; i++, bytes /= 1024)
|
||||
{
|
||||
dblsBytes = bytes / 1024.0;
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0:0.#}{1}", dblsBytes, sizes[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the file type from the file extension.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Name of the file.</param>
|
||||
/// <returns>A SupportedFileType.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
private SupportedFileType GetFileType(string fileName)
|
||||
{
|
||||
string extension = Path.GetExtension(fileName);
|
||||
return extension switch
|
||||
{
|
||||
".txt" => SupportedFileType.Txt,
|
||||
".pdf" => SupportedFileType.Pdf,
|
||||
".jpg" => SupportedFileType.Jpg,
|
||||
".jpeg" => SupportedFileType.Jpg,
|
||||
".png" => SupportedFileType.Png,
|
||||
".tif" => SupportedFileType.Tiff,
|
||||
".tiff" => SupportedFileType.Tiff,
|
||||
_ => throw new ArgumentOutOfRangeException($"Unsupported file type: {extension}"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the text content from an image file.
|
||||
/// </summary>
|
||||
/// <param name="file">An IFormFile object.</param>
|
||||
/// <returns>A string of the content of the file.</returns>
|
||||
private async Task<string> ReadTextFromImageFileAsync(IFormFile file)
|
||||
{
|
||||
await using (var ms = new MemoryStream())
|
||||
{
|
||||
await file.CopyToAsync(ms);
|
||||
var fileBytes = ms.ToArray();
|
||||
await using var imgStream = new MemoryStream(fileBytes);
|
||||
|
||||
using var img = Pix.LoadFromMemory(imgStream.ToArray());
|
||||
|
||||
using var page = this._tesseractEngine.Process(img);
|
||||
return page.GetText();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the content of a text file.
|
||||
/// </summary>
|
||||
/// <param name="file">An IFormFile object.</param>
|
||||
/// <returns>A string of the content of the file.</returns>
|
||||
private async Task<string> ReadTxtFileAsync(IFormFile file)
|
||||
{
|
||||
using var streamReader = new StreamReader(file.OpenReadStream());
|
||||
return await streamReader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the content of a PDF file, ignoring images.
|
||||
/// </summary>
|
||||
/// <param name="file">An IFormFile object.</param>
|
||||
/// <returns>A string of the content of the file.</returns>
|
||||
private string ReadPdfFile(IFormFile file)
|
||||
{
|
||||
var fileContent = string.Empty;
|
||||
|
||||
using var pdfDocument = PdfDocument.Open(file.OpenReadStream());
|
||||
foreach (var page in pdfDocument.GetPages())
|
||||
{
|
||||
var text = ContentOrderTextExtractor.GetText(page);
|
||||
fileContent += text;
|
||||
}
|
||||
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse the content of the document to memory.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The kernel instance from the service</param>
|
||||
/// <param name="documentName">The name of the uploaded document</param>
|
||||
/// <param name="content">The file content read from the uploaded document</param>
|
||||
/// <param name="documentImportForm">The document upload form that contains additional necessary info</param>
|
||||
/// <param name="memorySourceId">The ID of the MemorySource that the document content is linked to</param>
|
||||
private async Task<ImportResult> ParseDocumentContentToMemoryAsync(
|
||||
IKernel kernel,
|
||||
string documentName,
|
||||
string content,
|
||||
DocumentImportForm documentImportForm,
|
||||
string memorySourceId)
|
||||
{
|
||||
var targetCollectionName = documentImportForm.DocumentScope == DocumentImportForm.DocumentScopes.Global
|
||||
? this._options.GlobalDocumentCollectionName
|
||||
: this._options.ChatDocumentCollectionNamePrefix + documentImportForm.ChatId;
|
||||
var importResult = new ImportResult(targetCollectionName);
|
||||
|
||||
// Split the document into lines of text and then combine them into paragraphs.
|
||||
// Note that this is only one of many strategies to chunk documents. Feel free to experiment with other strategies.
|
||||
var lines = TextChunker.SplitPlainTextLines(content, this._options.DocumentLineSplitMaxTokens);
|
||||
var paragraphs = TextChunker.SplitPlainTextParagraphs(lines, this._options.DocumentParagraphSplitMaxLines);
|
||||
|
||||
// TODO: Perform the save in parallel.
|
||||
for (var i = 0; i < paragraphs.Count; i++)
|
||||
{
|
||||
var paragraph = paragraphs[i];
|
||||
var key = $"{memorySourceId}-{i}";
|
||||
await kernel.Memory.SaveInformationAsync(
|
||||
collection: targetCollectionName,
|
||||
text: paragraph,
|
||||
id: key,
|
||||
description: $"Document: {documentName}");
|
||||
importResult.AddKey(key);
|
||||
}
|
||||
|
||||
this._logger.LogInformation(
|
||||
"Parsed {0} paragraphs from local file {1}",
|
||||
paragraphs.Count,
|
||||
documentName
|
||||
);
|
||||
|
||||
return importResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the user has access to the chat session.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user ID.</param>
|
||||
/// <param name="chatId">The chat session ID.</param>
|
||||
/// <returns>A boolean indicating whether the user has access to the chat session.</returns>
|
||||
private async Task<bool> UserHasAccessToChatAsync(string userId, Guid chatId)
|
||||
{
|
||||
return await this._participantRepository.IsUserInChatAsync(userId, chatId.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the memories that were created during the import process if subsequent steps fail.
|
||||
/// </summary>
|
||||
/// <param name="kernel">The kernel instance from the service</param>
|
||||
/// <param name="importResult">The import result that contains the keys of the memories to be removed</param>
|
||||
/// <returns></returns>
|
||||
private async Task RemoveMemoriesAsync(IKernel kernel, ImportResult importResult)
|
||||
{
|
||||
foreach (var key in importResult.Keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
await kernel.Memory.RemoveAsync(importResult.CollectionName, key);
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsCriticalException())
|
||||
{
|
||||
this._logger.LogError(ex, "Failed to remove memory {0} from collection {1}. Skipped.", key, importResult.CollectionName);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
public class SpeechTokenController : ControllerBase
|
||||
{
|
||||
private sealed class TokenResult
|
||||
{
|
||||
public string? Token { get; set; }
|
||||
public HttpStatusCode? ResponseCode { get; set; }
|
||||
}
|
||||
|
||||
private readonly ILogger<SpeechTokenController> _logger;
|
||||
private readonly AzureSpeechOptions _options;
|
||||
|
||||
public SpeechTokenController(IOptions<AzureSpeechOptions> options, ILogger<SpeechTokenController> logger)
|
||||
{
|
||||
this._logger = logger;
|
||||
this._options = options.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an authorization token and region
|
||||
/// </summary>
|
||||
[Route("speechToken")]
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SpeechTokenResponse>> GetAsync()
|
||||
{
|
||||
// Azure Speech token support is optional. If the configuration is missing or incomplete, return an unsuccessful token response.
|
||||
if (string.IsNullOrWhiteSpace(this._options.Region) ||
|
||||
string.IsNullOrWhiteSpace(this._options.Key))
|
||||
{
|
||||
return new SpeechTokenResponse { IsSuccess = false };
|
||||
}
|
||||
|
||||
string fetchTokenUri = "https://" + this._options.Region + ".api.cognitive.microsoft.com/sts/v1.0/issueToken";
|
||||
|
||||
TokenResult tokenResult = await this.FetchTokenAsync(fetchTokenUri, this._options.Key);
|
||||
var isSuccess = tokenResult.ResponseCode != HttpStatusCode.NotFound;
|
||||
return new SpeechTokenResponse { Token = tokenResult.Token, Region = this._options.Region, IsSuccess = isSuccess };
|
||||
}
|
||||
|
||||
private async Task<TokenResult> FetchTokenAsync(string fetchUri, string subscriptionKey)
|
||||
{
|
||||
// TODO: get the HttpClient from the DI container
|
||||
using var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey);
|
||||
UriBuilder uriBuilder = new(fetchUri);
|
||||
|
||||
var result = await client.PostAsync(uriBuilder.Uri, null);
|
||||
if (result.IsSuccessStatusCode)
|
||||
{
|
||||
var response = result.EnsureSuccessStatusCode();
|
||||
this._logger.LogDebug("Token Uri: {0}", uriBuilder.Uri.AbsoluteUri);
|
||||
string token = await result.Content.ReadAsStringAsync();
|
||||
return new TokenResult { Token = token, ResponseCode = response.StatusCode };
|
||||
}
|
||||
|
||||
return new TokenResult { Token = "", ResponseCode = HttpStatusCode.NotFound };
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for enabling async LINQ operations on IAsyncEnumerable sequence.
|
||||
/// </summary>
|
||||
public static class IAsyncEnumerableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a List<T> from an IAsyncEnumerable<T> by enumerating it asynchronously.
|
||||
/// </summary>
|
||||
internal static async Task<List<T>> ToListAsync<T>(this IAsyncEnumerable<T> source)
|
||||
{
|
||||
var result = new List<T>();
|
||||
await foreach (var item in source.ConfigureAwait(false))
|
||||
{
|
||||
result.Add(item);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.SemanticKernel;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
using SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering Copilot Chat components to Semantic Kernel.
|
||||
/// </summary>
|
||||
public static class CopilotChatSemanticKernelExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add Planner services
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCopilotChatPlannerServices(this IServiceCollection services)
|
||||
{
|
||||
IOptions<PlannerOptions>? plannerOptions = services.BuildServiceProvider().GetService<IOptions<PlannerOptions>>();
|
||||
services.AddScoped<CopilotChatPlanner>(sp =>
|
||||
{
|
||||
IKernel plannerKernel = Kernel.Builder
|
||||
.WithLogger(sp.GetRequiredService<ILogger<IKernel>>())
|
||||
// TODO verify planner has AI service configured
|
||||
.WithPlannerBackend(sp.GetRequiredService<IOptions<AIServiceOptions>>().Value)
|
||||
.Build();
|
||||
return new CopilotChatPlanner(plannerKernel, plannerOptions?.Value);
|
||||
});
|
||||
|
||||
// Register Planner skills (AI plugins) here.
|
||||
// TODO: Move planner skill registration from ChatController to here.
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register the Copilot chat skills with the kernel.
|
||||
/// </summary>
|
||||
public static IKernel RegisterCopilotChatSkills(this IKernel kernel, IServiceProvider sp)
|
||||
{
|
||||
// Chat skill
|
||||
kernel.ImportSkill(new ChatSkill(
|
||||
kernel: kernel,
|
||||
chatMessageRepository: sp.GetRequiredService<ChatMessageRepository>(),
|
||||
chatSessionRepository: sp.GetRequiredService<ChatSessionRepository>(),
|
||||
promptOptions: sp.GetRequiredService<IOptions<PromptsOptions>>(),
|
||||
documentImportOptions: sp.GetRequiredService<IOptions<DocumentMemoryOptions>>(),
|
||||
planner: sp.GetRequiredService<CopilotChatPlanner>(),
|
||||
logger: sp.GetRequiredService<ILogger<ChatSkill>>()),
|
||||
nameof(ChatSkill));
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the completion backend to the kernel config for the planner.
|
||||
/// </summary>
|
||||
private static KernelBuilder WithPlannerBackend(this KernelBuilder kernelBuilder, AIServiceOptions options)
|
||||
{
|
||||
return options.Type switch
|
||||
{
|
||||
AIServiceOptions.AIServiceType.AzureOpenAI => kernelBuilder.WithAzureChatCompletionService(options.Models.Planner, options.Endpoint, options.Key),
|
||||
AIServiceOptions.AIServiceType.OpenAI => kernelBuilder.WithOpenAIChatCompletionService(options.Models.Planner, options.Key),
|
||||
_ => throw new ArgumentException($"Invalid {nameof(options.Type)} value in '{AIServiceOptions.PropertyName}' settings."),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
using SemanticKernel.Service.Options;
|
||||
using SemanticKernel.Service.Services;
|
||||
using Tesseract;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IServiceCollection"/>.
|
||||
/// Add options and services for Copilot Chat.
|
||||
/// </summary>
|
||||
public static class CopilotChatServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Parse configuration into options.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCopilotChatOptions(this IServiceCollection services, ConfigurationManager configuration)
|
||||
{
|
||||
// AI service configurations for Copilot Chat.
|
||||
// They are using the same configuration section as Semantic Kernel.
|
||||
services.AddOptions<AIServiceOptions>(AIServiceOptions.PropertyName)
|
||||
.Bind(configuration.GetSection(AIServiceOptions.PropertyName))
|
||||
.ValidateOnStart()
|
||||
.PostConfigure(TrimStringProperties);
|
||||
|
||||
// Chat log storage configuration
|
||||
services.AddOptions<ChatStoreOptions>()
|
||||
.Bind(configuration.GetSection(ChatStoreOptions.PropertyName))
|
||||
.ValidateOnStart()
|
||||
.PostConfigure(TrimStringProperties);
|
||||
|
||||
// Azure speech token configuration
|
||||
services.AddOptions<AzureSpeechOptions>()
|
||||
.Bind(configuration.GetSection(AzureSpeechOptions.PropertyName))
|
||||
.ValidateOnStart()
|
||||
.PostConfigure(TrimStringProperties);
|
||||
|
||||
// Bot schema configuration
|
||||
services.AddOptions<BotSchemaOptions>()
|
||||
.Bind(configuration.GetSection(BotSchemaOptions.PropertyName))
|
||||
.ValidateOnStart()
|
||||
.PostConfigure(TrimStringProperties);
|
||||
|
||||
// Document memory options
|
||||
services.AddOptions<DocumentMemoryOptions>()
|
||||
.Bind(configuration.GetSection(DocumentMemoryOptions.PropertyName))
|
||||
.ValidateOnStart()
|
||||
.PostConfigure(TrimStringProperties);
|
||||
|
||||
// Chat prompt options
|
||||
services.AddOptions<PromptsOptions>()
|
||||
.Bind(configuration.GetSection(PromptsOptions.PropertyName))
|
||||
.ValidateOnStart()
|
||||
.PostConfigure(TrimStringProperties);
|
||||
|
||||
// Planner options
|
||||
services.AddOptions<PlannerOptions>()
|
||||
.Bind(configuration.GetSection(PlannerOptions.PropertyName))
|
||||
.ValidateOnStart()
|
||||
.PostConfigure(TrimStringProperties);
|
||||
|
||||
// OCR support options
|
||||
services.AddOptions<OcrSupportOptions>()
|
||||
.Bind(configuration.GetSection(OcrSupportOptions.PropertyName))
|
||||
.ValidateOnStart()
|
||||
.PostConfigure(TrimStringProperties);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds persistent OCR support service.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static IServiceCollection AddPersistentOcrSupport(this IServiceCollection services)
|
||||
{
|
||||
OcrSupportOptions ocrSupportConfig = services.BuildServiceProvider().GetRequiredService<IOptions<OcrSupportOptions>>().Value;
|
||||
|
||||
switch (ocrSupportConfig.Type)
|
||||
{
|
||||
case OcrSupportOptions.OcrSupportType.Tesseract:
|
||||
{
|
||||
services.AddSingleton<ITesseractEngine>(sp => new TesseractEngineWrapper(new TesseractEngine(ocrSupportConfig.Tesseract!.FilePath, ocrSupportConfig.Tesseract!.Language, EngineMode.Default)));
|
||||
break;
|
||||
}
|
||||
|
||||
case OcrSupportOptions.OcrSupportType.None:
|
||||
{
|
||||
services.AddSingleton<ITesseractEngine>(sp => new NullTesseractEngine());
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported OcrSupport:Type '{ocrSupportConfig.Type}'");
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add persistent chat store services.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddPersistentChatStore(this IServiceCollection services)
|
||||
{
|
||||
IStorageContext<ChatSession> chatSessionStorageContext;
|
||||
IStorageContext<ChatMessage> chatMessageStorageContext;
|
||||
IStorageContext<MemorySource> chatMemorySourceStorageContext;
|
||||
IStorageContext<ChatParticipant> chatParticipantStorageContext;
|
||||
|
||||
ChatStoreOptions chatStoreConfig = services.BuildServiceProvider().GetRequiredService<IOptions<ChatStoreOptions>>().Value;
|
||||
|
||||
switch (chatStoreConfig.Type)
|
||||
{
|
||||
case ChatStoreOptions.ChatStoreType.Volatile:
|
||||
{
|
||||
chatSessionStorageContext = new VolatileContext<ChatSession>();
|
||||
chatMessageStorageContext = new VolatileContext<ChatMessage>();
|
||||
chatMemorySourceStorageContext = new VolatileContext<MemorySource>();
|
||||
chatParticipantStorageContext = new VolatileContext<ChatParticipant>();
|
||||
break;
|
||||
}
|
||||
|
||||
case ChatStoreOptions.ChatStoreType.Filesystem:
|
||||
{
|
||||
if (chatStoreConfig.Filesystem == null)
|
||||
{
|
||||
throw new InvalidOperationException("ChatStore:Filesystem is required when ChatStore:Type is 'Filesystem'");
|
||||
}
|
||||
|
||||
string fullPath = Path.GetFullPath(chatStoreConfig.Filesystem.FilePath);
|
||||
string directory = Path.GetDirectoryName(fullPath) ?? string.Empty;
|
||||
chatSessionStorageContext = new FileSystemContext<ChatSession>(
|
||||
new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_sessions{Path.GetExtension(fullPath)}")));
|
||||
chatMessageStorageContext = new FileSystemContext<ChatMessage>(
|
||||
new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_messages{Path.GetExtension(fullPath)}")));
|
||||
chatMemorySourceStorageContext = new FileSystemContext<MemorySource>(
|
||||
new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_memorysources{Path.GetExtension(fullPath)}")));
|
||||
chatParticipantStorageContext = new FileSystemContext<ChatParticipant>(
|
||||
new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_participants{Path.GetExtension(fullPath)}")));
|
||||
break;
|
||||
}
|
||||
|
||||
case ChatStoreOptions.ChatStoreType.Cosmos:
|
||||
{
|
||||
if (chatStoreConfig.Cosmos == null)
|
||||
{
|
||||
throw new InvalidOperationException("ChatStore:Cosmos is required when ChatStore:Type is 'Cosmos'");
|
||||
}
|
||||
#pragma warning disable CA2000 // Dispose objects before losing scope - objects are singletons for the duration of the process and disposed when the process exits.
|
||||
chatSessionStorageContext = new CosmosDbContext<ChatSession>(
|
||||
chatStoreConfig.Cosmos.ConnectionString, chatStoreConfig.Cosmos.Database, chatStoreConfig.Cosmos.ChatSessionsContainer);
|
||||
chatMessageStorageContext = new CosmosDbContext<ChatMessage>(
|
||||
chatStoreConfig.Cosmos.ConnectionString, chatStoreConfig.Cosmos.Database, chatStoreConfig.Cosmos.ChatMessagesContainer);
|
||||
chatMemorySourceStorageContext = new CosmosDbContext<MemorySource>(
|
||||
chatStoreConfig.Cosmos.ConnectionString, chatStoreConfig.Cosmos.Database, chatStoreConfig.Cosmos.ChatMemorySourcesContainer);
|
||||
chatParticipantStorageContext = new CosmosDbContext<ChatParticipant>(
|
||||
chatStoreConfig.Cosmos.ConnectionString, chatStoreConfig.Cosmos.Database, chatStoreConfig.Cosmos.ChatParticipantsContainer);
|
||||
#pragma warning restore CA2000 // Dispose objects before losing scope
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Invalid 'ChatStore' setting 'chatStoreConfig.Type'.");
|
||||
}
|
||||
}
|
||||
|
||||
services.AddSingleton<ChatSessionRepository>(new ChatSessionRepository(chatSessionStorageContext));
|
||||
services.AddSingleton<ChatMessageRepository>(new ChatMessageRepository(chatMessageStorageContext));
|
||||
services.AddSingleton<ChatMemorySourceRepository>(new ChatMemorySourceRepository(chatMemorySourceStorageContext));
|
||||
services.AddSingleton<ChatParticipantRepository>(new ChatParticipantRepository(chatParticipantStorageContext));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trim all string properties, recursively.
|
||||
/// </summary>
|
||||
private static void TrimStringProperties<T>(T options) where T : class
|
||||
{
|
||||
Queue<object> targets = new();
|
||||
targets.Enqueue(options);
|
||||
|
||||
while (targets.Count > 0)
|
||||
{
|
||||
object target = targets.Dequeue();
|
||||
Type targetType = target.GetType();
|
||||
foreach (PropertyInfo property in targetType.GetProperties())
|
||||
{
|
||||
// Skip enumerations
|
||||
if (property.PropertyType.IsEnum)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Property is a built-in type, readable, and writable.
|
||||
if (property.PropertyType.Namespace == "System" &&
|
||||
property.CanRead &&
|
||||
property.CanWrite)
|
||||
{
|
||||
// Property is a non-null string.
|
||||
if (property.PropertyType == typeof(string) &&
|
||||
property.GetValue(target) != null)
|
||||
{
|
||||
property.SetValue(target, property.GetValue(target)!.ToString()!.Trim());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Property is a non-built-in and non-enum type - queue it for processing.
|
||||
if (property.GetValue(target) != null)
|
||||
{
|
||||
targets.Enqueue(property.GetValue(target)!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a chat hub for real-time communication.
|
||||
/// </summary>
|
||||
public class MessageRelayHub : Hub
|
||||
{
|
||||
private const string ReceiveMessageClientCall = "ReceiveMessage";
|
||||
private const string ReceiveUserTypingStateClientCall = "ReceiveUserTypingState";
|
||||
private readonly ILogger<MessageRelayHub> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MessageRelayHub"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public MessageRelayHub(ILogger<MessageRelayHub> logger)
|
||||
{
|
||||
this._logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the user to the groups that they are a member of.
|
||||
/// Groups are identified by the chat ID.
|
||||
/// TODO: Retrieve the user ID from the claims and call this method
|
||||
/// from the OnConnectedAsync method instead of the frontend.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The ChatID used as group id for SignalR.</param>
|
||||
public async Task AddClientToGroupAsync(string chatId)
|
||||
{
|
||||
await this.Groups.AddToGroupAsync(this.Context.ConnectionId, chatId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to all users except the sender.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The ChatID used as group id for SignalR.</param>
|
||||
/// <param name="message">The message to send.</param>
|
||||
public async Task SendMessageAsync(string chatId, object message)
|
||||
{
|
||||
await this.Clients.OthersInGroup(chatId).SendAsync(ReceiveMessageClientCall, message, chatId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the typing state to all users except the sender.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The ChatID used as group id for SignalR.</param>
|
||||
/// <param name="userId">The user ID of the user who is typing.</param>
|
||||
/// <param name="isTyping">Whether the user is typing.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task SendUserTypingStateAsync(string chatId, string userId, bool isTyping)
|
||||
{
|
||||
await this.Clients.OthersInGroup(chatId).SendAsync(ReceiveUserTypingStateClientCall, chatId, userId, isTyping);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.SemanticKernel.Memory;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The data model of a bot for portability.
|
||||
/// </summary>
|
||||
public class Bot
|
||||
{
|
||||
/// <summary>
|
||||
/// The schema information of the bot data model.
|
||||
/// </summary>
|
||||
public BotSchemaOptions Schema { get; set; } = new BotSchemaOptions();
|
||||
|
||||
/// <summary>
|
||||
/// The embedding configurations.
|
||||
/// </summary>
|
||||
public BotEmbeddingConfig EmbeddingConfigurations { get; set; } = new BotEmbeddingConfig();
|
||||
|
||||
/// <summary>
|
||||
/// The title of the chat with the bot.
|
||||
/// </summary>
|
||||
public string ChatTitle { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The chat history. It contains all the messages in the conversation with the bot.
|
||||
/// </summary>
|
||||
public List<ChatMessage> ChatHistory { get; set; } = new List<ChatMessage>();
|
||||
|
||||
// TODO: Change from MemoryQueryResult to MemoryRecord
|
||||
/// <summary>
|
||||
/// The embeddings of the bot.
|
||||
/// </summary>
|
||||
public List<KeyValuePair<string, List<MemoryQueryResult>>> Embeddings { get; set; } = new List<KeyValuePair<string, List<MemoryQueryResult>>>();
|
||||
|
||||
// TODO: Change from MemoryQueryResult to MemoryRecord
|
||||
/// <summary>
|
||||
/// The embeddings of uploaded documents in Copilot Chat. It represents the document memory which is accessible to all chat sessions of a given user.
|
||||
/// </summary>
|
||||
public List<KeyValuePair<string, List<MemoryQueryResult>>> DocumentEmbeddings { get; set; } = new List<KeyValuePair<string, List<MemoryQueryResult>>>();
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The embedding configuration of a bot. Used in the Bot object for portability.
|
||||
/// </summary>
|
||||
public class BotEmbeddingConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The AI service.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public AIServiceOptions.AIServiceType AIService { get; set; } = AIServiceOptions.AIServiceType.AzureOpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// The deployment or the model id.
|
||||
/// </summary>
|
||||
public string DeploymentOrModelId { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Information about a single chat message.
|
||||
/// </summary>
|
||||
public class ChatMessage : IStorageEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Role of the author of a chat message.
|
||||
/// </summary>
|
||||
public enum AuthorRoles
|
||||
{
|
||||
/// <summary>
|
||||
/// The current user of the chat.
|
||||
/// </summary>
|
||||
User = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The bot.
|
||||
/// </summary>
|
||||
Bot,
|
||||
|
||||
/// <summary>
|
||||
/// The participant who is not the current user nor the bot of the chat.
|
||||
/// </summary>
|
||||
Participant
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Type of the chat message.
|
||||
/// </summary>
|
||||
public enum ChatMessageType
|
||||
{
|
||||
/// <summary>
|
||||
/// A standard message
|
||||
/// </summary>
|
||||
Message,
|
||||
|
||||
/// <summary>
|
||||
/// A message for a Plan
|
||||
/// </summary>
|
||||
Plan,
|
||||
|
||||
/// <summary>
|
||||
/// An uploaded document notification
|
||||
/// </summary>
|
||||
Document,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of the message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("timestamp")]
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id of the user who sent this message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("userId")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the user who sent this message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("userName")]
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id of the chat this message belongs to.
|
||||
/// </summary>
|
||||
[JsonPropertyName("chatId")]
|
||||
public string ChatId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Content of the message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Id of the message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Role of the author of the message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("authorRole")]
|
||||
public AuthorRoles AuthorRole { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Prompt used to generate the message.
|
||||
/// Will be empty if the message is not generated by a prompt.
|
||||
/// </summary>
|
||||
[JsonPropertyName("prompt")]
|
||||
public string Prompt { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Type of the message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public ChatMessageType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new chat message. Timestamp is automatically generated.
|
||||
/// </summary>
|
||||
/// <param name="userId">Id of the user who sent this message</param>
|
||||
/// <param name="userName">Name of the user who sent this message</param>
|
||||
/// <param name="chatId">The chat ID that this message belongs to</param>
|
||||
/// <param name="content">The message</param>
|
||||
/// <param name="prompt">The prompt used to generate the message</param>
|
||||
/// <param name="authorRole">Role of the author</param>
|
||||
/// <param name="type">Type of the message</param>
|
||||
public ChatMessage(
|
||||
string userId,
|
||||
string userName,
|
||||
string chatId,
|
||||
string content,
|
||||
string prompt = "",
|
||||
AuthorRoles authorRole = AuthorRoles.User,
|
||||
ChatMessageType type = ChatMessageType.Message)
|
||||
{
|
||||
this.Timestamp = DateTimeOffset.Now;
|
||||
this.UserId = userId;
|
||||
this.UserName = userName;
|
||||
this.ChatId = chatId;
|
||||
this.Content = content;
|
||||
this.Id = Guid.NewGuid().ToString();
|
||||
this.Prompt = prompt;
|
||||
this.AuthorRole = authorRole;
|
||||
this.Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new chat message for the bot response.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The chat ID that this message belongs to</param>
|
||||
/// <param name="content">The message</param>
|
||||
/// <param name="prompt">The prompt used to generate the message</param>
|
||||
public static ChatMessage CreateBotResponseMessage(string chatId, string content, string prompt)
|
||||
{
|
||||
return new ChatMessage("bot", "bot", chatId, content, prompt, AuthorRoles.Bot, IsPlan(content) ? ChatMessageType.Plan : ChatMessageType.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new chat message for a document upload.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user ID that uploaded the document</param>
|
||||
/// <param name="userName">The user name that uploaded the document</param>
|
||||
/// <param name="chatId">The chat ID that this message belongs to</param>
|
||||
/// <param name="documentMessageContent">The document message content</param>
|
||||
public static ChatMessage CreateDocumentMessage(string userId, string userName, string chatId, DocumentMessageContent documentMessageContent)
|
||||
{
|
||||
return new ChatMessage(userId, userName, chatId, documentMessageContent.ToString(), string.Empty, AuthorRoles.User, ChatMessageType.Document);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the object to a formatted string.
|
||||
/// </summary>
|
||||
/// <returns>A formatted string</returns>
|
||||
public string ToFormattedString()
|
||||
{
|
||||
var content = this.Content;
|
||||
if (this.Type == ChatMessageType.Document)
|
||||
{
|
||||
var documentMessageContent = DocumentMessageContent.FromString(content);
|
||||
content = (documentMessageContent != null) ? documentMessageContent.ToFormattedString() : "Uploaded documents";
|
||||
}
|
||||
|
||||
return $"[{this.Timestamp.ToString("G", CultureInfo.CurrentCulture)}] {this.UserName}: {content}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the object to a JSON string.
|
||||
/// </summary>
|
||||
/// <returns>A serialized json string</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JSON string to a ChatMessage object.
|
||||
/// </summary>
|
||||
/// <param name="json">A json string</param>
|
||||
/// <returns>A ChatMessage object</returns>
|
||||
public static ChatMessage? FromString(string json)
|
||||
{
|
||||
return JsonSerializer.Deserialize<ChatMessage>(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the response is a Plan.
|
||||
/// This is a copy of the `isPlan` function on the frontend.
|
||||
/// </summary>
|
||||
/// <param name="response">The response from the bot.</param>
|
||||
/// <returns>True if the response represents Plan, false otherwise.</returns>
|
||||
private static bool IsPlan(string response)
|
||||
{
|
||||
var planPrefix = "proposedPlan\":";
|
||||
return response.IndexOf(planPrefix, StringComparison.Ordinal) != -1;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A chat participant is a user that is part of a chat.
|
||||
/// A user can be part of multiple chats, thus a user can have multiple chat participants.
|
||||
/// </summary>
|
||||
public class ChatParticipant : IStorageEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Participant ID that is persistent and unique.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User ID that is persistent and unique.
|
||||
/// </summary>
|
||||
[JsonPropertyName("userId")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Chat ID that this participant belongs to.
|
||||
/// </summary>
|
||||
[JsonPropertyName("chatId")]
|
||||
public string ChatId { get; set; }
|
||||
|
||||
public ChatParticipant(string userId, string chatId)
|
||||
{
|
||||
this.Id = Guid.NewGuid().ToString();
|
||||
this.UserId = userId;
|
||||
this.ChatId = chatId;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A chat session
|
||||
/// </summary>
|
||||
public class ChatSession : IStorageEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Chat ID that is persistent and unique.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Title of the chat.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of the chat creation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdOn")]
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
|
||||
public ChatSession(string title)
|
||||
{
|
||||
this.Id = Guid.NewGuid().ToString();
|
||||
this.Title = title;
|
||||
this.CreatedOn = DateTimeOffset.Now;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Json body for creating a new chat session.
|
||||
/// </summary>
|
||||
public class CreateChatParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the user who sent this message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("userId")]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Title of the chat.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
public sealed class DocumentData
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the uploaded document.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Size of the uploaded document in bytes.
|
||||
/// </summary>
|
||||
[JsonPropertyName("size")]
|
||||
public string Size { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Status of the uploaded document.
|
||||
/// If true, the document is successfully uploaded. False otherwise.
|
||||
/// </summary>
|
||||
[JsonPropertyName("isUploaded")]
|
||||
public bool IsUploaded { get; set; } = false;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Form for importing a document from a POST Http request.
|
||||
/// </summary>
|
||||
public class DocumentImportForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Scope of the document. This determines the collection name in the document memory.
|
||||
/// </summary>
|
||||
public enum DocumentScopes
|
||||
{
|
||||
Global,
|
||||
Chat,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The file to import.
|
||||
/// </summary>
|
||||
public IEnumerable<IFormFile> FormFiles { get; set; } = Enumerable.Empty<IFormFile>();
|
||||
|
||||
/// <summary>
|
||||
/// Scope of the document. This determines the collection name in the document memory.
|
||||
/// </summary>
|
||||
public DocumentScopes DocumentScope { get; set; } = DocumentScopes.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the chat that owns the document.
|
||||
/// This is used to create a unique collection name for the chat.
|
||||
/// If the chat ID is not specified or empty, the documents will be stored in a global collection.
|
||||
/// If the document scope is set to global, this value is ignored.
|
||||
/// </summary>
|
||||
public Guid ChatId { get; set; } = Guid.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the user who is importing the document to a chat session.
|
||||
/// Will be use to validate if the user has access to the chat session.
|
||||
/// </summary>
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the user who sent this message.
|
||||
/// Will be used to create the chat message representing the document upload.
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Value of `Content` for a `ChatMessage` of type `ChatMessageType.Document`.
|
||||
/// </summary>
|
||||
public class DocumentMessageContent
|
||||
{
|
||||
/// <summary>
|
||||
/// List of documents contained in the message.
|
||||
/// </summary>
|
||||
[JsonPropertyName("documents")]
|
||||
public IEnumerable<DocumentData> Documents { get; private set; } = Enumerable.Empty<DocumentData>();
|
||||
|
||||
/// <summary>
|
||||
/// Add a document to the list of documents.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the uploaded document</param>
|
||||
/// <param name="size">Size of the uploaded document in bytes</param>
|
||||
/// <param name="isUploaded">Status of the uploaded document</param>
|
||||
public void AddDocument(string name, string size, bool isUploaded)
|
||||
{
|
||||
this.Documents = this.Documents.Append(new DocumentData
|
||||
{
|
||||
Name = name,
|
||||
Size = size,
|
||||
IsUploaded = isUploaded,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the object to a JSON string.
|
||||
/// </summary>
|
||||
/// <returns>A serialized JSON string</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the object to a formatted string.
|
||||
/// Only successful uploads will be included in the formatted string.
|
||||
/// </summary>
|
||||
/// <returns>A formatted string</returns>
|
||||
public string ToFormattedString()
|
||||
{
|
||||
if (!this.Documents.Any())
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var formattedStrings = this.Documents
|
||||
.Where(document => document.IsUploaded)
|
||||
.Select(document => $"[Name: {document.Name}, Size: {document.Size}]").ToList();
|
||||
|
||||
if (formattedStrings.Count == 1)
|
||||
{
|
||||
return $"Uploaded a document {formattedStrings.First()}.";
|
||||
}
|
||||
|
||||
return $"Uploaded documents: {string.Join(", ", formattedStrings)}.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the object to a formatted string that only
|
||||
/// contains document names separated by comma.
|
||||
/// </summary>
|
||||
/// <returns>A formatted string</returns>
|
||||
public string ToFormattedStringNamesOnly()
|
||||
{
|
||||
if (!this.Documents.Any())
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var formattedStrings = this.Documents
|
||||
.Where(document => document.IsUploaded)
|
||||
.Select(document => document.Name).ToList();
|
||||
|
||||
if (formattedStrings.Count == 1)
|
||||
{
|
||||
return formattedStrings.First();
|
||||
}
|
||||
|
||||
return string.Join(", ", formattedStrings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JSON string to a DocumentMessageContent object.
|
||||
/// </summary>
|
||||
/// <param name="json">A JSON string</param>
|
||||
/// <returns>A DocumentMessageContent object</returns>
|
||||
public static DocumentMessageContent? FromString(string json)
|
||||
{
|
||||
return JsonSerializer.Deserialize<DocumentMessageContent>(json);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// <summary>
|
||||
/// The external memory source.
|
||||
/// </summary>
|
||||
public class MemorySource : IStorageEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of the memory source.
|
||||
/// </summary>
|
||||
public enum MemorySourceType
|
||||
{
|
||||
// A file source.
|
||||
File,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source ID that is persistent and unique.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The Chat ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("chatId")]
|
||||
public string ChatId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The type of the source.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
[JsonPropertyName("sourceType")]
|
||||
public MemorySourceType SourceType { get; set; } = MemorySourceType.File;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the source.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The external link to the source.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hyperlink")]
|
||||
public Uri? HyperLink { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The user ID of who shared the source.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sharedBy")]
|
||||
public string SharedBy { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the source is created in the bot.
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdOn")]
|
||||
public DateTimeOffset CreatedOn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the source in bytes.
|
||||
/// </summary>
|
||||
[JsonPropertyName("size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Empty constructor for serialization.
|
||||
/// </summary>
|
||||
public MemorySource()
|
||||
{
|
||||
}
|
||||
|
||||
public MemorySource(string chatId, string name, string sharedBy, MemorySourceType type, long size, Uri? hyperlink)
|
||||
{
|
||||
this.Id = Guid.NewGuid().ToString();
|
||||
this.ChatId = chatId;
|
||||
this.Name = name;
|
||||
this.SourceType = type;
|
||||
this.HyperLink = hyperlink;
|
||||
this.SharedBy = sharedBy;
|
||||
this.CreatedOn = DateTimeOffset.Now;
|
||||
this.Size = size;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// /// <summary>
|
||||
/// Represents the authentication headers for imported OpenAPI Plugin Skills.
|
||||
/// </summary>
|
||||
public class OpenApiSkillsAuthHeaders
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the MS Graph authentication header value.
|
||||
/// </summary>
|
||||
[FromHeader(Name = "x-sk-copilot-graph-auth")]
|
||||
public string? GraphAuthentication { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Jira authentication header value.
|
||||
/// </summary>
|
||||
[FromHeader(Name = "x-sk-copilot-jira-auth")]
|
||||
public string? JiraAuthentication { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the GitHub authentication header value.
|
||||
/// </summary>
|
||||
[FromHeader(Name = "x-sk-copilot-github-auth")]
|
||||
public string? GithubAuthentication { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Klarna header value.
|
||||
/// </summary>
|
||||
[FromHeader(Name = "x-sk-copilot-klarna-auth")]
|
||||
public string? KlarnaAuthentication { get; set; }
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.SemanticKernel.Planning;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
// Type of Plan
|
||||
public enum PlanType
|
||||
{
|
||||
Action, // single-step
|
||||
Sequential, // multi-step
|
||||
}
|
||||
|
||||
// State of Plan
|
||||
public enum PlanState
|
||||
{
|
||||
NoOp, // Plan has not received any user input
|
||||
Approved,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a single proposed plan.
|
||||
/// </summary>
|
||||
public class ProposedPlan
|
||||
{
|
||||
/// <summary>
|
||||
/// Plan object to be approved or invoked.
|
||||
/// </summary>
|
||||
[JsonPropertyName("proposedPlan")]
|
||||
public Plan Plan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether plan is Action (single-step) or Sequential (multi-step).
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public PlanType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// State of plan
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public PlanState State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new proposed plan.
|
||||
/// </summary>
|
||||
/// <param name="plan">Proposed plan object</param>
|
||||
public ProposedPlan(Plan plan, PlanType type, PlanState state)
|
||||
{
|
||||
this.Plan = plan;
|
||||
this.Type = type;
|
||||
this.State = state;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Token Response is a simple wrapper around the token and region
|
||||
/// </summary>
|
||||
public class SpeechTokenResponse
|
||||
{
|
||||
public string? Token { get; set; }
|
||||
public string? Region { get; set; }
|
||||
public bool? IsSuccess { get; set; }
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for Azure speech recognition.
|
||||
/// </summary>
|
||||
public sealed class AzureSpeechOptions
|
||||
{
|
||||
public const string PropertyName = "AzureSpeech";
|
||||
|
||||
/// <summary>
|
||||
/// Location of the Azure speech service to use (e.g. "South Central US")
|
||||
/// </summary>
|
||||
public string? Region { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Key to access the Azure speech service.
|
||||
/// </summary>
|
||||
public string? Key { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the bot file schema that is supported by this application.
|
||||
/// </summary>
|
||||
public class BotSchemaOptions
|
||||
{
|
||||
public const string PropertyName = "BotSchema";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the schema.
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The version of the schema.
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int Version { get; set; }
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration settings for the chat store.
|
||||
/// </summary>
|
||||
public class ChatStoreOptions
|
||||
{
|
||||
public const string PropertyName = "ChatStore";
|
||||
|
||||
/// <summary>
|
||||
/// The type of chat store to use.
|
||||
/// </summary>
|
||||
public enum ChatStoreType
|
||||
{
|
||||
/// <summary>
|
||||
/// Non-persistent chat store
|
||||
/// </summary>
|
||||
Volatile,
|
||||
|
||||
/// <summary>
|
||||
/// File-system based persistent chat store.
|
||||
/// </summary>
|
||||
Filesystem,
|
||||
|
||||
/// <summary>
|
||||
/// Azure CosmosDB based persistent chat store.
|
||||
/// </summary>
|
||||
Cosmos
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of chat store to use.
|
||||
/// </summary>
|
||||
public ChatStoreType Type { get; set; } = ChatStoreType.Volatile;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration for the file system chat store.
|
||||
/// </summary>
|
||||
[RequiredOnPropertyValue(nameof(Type), ChatStoreType.Filesystem)]
|
||||
public FileSystemOptions? Filesystem { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration for the Azure CosmosDB chat store.
|
||||
/// </summary>
|
||||
[RequiredOnPropertyValue(nameof(Type), ChatStoreType.Cosmos)]
|
||||
public CosmosOptions? Cosmos { get; set; }
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration settings for connecting to Azure CosmosDB.
|
||||
/// </summary>
|
||||
public class CosmosOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Cosmos database name.
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string Database { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Cosmos connection string.
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string ConnectionString { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Cosmos container for chat sessions.
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string ChatSessionsContainer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Cosmos container for chat messages.
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string ChatMessagesContainer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Cosmos container for chat memory sources.
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string ChatMemorySourcesContainer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Cosmos container for chat participants.
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string ChatParticipantsContainer { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for handling memorized documents.
|
||||
/// </summary>
|
||||
public class DocumentMemoryOptions
|
||||
{
|
||||
public const string PropertyName = "DocumentMemory";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the global document collection.
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string GlobalDocumentCollectionName { get; set; } = "global-documents";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the prefix for the chat document collection name.
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string ChatDocumentCollectionNamePrefix { get; set; } = "chat-documents-";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of tokens to use when splitting a document into lines.
|
||||
/// Default token limits are suggested by OpenAI:
|
||||
/// https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int DocumentLineSplitMaxTokens { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of lines to use when combining lines into paragraphs.
|
||||
/// Default token limits are suggested by OpenAI:
|
||||
/// https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int DocumentParagraphSplitMaxLines { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum size in bytes of a document to be allowed for importing.
|
||||
/// Prevent large uploads by setting a file size limit (in bytes) as suggested here:
|
||||
/// https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-6.0
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int FileSizeLimit { get; set; } = 1000000;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of files to be allowed for importing in a single request.
|
||||
/// </summary>
|
||||
[Range(0, int.MaxValue)]
|
||||
public int FileCountLimit { get; set; } = 10;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
/// <summary>
|
||||
/// File system storage configuration.
|
||||
/// </summary>
|
||||
public class FileSystemOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the file path for persistent file system storage.
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Ocr Support Configuration Options
|
||||
/// </summary>
|
||||
public class OcrSupportOptions
|
||||
{
|
||||
public const string PropertyName = "OcrSupport";
|
||||
|
||||
public enum OcrSupportType
|
||||
{
|
||||
/// <summary>
|
||||
/// No OCR Support
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Tesseract OCR Support
|
||||
/// </summary>
|
||||
Tesseract
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of OCR support to use.
|
||||
/// </summary>
|
||||
public OcrSupportType Type { get; set; } = OcrSupportType.None;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration for the Tesseract OCR support.
|
||||
/// </summary>
|
||||
[RequiredOnPropertyValue(nameof(Type), OcrSupportType.Tesseract)]
|
||||
public TesseractOptions? Tesseract { get; set; }
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the planner.
|
||||
/// </summary>
|
||||
public class PlannerOptions
|
||||
{
|
||||
public const string PropertyName = "Planner";
|
||||
|
||||
/// <summary>
|
||||
/// Define if the planner must be Sequential or not.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public PlanType Type { get; set; } = PlanType.Action;
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the chat
|
||||
/// </summary>
|
||||
public class PromptsOptions
|
||||
{
|
||||
public const string PropertyName = "Prompts";
|
||||
|
||||
/// <summary>
|
||||
/// Token limit of the chat model.
|
||||
/// </summary>
|
||||
/// <remarks>https://platform.openai.com/docs/models/overview for token limits.</remarks>
|
||||
[Required, Range(0, int.MaxValue)] public int CompletionTokenLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The token count left for the model to generate text after the prompt.
|
||||
/// </summary>
|
||||
[Required, Range(0, int.MaxValue)] public int ResponseTokenLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Weight of memories in the contextual part of the final prompt.
|
||||
/// Contextual prompt excludes all the system commands and user intent.
|
||||
/// </summary>
|
||||
internal double MemoriesResponseContextWeight { get; } = 0.3;
|
||||
|
||||
/// <summary>
|
||||
/// Weight of documents in the contextual part of the final prompt.
|
||||
/// Contextual prompt excludes all the system commands and user intent.
|
||||
/// </summary>
|
||||
internal double DocumentContextWeight { get; } = 0.3;
|
||||
|
||||
/// <summary>
|
||||
/// Weight of information returned from planner (i.e., responses from OpenAPI skills).
|
||||
/// Contextual prompt excludes all the system commands and user intent.
|
||||
/// </summary>
|
||||
internal double ExternalInformationContextWeight { get; } = 0.3;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum relevance of a semantic memory to be included in the final prompt.
|
||||
/// The higher the value, the answer will be more relevant to the user intent.
|
||||
/// </summary>
|
||||
internal double SemanticMemoryMinRelevance { get; } = 0.8;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum relevance of a document memory to be included in the final prompt.
|
||||
/// The higher the value, the answer will be more relevant to the user intent.
|
||||
/// </summary>
|
||||
internal double DocumentMemoryMinRelevance { get; } = 0.8;
|
||||
|
||||
// System
|
||||
[Required, NotEmptyOrWhitespace] public string KnowledgeCutoffDate { get; set; } = string.Empty;
|
||||
[Required, NotEmptyOrWhitespace] public string InitialBotMessage { get; set; } = string.Empty;
|
||||
[Required, NotEmptyOrWhitespace] public string SystemDescription { get; set; } = string.Empty;
|
||||
[Required, NotEmptyOrWhitespace] public string SystemResponse { get; set; } = string.Empty;
|
||||
|
||||
internal string[] SystemAudiencePromptComponents => new string[]
|
||||
{
|
||||
this.SystemAudience,
|
||||
"{{ChatSkill.ExtractChatHistory}}",
|
||||
this.SystemAudienceContinuation
|
||||
};
|
||||
|
||||
internal string SystemAudienceExtraction => string.Join("\n", this.SystemAudiencePromptComponents);
|
||||
|
||||
internal string[] SystemIntentPromptComponents => new string[]
|
||||
{
|
||||
this.SystemDescription,
|
||||
this.SystemIntent,
|
||||
"{{ChatSkill.ExtractChatHistory}}",
|
||||
this.SystemIntentContinuation
|
||||
};
|
||||
|
||||
internal string SystemIntentExtraction => string.Join("\n", this.SystemIntentPromptComponents);
|
||||
|
||||
// Intent extraction
|
||||
[Required, NotEmptyOrWhitespace] public string SystemIntent { get; set; } = string.Empty;
|
||||
[Required, NotEmptyOrWhitespace] public string SystemIntentContinuation { get; set; } = string.Empty;
|
||||
|
||||
// Audience extraction
|
||||
[Required, NotEmptyOrWhitespace] public string SystemAudience { get; set; } = string.Empty;
|
||||
[Required, NotEmptyOrWhitespace] public string SystemAudienceContinuation { get; set; } = string.Empty;
|
||||
|
||||
// Memory extraction
|
||||
[Required, NotEmptyOrWhitespace] public string SystemCognitive { get; set; } = string.Empty;
|
||||
[Required, NotEmptyOrWhitespace] public string MemoryFormat { get; set; } = string.Empty;
|
||||
[Required, NotEmptyOrWhitespace] public string MemoryAntiHallucination { get; set; } = string.Empty;
|
||||
[Required, NotEmptyOrWhitespace] public string MemoryContinuation { get; set; } = string.Empty;
|
||||
|
||||
// Long-term memory
|
||||
[Required, NotEmptyOrWhitespace] public string LongTermMemoryName { get; set; } = string.Empty;
|
||||
[Required, NotEmptyOrWhitespace] public string LongTermMemoryExtraction { get; set; } = string.Empty;
|
||||
|
||||
internal string[] LongTermMemoryPromptComponents => new string[]
|
||||
{
|
||||
this.SystemCognitive,
|
||||
$"{this.LongTermMemoryName} Description:\n{this.LongTermMemoryExtraction}",
|
||||
this.MemoryAntiHallucination,
|
||||
$"Chat Description:\n{this.SystemDescription}",
|
||||
"{{ChatSkill.ExtractChatHistory}}",
|
||||
this.MemoryContinuation
|
||||
};
|
||||
|
||||
internal string LongTermMemory => string.Join("\n", this.LongTermMemoryPromptComponents);
|
||||
|
||||
// Working memory
|
||||
[Required, NotEmptyOrWhitespace] public string WorkingMemoryName { get; set; } = string.Empty;
|
||||
[Required, NotEmptyOrWhitespace] public string WorkingMemoryExtraction { get; set; } = string.Empty;
|
||||
|
||||
internal string[] WorkingMemoryPromptComponents => new string[]
|
||||
{
|
||||
this.SystemCognitive,
|
||||
$"{this.WorkingMemoryName} Description:\n{this.WorkingMemoryExtraction}",
|
||||
this.MemoryAntiHallucination,
|
||||
$"Chat Description:\n{this.SystemDescription}",
|
||||
"{{ChatSkill.ExtractChatHistory}}",
|
||||
this.MemoryContinuation
|
||||
};
|
||||
|
||||
internal string WorkingMemory => string.Join("\n", this.WorkingMemoryPromptComponents);
|
||||
|
||||
// Memory map
|
||||
internal IDictionary<string, string> MemoryMap => new Dictionary<string, string>()
|
||||
{
|
||||
{ this.LongTermMemoryName, this.LongTermMemory },
|
||||
{ this.WorkingMemoryName, this.WorkingMemory }
|
||||
};
|
||||
|
||||
// Chat commands
|
||||
internal string SystemChatContinuation = "SINGLE RESPONSE FROM BOT TO USER:\n[{{TimeSkill.Now}} {{timeSkill.Second}}] bot:";
|
||||
|
||||
internal string[] SystemChatPromptComponents => new string[]
|
||||
{
|
||||
this.SystemDescription,
|
||||
this.SystemResponse,
|
||||
"{{$audience}}",
|
||||
"{{$userIntent}}",
|
||||
"{{$chatContext}}",
|
||||
this.SystemChatContinuation
|
||||
};
|
||||
|
||||
internal string SystemChatPrompt => string.Join("\n\n", this.SystemChatPromptComponents);
|
||||
|
||||
internal double ResponseTemperature { get; } = 0.7;
|
||||
internal double ResponseTopP { get; } = 1;
|
||||
internal double ResponsePresencePenalty { get; } = 0.5;
|
||||
internal double ResponseFrequencyPenalty { get; } = 0.5;
|
||||
|
||||
internal double IntentTemperature { get; } = 0.7;
|
||||
internal double IntentTopP { get; } = 1;
|
||||
internal double IntentPresencePenalty { get; } = 0.5;
|
||||
internal double IntentFrequencyPenalty { get; } = 0.5;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using SemanticKernel.Service.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for Tesseract OCR support.
|
||||
/// </summary>
|
||||
public sealed class TesseractOptions
|
||||
{
|
||||
public const string PropertyName = "Tesseract";
|
||||
|
||||
/// <summary>
|
||||
/// The file path where the Tesseract language file is stored (e.g. "./data")
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string? FilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The language file prefix name (e.g. "eng")
|
||||
/// </summary>
|
||||
[Required, NotEmptyOrWhitespace]
|
||||
public string? Language { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,632 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.AI.TextCompletion;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
using Microsoft.SemanticKernel.SkillDefinition;
|
||||
using Microsoft.SemanticKernel.TemplateEngine;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
using SemanticKernel.Service.CopilotChat.Storage;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
|
||||
|
||||
/// <summary>
|
||||
/// ChatSkill offers a more coherent chat experience by using memories
|
||||
/// to extract conversation history and user intentions.
|
||||
/// </summary>
|
||||
public class ChatSkill
|
||||
{
|
||||
/// <summary>
|
||||
/// A kernel instance to create a completion function since each invocation
|
||||
/// of the <see cref="ChatAsync"/> function will generate a new prompt dynamically.
|
||||
/// </summary>
|
||||
private readonly IKernel _kernel;
|
||||
|
||||
/// <summary>
|
||||
/// A repository to save and retrieve chat messages.
|
||||
/// </summary>
|
||||
private readonly ChatMessageRepository _chatMessageRepository;
|
||||
|
||||
/// <summary>
|
||||
/// A repository to save and retrieve chat sessions.
|
||||
/// </summary>
|
||||
private readonly ChatSessionRepository _chatSessionRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Settings containing prompt texts.
|
||||
/// </summary>
|
||||
private readonly PromptsOptions _promptOptions;
|
||||
|
||||
/// <summary>
|
||||
/// A semantic chat memory skill instance to query semantic memories.
|
||||
/// </summary>
|
||||
private readonly SemanticChatMemorySkill _semanticChatMemorySkill;
|
||||
|
||||
/// <summary>
|
||||
/// A document memory skill instance to query document memories.
|
||||
/// </summary>
|
||||
private readonly DocumentMemorySkill _documentMemorySkill;
|
||||
|
||||
/// <summary>
|
||||
/// A skill instance to acquire external information.
|
||||
/// </summary>
|
||||
private readonly ExternalInformationSkill _externalInformationSkill;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of <see cref="ChatSkill"/>.
|
||||
/// </summary>
|
||||
public ChatSkill(
|
||||
IKernel kernel,
|
||||
ChatMessageRepository chatMessageRepository,
|
||||
ChatSessionRepository chatSessionRepository,
|
||||
IOptions<PromptsOptions> promptOptions,
|
||||
IOptions<DocumentMemoryOptions> documentImportOptions,
|
||||
CopilotChatPlanner planner,
|
||||
ILogger logger)
|
||||
{
|
||||
this._kernel = kernel;
|
||||
this._chatMessageRepository = chatMessageRepository;
|
||||
this._chatSessionRepository = chatSessionRepository;
|
||||
this._promptOptions = promptOptions.Value;
|
||||
|
||||
this._semanticChatMemorySkill = new SemanticChatMemorySkill(
|
||||
promptOptions);
|
||||
this._documentMemorySkill = new DocumentMemorySkill(
|
||||
promptOptions,
|
||||
documentImportOptions);
|
||||
this._externalInformationSkill = new ExternalInformationSkill(
|
||||
promptOptions,
|
||||
planner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract user intent from the conversation history.
|
||||
/// </summary>
|
||||
/// <param name="context">The SKContext.</param>
|
||||
[SKFunction, Description("Extract user intent")]
|
||||
[SKParameter("chatId", "Chat ID to extract history from")]
|
||||
[SKParameter("audience", "The audience the chat bot is interacting with.")]
|
||||
public async Task<string> ExtractUserIntentAsync(SKContext context)
|
||||
{
|
||||
var tokenLimit = this._promptOptions.CompletionTokenLimit;
|
||||
var historyTokenBudget =
|
||||
tokenLimit -
|
||||
this._promptOptions.ResponseTokenLimit -
|
||||
Utilities.TokenCount(string.Join("\n", new string[]
|
||||
{
|
||||
this._promptOptions.SystemDescription,
|
||||
this._promptOptions.SystemIntent,
|
||||
this._promptOptions.SystemIntentContinuation
|
||||
})
|
||||
);
|
||||
|
||||
// Clone the context to avoid modifying the original context variables.
|
||||
var intentExtractionContext = Utilities.CopyContextWithVariablesClone(context);
|
||||
intentExtractionContext.Variables.Set("tokenLimit", historyTokenBudget.ToString(new NumberFormatInfo()));
|
||||
intentExtractionContext.Variables.Set("knowledgeCutoff", this._promptOptions.KnowledgeCutoffDate);
|
||||
|
||||
var completionFunction = this._kernel.CreateSemanticFunction(
|
||||
this._promptOptions.SystemIntentExtraction,
|
||||
skillName: nameof(ChatSkill),
|
||||
description: "Complete the prompt.");
|
||||
|
||||
var result = await completionFunction.InvokeAsync(
|
||||
intentExtractionContext,
|
||||
settings: this.CreateIntentCompletionSettings()
|
||||
);
|
||||
|
||||
if (result.ErrorOccurred)
|
||||
{
|
||||
context.Log.LogError("{0}: {1}", result.LastErrorDescription, result.LastException);
|
||||
context.Fail(result.LastErrorDescription);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"User intent: {result}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the list of participants from the conversation history.
|
||||
/// Note that only those who have spoken will be included.
|
||||
/// </summary>
|
||||
[SKFunction, Description("Extract audience list")]
|
||||
[SKParameter("chatId", "Chat ID to extract history from")]
|
||||
public async Task<string> ExtractAudienceAsync(SKContext context)
|
||||
{
|
||||
var tokenLimit = this._promptOptions.CompletionTokenLimit;
|
||||
var historyTokenBudget =
|
||||
tokenLimit -
|
||||
this._promptOptions.ResponseTokenLimit -
|
||||
Utilities.TokenCount(string.Join("\n", new string[]
|
||||
{
|
||||
this._promptOptions.SystemAudience,
|
||||
this._promptOptions.SystemAudienceContinuation,
|
||||
})
|
||||
);
|
||||
|
||||
// Clone the context to avoid modifying the original context variables.
|
||||
var audienceExtractionContext = Utilities.CopyContextWithVariablesClone(context);
|
||||
audienceExtractionContext.Variables.Set("tokenLimit", historyTokenBudget.ToString(new NumberFormatInfo()));
|
||||
|
||||
var completionFunction = this._kernel.CreateSemanticFunction(
|
||||
this._promptOptions.SystemAudienceExtraction,
|
||||
skillName: nameof(ChatSkill),
|
||||
description: "Complete the prompt.");
|
||||
|
||||
var result = await completionFunction.InvokeAsync(
|
||||
audienceExtractionContext,
|
||||
settings: this.CreateIntentCompletionSettings()
|
||||
);
|
||||
|
||||
if (result.ErrorOccurred)
|
||||
{
|
||||
context.Log.LogError("{0}: {1}", result.LastErrorDescription, result.LastException);
|
||||
context.Fail(result.LastErrorDescription);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"List of participants: {result}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract chat history.
|
||||
/// </summary>
|
||||
/// <param name="context">Contains the 'tokenLimit' controlling the length of the prompt.</param>
|
||||
[SKFunction, Description("Extract chat history")]
|
||||
public async Task<string> ExtractChatHistoryAsync(
|
||||
[Description("Chat ID to extract history from")] string chatId,
|
||||
[Description("Maximum number of tokens")] int tokenLimit)
|
||||
{
|
||||
var messages = await this._chatMessageRepository.FindByChatIdAsync(chatId);
|
||||
var sortedMessages = messages.OrderByDescending(m => m.Timestamp);
|
||||
|
||||
var remainingToken = tokenLimit;
|
||||
|
||||
string historyText = "";
|
||||
foreach (var chatMessage in sortedMessages)
|
||||
{
|
||||
var formattedMessage = chatMessage.ToFormattedString();
|
||||
|
||||
// Plan object is not meaningful content in generating bot response, so shorten to intent only to save on tokens
|
||||
if (formattedMessage.Contains("proposedPlan\":", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
string pattern = @"(\[.*?\]).*User Intent:User intent: (.*)(?=""}})";
|
||||
Match match = Regex.Match(formattedMessage, pattern);
|
||||
if (match.Success)
|
||||
{
|
||||
string timestamp = match.Groups[1].Value.Trim();
|
||||
string userIntent = match.Groups[2].Value.Trim();
|
||||
|
||||
formattedMessage = $"{timestamp} Bot proposed plan to fulfill user intent: {userIntent}";
|
||||
}
|
||||
else
|
||||
{
|
||||
formattedMessage = "Bot proposed plan";
|
||||
}
|
||||
}
|
||||
|
||||
var tokenCount = Utilities.TokenCount(formattedMessage);
|
||||
|
||||
if (remainingToken - tokenCount >= 0)
|
||||
{
|
||||
historyText = $"{formattedMessage}\n{historyText}";
|
||||
remainingToken -= tokenCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $"Chat history:\n{historyText.Trim()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the entry point for getting a chat response. It manages the token limit, saves
|
||||
/// messages to memory, and fill in the necessary context variables for completing the
|
||||
/// prompt that will be rendered by the template engine.
|
||||
/// </summary>
|
||||
[SKFunction, Description("Get chat response")]
|
||||
public async Task<SKContext> ChatAsync(
|
||||
[Description("The new message")] string message,
|
||||
[Description("Unique and persistent identifier for the user")] string userId,
|
||||
[Description("Name of the user")] string userName,
|
||||
[Description("Unique and persistent identifier for the chat")] string chatId,
|
||||
[Description("Type of the message")] string messageType,
|
||||
[Description("Previously proposed plan that is approved"), DefaultValue(null), SKName("proposedPlan")] string? planJson,
|
||||
[Description("ID of the response message for planner"), DefaultValue(null), SKName("responseMessageId")] string? messageId,
|
||||
SKContext context)
|
||||
{
|
||||
// Save this new message to memory such that subsequent chat responses can use it
|
||||
await this.SaveNewMessageAsync(message, userId, userName, chatId, messageType);
|
||||
|
||||
// Clone the context to avoid modifying the original context variables.
|
||||
var chatContext = Utilities.CopyContextWithVariablesClone(context);
|
||||
chatContext.Variables.Set("knowledgeCutoff", this._promptOptions.KnowledgeCutoffDate);
|
||||
|
||||
// Check if plan exists in ask's context variables.
|
||||
// If plan was returned at this point, that means it was approved or cancelled.
|
||||
// Update the response previously saved in chat history with state
|
||||
if (!string.IsNullOrWhiteSpace(planJson) &&
|
||||
!string.IsNullOrEmpty(messageId))
|
||||
{
|
||||
await this.UpdateResponseAsync(planJson, messageId);
|
||||
}
|
||||
|
||||
var response = chatContext.Variables.ContainsKey("userCancelledPlan")
|
||||
? "I am sorry the plan did not meet your goals."
|
||||
: await this.GetChatResponseAsync(chatId, chatContext);
|
||||
|
||||
if (chatContext.ErrorOccurred)
|
||||
{
|
||||
context.Fail(chatContext.LastErrorDescription);
|
||||
return context;
|
||||
}
|
||||
|
||||
// Retrieve the prompt used to generate the response
|
||||
// and return it to the caller via the context variables.
|
||||
chatContext.Variables.TryGetValue("prompt", out string? prompt);
|
||||
prompt ??= string.Empty;
|
||||
context.Variables.Set("prompt", prompt);
|
||||
|
||||
// Save this response to memory such that subsequent chat responses can use it
|
||||
ChatMessage botMessage = await this.SaveNewResponseAsync(response, prompt, chatId);
|
||||
context.Variables.Set("messageId", botMessage.Id);
|
||||
context.Variables.Set("messageType", ((int)botMessage.Type).ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
// Extract semantic chat memory
|
||||
await SemanticChatMemoryExtractor.ExtractSemanticChatMemoryAsync(
|
||||
chatId,
|
||||
this._kernel,
|
||||
chatContext,
|
||||
this._promptOptions);
|
||||
|
||||
context.Variables.Update(response);
|
||||
return context;
|
||||
}
|
||||
|
||||
#region Private
|
||||
|
||||
/// <summary>
|
||||
/// Generate the necessary chat context to create a prompt then invoke the model to get a response.
|
||||
/// </summary>
|
||||
/// <param name="chatContext">The SKContext.</param>
|
||||
/// <returns>A response from the model.</returns>
|
||||
private async Task<string> GetChatResponseAsync(string chatId, SKContext chatContext)
|
||||
{
|
||||
// 0. Get the audience
|
||||
var audience = await this.GetAudienceAsync(chatContext);
|
||||
if (chatContext.ErrorOccurred)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// 1. Extract user intent from the conversation history.
|
||||
var userIntent = await this.GetUserIntentAsync(chatContext);
|
||||
if (chatContext.ErrorOccurred)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// 2. Calculate the remaining token budget.
|
||||
var remainingToken = this.GetChatContextTokenLimit(userIntent);
|
||||
|
||||
// 3. Acquire external information from planner
|
||||
var externalInformationTokenLimit = (int)(remainingToken * this._promptOptions.ExternalInformationContextWeight);
|
||||
var planResult = await this.AcquireExternalInformationAsync(chatContext, userIntent, externalInformationTokenLimit);
|
||||
if (chatContext.ErrorOccurred)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// If plan is suggested, send back to user for approval before running
|
||||
if (this._externalInformationSkill.ProposedPlan != null)
|
||||
{
|
||||
return JsonSerializer.Serialize<ProposedPlan>(this._externalInformationSkill.ProposedPlan);
|
||||
}
|
||||
|
||||
// 4. Query relevant semantic memories
|
||||
var chatMemoriesTokenLimit = (int)(remainingToken * this._promptOptions.MemoriesResponseContextWeight);
|
||||
var chatMemories = await this._semanticChatMemorySkill.QueryMemoriesAsync(userIntent, chatId, chatMemoriesTokenLimit, chatContext.Memory);
|
||||
if (chatContext.ErrorOccurred)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// 5. Query relevant document memories
|
||||
var documentContextTokenLimit = (int)(remainingToken * this._promptOptions.DocumentContextWeight);
|
||||
var documentMemories = await this._documentMemorySkill.QueryDocumentsAsync(userIntent, chatId, documentContextTokenLimit, chatContext.Memory);
|
||||
if (chatContext.ErrorOccurred)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// 6. Fill in the chat history if there is any token budget left
|
||||
var chatContextComponents = new List<string>() { chatMemories, documentMemories, planResult };
|
||||
var chatContextText = string.Join("\n\n", chatContextComponents.Where(c => !string.IsNullOrEmpty(c)));
|
||||
var chatContextTextTokenCount = remainingToken - Utilities.TokenCount(chatContextText);
|
||||
if (chatContextTextTokenCount > 0)
|
||||
{
|
||||
var chatHistory = await this.ExtractChatHistoryAsync(chatId, chatContextTextTokenCount);
|
||||
if (chatContext.ErrorOccurred)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
chatContextText = $"{chatContextText}\n{chatHistory}";
|
||||
}
|
||||
|
||||
// Invoke the model
|
||||
chatContext.Variables.Set("audience", audience);
|
||||
chatContext.Variables.Set("UserIntent", userIntent);
|
||||
chatContext.Variables.Set("ChatContext", chatContextText);
|
||||
|
||||
var promptRenderer = new PromptTemplateEngine();
|
||||
var renderedPrompt = await promptRenderer.RenderAsync(
|
||||
this._promptOptions.SystemChatPrompt,
|
||||
chatContext);
|
||||
|
||||
var completionFunction = this._kernel.CreateSemanticFunction(
|
||||
renderedPrompt,
|
||||
skillName: nameof(ChatSkill),
|
||||
description: "Complete the prompt.");
|
||||
|
||||
chatContext = await completionFunction.InvokeAsync(
|
||||
context: chatContext,
|
||||
settings: this.CreateChatResponseCompletionSettings()
|
||||
);
|
||||
|
||||
// Allow the caller to view the prompt used to generate the response
|
||||
chatContext.Variables.Set("prompt", renderedPrompt);
|
||||
|
||||
if (chatContext.ErrorOccurred)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return chatContext.Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function create the correct context variables to
|
||||
/// extract audience from the conversation history.
|
||||
/// </summary>
|
||||
private async Task<string> GetAudienceAsync(SKContext context)
|
||||
{
|
||||
var contextVariables = new ContextVariables();
|
||||
contextVariables.Set("chatId", context["chatId"]);
|
||||
|
||||
var audienceContext = new SKContext(
|
||||
contextVariables,
|
||||
context.Memory,
|
||||
context.Skills,
|
||||
context.Log,
|
||||
context.CancellationToken
|
||||
);
|
||||
|
||||
var audience = await this.ExtractAudienceAsync(audienceContext);
|
||||
|
||||
// Propagate the error
|
||||
if (audienceContext.ErrorOccurred)
|
||||
{
|
||||
context.Fail(audienceContext.LastErrorDescription);
|
||||
}
|
||||
|
||||
return audience;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function create the correct context variables to
|
||||
/// extract user intent from the conversation history.
|
||||
/// </summary>
|
||||
private async Task<string> GetUserIntentAsync(SKContext context)
|
||||
{
|
||||
// TODO: Regenerate user intent if plan was modified
|
||||
if (!context.Variables.TryGetValue("planUserIntent", out string? userIntent))
|
||||
{
|
||||
var contextVariables = new ContextVariables();
|
||||
contextVariables.Set("chatId", context["chatId"]);
|
||||
contextVariables.Set("audience", context["userName"]);
|
||||
|
||||
var intentContext = new SKContext(
|
||||
contextVariables,
|
||||
context.Memory,
|
||||
context.Skills,
|
||||
context.Log,
|
||||
context.CancellationToken
|
||||
);
|
||||
|
||||
userIntent = await this.ExtractUserIntentAsync(intentContext);
|
||||
// Propagate the error
|
||||
if (intentContext.ErrorOccurred)
|
||||
{
|
||||
context.Fail(intentContext.LastErrorDescription);
|
||||
}
|
||||
}
|
||||
|
||||
return userIntent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function create the correct context variables to
|
||||
/// query chat memories from the chat memory store.
|
||||
/// </summary>
|
||||
private Task<string> QueryChatMemoriesAsync(SKContext context, string userIntent, int tokenLimit)
|
||||
{
|
||||
return this._semanticChatMemorySkill.QueryMemoriesAsync(userIntent, context["chatId"], tokenLimit, context.Memory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function create the correct context variables to
|
||||
/// query document memories from the document memory store.
|
||||
/// </summary>
|
||||
private Task<string> QueryDocumentsAsync(SKContext context, string userIntent, int tokenLimit)
|
||||
{
|
||||
return this._documentMemorySkill.QueryDocumentsAsync(userIntent, context["chatId"], tokenLimit, context.Memory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function create the correct context variables to acquire external information.
|
||||
/// </summary>
|
||||
private async Task<string> AcquireExternalInformationAsync(SKContext context, string userIntent, int tokenLimit)
|
||||
{
|
||||
var contextVariables = context.Variables.Clone();
|
||||
contextVariables.Set("tokenLimit", tokenLimit.ToString(new NumberFormatInfo()));
|
||||
|
||||
var planContext = new SKContext(
|
||||
contextVariables,
|
||||
context.Memory,
|
||||
context.Skills,
|
||||
context.Log,
|
||||
context.CancellationToken
|
||||
);
|
||||
|
||||
var plan = await this._externalInformationSkill.AcquireExternalInformationAsync(userIntent, planContext);
|
||||
|
||||
// Propagate the error
|
||||
if (planContext.ErrorOccurred)
|
||||
{
|
||||
context.Fail(planContext.LastErrorDescription);
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a new message to the chat history.
|
||||
/// </summary>
|
||||
/// <param name="message">The message</param>
|
||||
/// <param name="userId">The user ID</param>
|
||||
/// <param name="userName"></param>
|
||||
/// <param name="chatId">The chat ID</param>
|
||||
/// <param name="type">Type of the message</param>
|
||||
private async Task<ChatMessage> SaveNewMessageAsync(string message, string userId, string userName, string chatId, string type)
|
||||
{
|
||||
// Make sure the chat exists.
|
||||
if (!await this._chatSessionRepository.TryFindByIdAsync(chatId, v => _ = v))
|
||||
{
|
||||
throw new ArgumentException("Chat session does not exist.");
|
||||
}
|
||||
|
||||
var chatMessage = new ChatMessage(
|
||||
userId,
|
||||
userName,
|
||||
chatId,
|
||||
message,
|
||||
"",
|
||||
ChatMessage.AuthorRoles.User,
|
||||
// Default to a standard message if the `type` is not recognized
|
||||
Enum.TryParse(type, out ChatMessage.ChatMessageType typeAsEnum) && Enum.IsDefined(typeof(ChatMessage.ChatMessageType), typeAsEnum)
|
||||
? typeAsEnum
|
||||
: ChatMessage.ChatMessageType.Message);
|
||||
|
||||
await this._chatMessageRepository.CreateAsync(chatMessage);
|
||||
return chatMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a new response to the chat history.
|
||||
/// </summary>
|
||||
/// <param name="response">Response from the chat.</param>
|
||||
/// <param name="prompt">Prompt used to generate the response.</param>
|
||||
/// <param name="chatId">The chat ID</param>
|
||||
/// <returns>The created chat message.</returns>
|
||||
private async Task<ChatMessage> SaveNewResponseAsync(string response, string prompt, string chatId)
|
||||
{
|
||||
// Make sure the chat exists.
|
||||
if (!await this._chatSessionRepository.TryFindByIdAsync(chatId, v => _ = v))
|
||||
{
|
||||
throw new ArgumentException("Chat session does not exist.");
|
||||
}
|
||||
|
||||
var chatMessage = ChatMessage.CreateBotResponseMessage(chatId, response, prompt);
|
||||
await this._chatMessageRepository.CreateAsync(chatMessage);
|
||||
|
||||
return chatMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates previously saved response in the chat history.
|
||||
/// </summary>
|
||||
/// <param name="updatedResponse">Updated response from the chat.</param>
|
||||
/// <param name="messageId">The chat message ID</param>
|
||||
private async Task UpdateResponseAsync(string updatedResponse, string messageId)
|
||||
{
|
||||
// Make sure the chat exists.
|
||||
var chatMessage = await this._chatMessageRepository.FindByIdAsync(messageId);
|
||||
chatMessage.Content = updatedResponse;
|
||||
|
||||
await this._chatMessageRepository.UpsertAsync(chatMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a completion settings object for chat response. Parameters are read from the PromptSettings class.
|
||||
/// </summary>
|
||||
private CompleteRequestSettings CreateChatResponseCompletionSettings()
|
||||
{
|
||||
var completionSettings = new CompleteRequestSettings
|
||||
{
|
||||
MaxTokens = this._promptOptions.ResponseTokenLimit,
|
||||
Temperature = this._promptOptions.ResponseTemperature,
|
||||
TopP = this._promptOptions.ResponseTopP,
|
||||
FrequencyPenalty = this._promptOptions.ResponseFrequencyPenalty,
|
||||
PresencePenalty = this._promptOptions.ResponsePresencePenalty
|
||||
};
|
||||
|
||||
return completionSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a completion settings object for intent response. Parameters are read from the PromptSettings class.
|
||||
/// </summary>
|
||||
private CompleteRequestSettings CreateIntentCompletionSettings()
|
||||
{
|
||||
var completionSettings = new CompleteRequestSettings
|
||||
{
|
||||
MaxTokens = this._promptOptions.ResponseTokenLimit,
|
||||
Temperature = this._promptOptions.IntentTemperature,
|
||||
TopP = this._promptOptions.IntentTopP,
|
||||
FrequencyPenalty = this._promptOptions.IntentFrequencyPenalty,
|
||||
PresencePenalty = this._promptOptions.IntentPresencePenalty,
|
||||
StopSequences = new string[] { "] bot:" }
|
||||
};
|
||||
|
||||
return completionSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the remaining token budget for the chat response prompt.
|
||||
/// This is the token limit minus the token count of the user intent and the system commands.
|
||||
/// </summary>
|
||||
/// <param name="userIntent">The user intent returned by the model.</param>
|
||||
/// <returns>The remaining token limit.</returns>
|
||||
private int GetChatContextTokenLimit(string userIntent)
|
||||
{
|
||||
var tokenLimit = this._promptOptions.CompletionTokenLimit;
|
||||
var remainingToken =
|
||||
tokenLimit -
|
||||
Utilities.TokenCount(userIntent) -
|
||||
this._promptOptions.ResponseTokenLimit -
|
||||
Utilities.TokenCount(string.Join("\n", new string[]
|
||||
{
|
||||
this._promptOptions.SystemDescription,
|
||||
this._promptOptions.SystemResponse,
|
||||
this._promptOptions.SystemChatContinuation
|
||||
})
|
||||
);
|
||||
|
||||
return remainingToken;
|
||||
}
|
||||
|
||||
# endregion
|
||||
}
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.Planning;
|
||||
using Microsoft.SemanticKernel.SkillDefinition;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight wrapper around a planner to allow for curating which skills are available to it.
|
||||
/// </summary>
|
||||
public class CopilotChatPlanner
|
||||
{
|
||||
/// <summary>
|
||||
/// The planner's kernel.
|
||||
/// </summary>
|
||||
public IKernel Kernel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Options for the planner.
|
||||
/// </summary>
|
||||
private readonly PlannerOptions? _plannerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pptions for the planner.
|
||||
/// </summary>
|
||||
public PlannerOptions? PlannerOptions => this._plannerOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CopilotChatPlanner"/> class.
|
||||
/// </summary>
|
||||
/// <param name="plannerKernel">The planner's kernel.</param>
|
||||
public CopilotChatPlanner(IKernel plannerKernel, PlannerOptions? plannerOptions)
|
||||
{
|
||||
this.Kernel = plannerKernel;
|
||||
this._plannerOptions = plannerOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a plan for a goal.
|
||||
/// </summary>
|
||||
/// <param name="goal">The goal to create a plan for.</param>
|
||||
/// <returns>The plan.</returns>
|
||||
public Task<Plan> CreatePlanAsync(string goal)
|
||||
{
|
||||
FunctionsView plannerFunctionsView = this.Kernel.Skills.GetFunctionsView(true, true);
|
||||
if (plannerFunctionsView.NativeFunctions.IsEmpty && plannerFunctionsView.SemanticFunctions.IsEmpty)
|
||||
{
|
||||
// No functions are available - return an empty plan.
|
||||
return Task.FromResult(new Plan(goal));
|
||||
}
|
||||
|
||||
if (this._plannerOptions?.Type == PlanType.Sequential)
|
||||
{
|
||||
return new SequentialPlanner(this.Kernel).CreatePlanAsync(goal);
|
||||
}
|
||||
|
||||
return new ActionPlanner(this.Kernel).CreatePlanAsync(goal);
|
||||
}
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.SemanticKernel.Memory;
|
||||
using Microsoft.SemanticKernel.SkillDefinition;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
|
||||
|
||||
/// <summary>
|
||||
/// This skill provides the functions to query the document memory.
|
||||
/// </summary>
|
||||
public class DocumentMemorySkill
|
||||
{
|
||||
/// <summary>
|
||||
/// Prompt settings.
|
||||
/// </summary>
|
||||
private readonly PromptsOptions _promptOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration settings for importing documents to memory.
|
||||
/// </summary>
|
||||
private readonly DocumentMemoryOptions _documentImportOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of DocumentMemorySkill.
|
||||
/// </summary>
|
||||
public DocumentMemorySkill(
|
||||
IOptions<PromptsOptions> promptOptions,
|
||||
IOptions<DocumentMemoryOptions> documentImportOptions)
|
||||
{
|
||||
this._promptOptions = promptOptions.Value;
|
||||
this._documentImportOptions = documentImportOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query the document memory collection for documents that match the query.
|
||||
/// </summary>
|
||||
/// <param name="query">Query to match.</param>
|
||||
/// <param name="context">The SkContext.</param>
|
||||
[SKFunction, Description("Query documents in the memory given a user message")]
|
||||
public async Task<string> QueryDocumentsAsync(
|
||||
[Description("Query to match.")] string query,
|
||||
[Description("ID of the chat that owns the documents")] string chatId,
|
||||
[Description("Maximum number of tokens")] int tokenLimit,
|
||||
ISemanticTextMemory textMemory)
|
||||
{
|
||||
var remainingToken = tokenLimit;
|
||||
|
||||
// Search for relevant document snippets.
|
||||
string[] documentCollections = new string[]
|
||||
{
|
||||
this._documentImportOptions.ChatDocumentCollectionNamePrefix + chatId,
|
||||
this._documentImportOptions.GlobalDocumentCollectionName
|
||||
};
|
||||
|
||||
List<MemoryQueryResult> relevantMemories = new();
|
||||
foreach (var documentCollection in documentCollections)
|
||||
{
|
||||
var results = textMemory.SearchAsync(
|
||||
documentCollection,
|
||||
query,
|
||||
limit: 100,
|
||||
minRelevanceScore: this._promptOptions.DocumentMemoryMinRelevance);
|
||||
await foreach (var memory in results)
|
||||
{
|
||||
relevantMemories.Add(memory);
|
||||
}
|
||||
}
|
||||
|
||||
relevantMemories = relevantMemories.OrderByDescending(m => m.Relevance).ToList();
|
||||
|
||||
// Concatenate the relevant document snippets.
|
||||
string documentsText = string.Empty;
|
||||
foreach (var memory in relevantMemories)
|
||||
{
|
||||
var tokenCount = Utilities.TokenCount(memory.Metadata.Text);
|
||||
if (remainingToken - tokenCount > 0)
|
||||
{
|
||||
documentsText += $"\n\nSnippet from {memory.Metadata.Description}: {memory.Metadata.Text}";
|
||||
remainingToken -= tokenCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(documentsText))
|
||||
{
|
||||
// No relevant documents found
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"User has also shared some document snippets:\n{documentsText}";
|
||||
}
|
||||
}
|
||||
-360
@@ -1,360 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
using Microsoft.SemanticKernel.Planning;
|
||||
using Microsoft.SemanticKernel.SkillDefinition;
|
||||
using SemanticKernel.Service.CopilotChat.Models;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
using SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.GitHubSkill.Model;
|
||||
using SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.JiraSkill.Model;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
|
||||
|
||||
/// <summary>
|
||||
/// This skill provides the functions to acquire external information.
|
||||
/// </summary>
|
||||
public class ExternalInformationSkill
|
||||
{
|
||||
/// <summary>
|
||||
/// Prompt settings.
|
||||
/// </summary>
|
||||
private readonly PromptsOptions _promptOptions;
|
||||
|
||||
/// <summary>
|
||||
/// CopilotChat's planner to gather additional information for the chat context.
|
||||
/// </summary>
|
||||
private readonly CopilotChatPlanner _planner;
|
||||
|
||||
/// <summary>
|
||||
/// Proposed plan to return for approval.
|
||||
/// </summary>
|
||||
public ProposedPlan? ProposedPlan { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Preamble to add to the related information text.
|
||||
/// </summary>
|
||||
private const string PromptPreamble = "[RELATED START]";
|
||||
|
||||
/// <summary>
|
||||
/// Postamble to add to the related information text.
|
||||
/// </summary>
|
||||
private const string PromptPostamble = "[RELATED END]";
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of ExternalInformationSkill.
|
||||
/// </summary>
|
||||
public ExternalInformationSkill(
|
||||
IOptions<PromptsOptions> promptOptions,
|
||||
CopilotChatPlanner planner)
|
||||
{
|
||||
this._promptOptions = promptOptions.Value;
|
||||
this._planner = planner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract relevant additional knowledge using a planner.
|
||||
/// </summary>
|
||||
[SKFunction, Description("Acquire external information")]
|
||||
[SKParameter("tokenLimit", "Maximum number of tokens")]
|
||||
[SKParameter("proposedPlan", "Previously proposed plan that is approved")]
|
||||
public async Task<string> AcquireExternalInformationAsync(
|
||||
[Description("The intent to whether external information is needed")] string userIntent,
|
||||
SKContext context)
|
||||
{
|
||||
FunctionsView functions = this._planner.Kernel.Skills.GetFunctionsView(true, true);
|
||||
if (functions.NativeFunctions.IsEmpty && functions.SemanticFunctions.IsEmpty)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Check if plan exists in ask's context variables.
|
||||
var planExists = context.Variables.TryGetValue("proposedPlan", out string? proposedPlanJson);
|
||||
var deserializedPlan = planExists && !string.IsNullOrWhiteSpace(proposedPlanJson) ? JsonSerializer.Deserialize<ProposedPlan>(proposedPlanJson) : null;
|
||||
|
||||
// Run plan if it was approved
|
||||
if (deserializedPlan != null && deserializedPlan.State == PlanState.Approved)
|
||||
{
|
||||
string planJson = JsonSerializer.Serialize(deserializedPlan.Plan);
|
||||
// Reload the plan with the planner's kernel so
|
||||
// it has full context to be executed
|
||||
var newPlanContext = new SKContext(
|
||||
null,
|
||||
this._planner.Kernel.Memory,
|
||||
this._planner.Kernel.Skills,
|
||||
this._planner.Kernel.Log
|
||||
);
|
||||
var plan = Plan.FromJson(planJson, newPlanContext);
|
||||
|
||||
// Invoke plan
|
||||
newPlanContext = await plan.InvokeAsync(newPlanContext);
|
||||
int tokenLimit =
|
||||
int.Parse(context["tokenLimit"], new NumberFormatInfo()) -
|
||||
Utilities.TokenCount(PromptPreamble) -
|
||||
Utilities.TokenCount(PromptPostamble);
|
||||
|
||||
// The result of the plan may be from an OpenAPI skill. Attempt to extract JSON from the response.
|
||||
bool extractJsonFromOpenApi =
|
||||
this.TryExtractJsonFromOpenApiPlanResult(newPlanContext, newPlanContext.Result, out string planResult);
|
||||
if (extractJsonFromOpenApi)
|
||||
{
|
||||
planResult = this.OptimizeOpenApiSkillJson(planResult, tokenLimit, plan);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If not, use result of the plan execution result directly.
|
||||
planResult = newPlanContext.Variables.Input;
|
||||
}
|
||||
|
||||
return $"{PromptPreamble}\n{planResult.Trim()}\n{PromptPostamble}\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a plan and set it in context for approval.
|
||||
var contextString = string.Join("\n", context.Variables.Where(v => v.Key != "userIntent").Select(v => $"{v.Key}: {v.Value}"));
|
||||
Plan plan = await this._planner.CreatePlanAsync($"Given the following context, accomplish the user intent.\nContext:{contextString}\nUser Intent:{userIntent}");
|
||||
|
||||
if (plan.Steps.Count > 0)
|
||||
{
|
||||
// Parameters stored in plan's top level
|
||||
this.MergeContextIntoPlan(context.Variables, plan.Parameters);
|
||||
|
||||
// TODO: Improve Kernel to give developers option to skip this override
|
||||
// (i.e., keep functions regardless of whether they're available in the planner's context or not)
|
||||
Plan sanitizedPlan = this.SanitizePlan(plan, context);
|
||||
sanitizedPlan.Parameters.Update(plan.Parameters);
|
||||
|
||||
this.ProposedPlan = new ProposedPlan(sanitizedPlan, this._planner.PlannerOptions!.Type, PlanState.NoOp);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
#region Private
|
||||
|
||||
/// <summary>
|
||||
/// Scrubs plan of functions not available in Planner's kernel.
|
||||
/// </summary>
|
||||
private Plan SanitizePlan(Plan plan, SKContext context)
|
||||
{
|
||||
List<Plan> sanitizedSteps = new();
|
||||
var availableFunctions = this._planner.Kernel.Skills.GetFunctionsView(true);
|
||||
|
||||
foreach (var step in plan.Steps)
|
||||
{
|
||||
if (this._planner.Kernel.Skills.TryGetFunction(step.SkillName, step.Name, out var function))
|
||||
{
|
||||
this.MergeContextIntoPlan(context.Variables, step.Parameters);
|
||||
sanitizedSteps.Add(step);
|
||||
}
|
||||
}
|
||||
|
||||
return new Plan(plan.Description, sanitizedSteps.ToArray<Plan>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge any variables from the context into plan parameters as these will be used on plan execution.
|
||||
/// These context variables come from user input, so they are prioritized.
|
||||
/// </summary>
|
||||
private void MergeContextIntoPlan(ContextVariables variables, ContextVariables planParams)
|
||||
{
|
||||
foreach (var param in planParams)
|
||||
{
|
||||
if (param.Key.Equals("INPUT", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variables.TryGetValue(param.Key, out string? value))
|
||||
{
|
||||
planParams.Set(param.Key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to extract json from the planner response as if it were from an OpenAPI skill.
|
||||
/// </summary>
|
||||
private bool TryExtractJsonFromOpenApiPlanResult(SKContext context, string openApiSkillResponse, out string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
JsonNode? jsonNode = JsonNode.Parse(openApiSkillResponse);
|
||||
string contentType = jsonNode?["contentType"]?.ToString() ?? string.Empty;
|
||||
if (contentType.StartsWith("application/json", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var content = jsonNode?["content"]?.ToString() ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
json = content;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
context.Log.LogDebug("Unable to extract JSON from planner response, it is likely not from an OpenAPI skill.");
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
context.Log.LogDebug("Unable to extract JSON from planner response, it may already be proper JSON.");
|
||||
}
|
||||
|
||||
json = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to optimize json from the planner response
|
||||
/// based on token limit
|
||||
/// </summary>
|
||||
private string OptimizeOpenApiSkillJson(string jsonContent, int tokenLimit, Plan plan)
|
||||
{
|
||||
// Remove all new line characters + leading and trailing white space
|
||||
jsonContent = Regex.Replace(jsonContent.Trim(), @"[\n\r]", string.Empty);
|
||||
var document = JsonDocument.Parse(jsonContent);
|
||||
string lastSkillInvoked = plan.Steps[^1].SkillName;
|
||||
string lastSkillFunctionInvoked = plan.Steps[^1].Name;
|
||||
bool trimSkillResponse = false;
|
||||
|
||||
// The json will be deserialized based on the response type of the particular operation that was last invoked by the planner
|
||||
// The response type can be a custom trimmed down json structure, which is useful in staying within the token limit
|
||||
Type skillResponseType = this.GetOpenApiSkillResponseType(ref document, ref lastSkillInvoked, ref lastSkillFunctionInvoked, ref trimSkillResponse);
|
||||
|
||||
if (trimSkillResponse)
|
||||
{
|
||||
// Deserializing limits the json content to only the fields defined in the respective OpenApiSkill's Model classes
|
||||
var skillResponse = JsonSerializer.Deserialize(jsonContent, skillResponseType);
|
||||
jsonContent = skillResponse != null ? JsonSerializer.Serialize(skillResponse) : string.Empty;
|
||||
document = JsonDocument.Parse(jsonContent);
|
||||
}
|
||||
|
||||
int jsonContentTokenCount = Utilities.TokenCount(jsonContent);
|
||||
|
||||
// Return the JSON content if it does not exceed the token limit
|
||||
if (jsonContentTokenCount < tokenLimit)
|
||||
{
|
||||
return jsonContent;
|
||||
}
|
||||
|
||||
List<object> itemList = new();
|
||||
|
||||
// Some APIs will return a JSON response with one property key representing an embedded answer.
|
||||
// Extract this value for further processing
|
||||
string resultsDescriptor = "";
|
||||
|
||||
if (document.RootElement.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
int propertyCount = 0;
|
||||
foreach (JsonProperty property in document.RootElement.EnumerateObject())
|
||||
{
|
||||
propertyCount++;
|
||||
}
|
||||
|
||||
if (propertyCount == 1)
|
||||
{
|
||||
// Save property name for result interpolation
|
||||
JsonProperty firstProperty = document.RootElement.EnumerateObject().First();
|
||||
tokenLimit -= Utilities.TokenCount(firstProperty.Name);
|
||||
resultsDescriptor = string.Format(CultureInfo.InvariantCulture, "{0}: ", firstProperty.Name);
|
||||
|
||||
// Extract object to be truncated
|
||||
JsonElement value = firstProperty.Value;
|
||||
document = JsonDocument.Parse(value.GetRawText());
|
||||
}
|
||||
}
|
||||
|
||||
// Detail Object
|
||||
// To stay within token limits, attempt to truncate the list of properties
|
||||
if (document.RootElement.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (JsonProperty property in document.RootElement.EnumerateObject())
|
||||
{
|
||||
int propertyTokenCount = Utilities.TokenCount(property.ToString());
|
||||
|
||||
if (tokenLimit - propertyTokenCount > 0)
|
||||
{
|
||||
itemList.Add(property);
|
||||
tokenLimit -= propertyTokenCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary (List) Object
|
||||
// To stay within token limits, attempt to truncate the list of results
|
||||
if (document.RootElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (JsonElement item in document.RootElement.EnumerateArray())
|
||||
{
|
||||
int itemTokenCount = Utilities.TokenCount(item.ToString());
|
||||
|
||||
if (tokenLimit - itemTokenCount > 0)
|
||||
{
|
||||
itemList.Add(item);
|
||||
tokenLimit -= itemTokenCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return itemList.Count > 0
|
||||
? string.Format(CultureInfo.InvariantCulture, "{0}{1}", resultsDescriptor, JsonSerializer.Serialize(itemList))
|
||||
: string.Format(CultureInfo.InvariantCulture, "JSON response for {0} is too large to be consumed at this time.", lastSkillInvoked);
|
||||
}
|
||||
|
||||
private Type GetOpenApiSkillResponseType(ref JsonDocument document, ref string lastSkillInvoked, ref string lastSkillFunctionInvoked, ref bool trimSkillResponse)
|
||||
{
|
||||
Type skillResponseType = typeof(object); // Use a reasonable default response type
|
||||
|
||||
// Different operations under the skill will return responses as json structures;
|
||||
// Prune each operation response according to the most important/contextual fields only to avoid going over the token limit
|
||||
// Check what the last skill invoked was and deserialize the JSON content accordingly
|
||||
if (string.Equals(lastSkillInvoked, "GitHubSkill", StringComparison.Ordinal))
|
||||
{
|
||||
trimSkillResponse = true;
|
||||
skillResponseType = this.GetGithubSkillResponseType(ref document);
|
||||
}
|
||||
else if (string.Equals(lastSkillInvoked, "JiraSkill", StringComparison.Ordinal))
|
||||
{
|
||||
trimSkillResponse = true;
|
||||
skillResponseType = this.GetJiraSkillResponseType(ref document, ref lastSkillFunctionInvoked);
|
||||
}
|
||||
|
||||
return skillResponseType;
|
||||
}
|
||||
|
||||
private Type GetGithubSkillResponseType(ref JsonDocument document)
|
||||
{
|
||||
return document.RootElement.ValueKind == JsonValueKind.Array ? typeof(PullRequest[]) : typeof(PullRequest);
|
||||
}
|
||||
|
||||
private Type GetJiraSkillResponseType(ref JsonDocument document, ref string lastSkillFunctionInvoked)
|
||||
{
|
||||
if (lastSkillFunctionInvoked == "GetIssue")
|
||||
{
|
||||
return document.RootElement.ValueKind == JsonValueKind.Array ? typeof(IssueResponse[]) : typeof(IssueResponse);
|
||||
}
|
||||
|
||||
return typeof(IssueResponse);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
|
||||
|
||||
/// <summary>
|
||||
/// A collection of semantic chat memory.
|
||||
/// </summary>
|
||||
public class SemanticChatMemory
|
||||
{
|
||||
/// <summary>
|
||||
/// The chat memory items.
|
||||
/// </summary>
|
||||
[JsonPropertyName("items")]
|
||||
public List<SemanticChatMemoryItem> Items { get; set; } = new List<SemanticChatMemoryItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Create and add a chat memory item.
|
||||
/// </summary>
|
||||
/// <param name="label">Label for the chat memory item.</param>
|
||||
/// <param name="details">Details for the chat memory item.</param>
|
||||
public void AddItem(string label, string details)
|
||||
{
|
||||
this.Items.Add(new SemanticChatMemoryItem(label, details));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the chat memory to a Json string.
|
||||
/// </summary>
|
||||
/// <returns>A Json string representing the chat memory.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a semantic chat memory from a Json string.
|
||||
/// </summary>
|
||||
/// <param name="json">Json string to deserialize.</param>
|
||||
/// <returns>A semantic chat memory.</returns>
|
||||
public static SemanticChatMemory FromJson(string json)
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<SemanticChatMemory>(json);
|
||||
return result ?? throw new ArgumentException("Failed to deserialize chat memory to json.");
|
||||
}
|
||||
}
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.SemanticKernel;
|
||||
using Microsoft.SemanticKernel.AI.TextCompletion;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
using SemanticKernel.Service.CopilotChat.Extensions;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to extract and create semantic memory from chat history.
|
||||
/// </summary>
|
||||
internal static class SemanticChatMemoryExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the name of the semantic text memory collection that stores chat semantic memory.
|
||||
/// </summary>
|
||||
/// <param name="chatId">Chat ID that is persistent and unique for the chat session.</param>
|
||||
/// <param name="memoryName">Name of the memory category</param>
|
||||
internal static string MemoryCollectionName(string chatId, string memoryName) => $"{chatId}-{memoryName}";
|
||||
|
||||
/// <summary>
|
||||
/// Extract and save semantic memory.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The Chat ID.</param>
|
||||
/// <param name="kernel">The semantic kernel.</param>
|
||||
/// <param name="context">The context containing the memory.</param>
|
||||
/// <param name="options">The prompts options.</param>
|
||||
internal static async Task ExtractSemanticChatMemoryAsync(
|
||||
string chatId,
|
||||
IKernel kernel,
|
||||
SKContext context,
|
||||
PromptsOptions options)
|
||||
{
|
||||
foreach (var memoryName in options.MemoryMap.Keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
var semanticMemory = await ExtractCognitiveMemoryAsync(
|
||||
memoryName,
|
||||
kernel,
|
||||
context,
|
||||
options
|
||||
);
|
||||
foreach (var item in semanticMemory.Items)
|
||||
{
|
||||
await CreateMemoryAsync(item, chatId, context, memoryName, options);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsCriticalException())
|
||||
{
|
||||
// Skip semantic memory extraction for this item if it fails.
|
||||
// We cannot rely on the model to response with perfect Json each time.
|
||||
context.Log.LogInformation("Unable to extract semantic memory for {0}: {1}. Continuing...", memoryName, ex.Message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the semantic chat memory from the chat session.
|
||||
/// </summary>
|
||||
/// <param name="memoryName">Name of the memory category</param>
|
||||
/// <param name="kernel">The semantic kernel.</param>
|
||||
/// <param name="context">The SKContext</param>
|
||||
/// <param name="options">The prompts options.</param>
|
||||
/// <returns>A SemanticChatMemory object.</returns>
|
||||
internal static async Task<SemanticChatMemory> ExtractCognitiveMemoryAsync(
|
||||
string memoryName,
|
||||
IKernel kernel,
|
||||
SKContext context,
|
||||
PromptsOptions options)
|
||||
{
|
||||
if (!options.MemoryMap.TryGetValue(memoryName, out var memoryPrompt))
|
||||
{
|
||||
throw new ArgumentException($"Memory name {memoryName} is not supported.");
|
||||
}
|
||||
|
||||
// Token limit for chat history
|
||||
var tokenLimit = options.CompletionTokenLimit;
|
||||
var remainingToken =
|
||||
tokenLimit -
|
||||
options.ResponseTokenLimit -
|
||||
Utilities.TokenCount(memoryPrompt); ;
|
||||
|
||||
var memoryExtractionContext = Utilities.CopyContextWithVariablesClone(context);
|
||||
memoryExtractionContext.Variables.Set("tokenLimit", remainingToken.ToString(new NumberFormatInfo()));
|
||||
memoryExtractionContext.Variables.Set("memoryName", memoryName);
|
||||
memoryExtractionContext.Variables.Set("format", options.MemoryFormat);
|
||||
memoryExtractionContext.Variables.Set("knowledgeCutoff", options.KnowledgeCutoffDate);
|
||||
|
||||
var completionFunction = kernel.CreateSemanticFunction(memoryPrompt);
|
||||
var result = await completionFunction.InvokeAsync(
|
||||
context: memoryExtractionContext,
|
||||
settings: CreateMemoryExtractionSettings(options)
|
||||
);
|
||||
|
||||
SemanticChatMemory memory = SemanticChatMemory.FromJson(result.ToString());
|
||||
return memory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a memory item in the memory collection.
|
||||
/// If there is already a memory item that has a high similarity score with the new item, it will be skipped.
|
||||
/// </summary>
|
||||
/// <param name="item">A SemanticChatMemoryItem instance</param>
|
||||
/// <param name="chatId">The ID of the chat the memories belong to</param>
|
||||
/// <param name="context">The context that contains the memory</param>
|
||||
/// <param name="memoryName">Name of the memory</param>
|
||||
/// <param name="options">The prompts options.</param>
|
||||
internal static async Task CreateMemoryAsync(
|
||||
SemanticChatMemoryItem item,
|
||||
string chatId,
|
||||
SKContext context,
|
||||
string memoryName,
|
||||
PromptsOptions options)
|
||||
{
|
||||
var memoryCollectionName = SemanticChatMemoryExtractor.MemoryCollectionName(chatId, memoryName);
|
||||
|
||||
var memories = await context.Memory.SearchAsync(
|
||||
collection: memoryCollectionName,
|
||||
query: item.ToFormattedString(),
|
||||
limit: 1,
|
||||
minRelevanceScore: options.SemanticMemoryMinRelevance,
|
||||
cancellationToken: context.CancellationToken
|
||||
)
|
||||
.ToListAsync()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (memories.Count == 0)
|
||||
{
|
||||
await context.Memory.SaveInformationAsync(
|
||||
collection: memoryCollectionName,
|
||||
text: item.ToFormattedString(),
|
||||
id: Guid.NewGuid().ToString(),
|
||||
description: memoryName,
|
||||
cancellationToken: context.CancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a completion settings object for chat response. Parameters are read from the PromptSettings class.
|
||||
/// </summary>
|
||||
private static CompleteRequestSettings CreateMemoryExtractionSettings(PromptsOptions options)
|
||||
{
|
||||
var completionSettings = new CompleteRequestSettings
|
||||
{
|
||||
MaxTokens = options.ResponseTokenLimit,
|
||||
Temperature = options.ResponseTemperature,
|
||||
TopP = options.ResponseTopP,
|
||||
FrequencyPenalty = options.ResponseFrequencyPenalty,
|
||||
PresencePenalty = options.ResponsePresencePenalty
|
||||
};
|
||||
|
||||
return completionSettings;
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
|
||||
|
||||
/// <summary>
|
||||
/// A single entry in the chat memory.
|
||||
/// </summary>
|
||||
public class SemanticChatMemoryItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Label for the chat memory item.
|
||||
/// </summary>
|
||||
[JsonPropertyName("label")]
|
||||
public string Label { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Details for the chat memory item.
|
||||
/// </summary>
|
||||
[JsonPropertyName("details")]
|
||||
public string Details { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new chat memory item.
|
||||
/// </summary>
|
||||
/// <param name="label">Label of the item.</param>
|
||||
/// <param name="details">Details of the item.</param>
|
||||
public SemanticChatMemoryItem(string label, string details)
|
||||
{
|
||||
this.Label = label;
|
||||
this.Details = details;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format the chat memory item as a string.
|
||||
/// </summary>
|
||||
/// <returns>A formatted string representing the item.</returns>
|
||||
public string ToFormattedString()
|
||||
{
|
||||
return $"{this.Label}: {this.Details}";
|
||||
}
|
||||
}
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.SemanticKernel.Memory;
|
||||
using Microsoft.SemanticKernel.SkillDefinition;
|
||||
using SemanticKernel.Service.CopilotChat.Options;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.ChatSkills;
|
||||
|
||||
/// <summary>
|
||||
/// This skill provides the functions to query the semantic chat memory.
|
||||
/// </summary>
|
||||
public class SemanticChatMemorySkill
|
||||
{
|
||||
/// <summary>
|
||||
/// Prompt settings.
|
||||
/// </summary>
|
||||
private readonly PromptsOptions _promptOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of SemanticChatMemorySkill.
|
||||
/// </summary>
|
||||
public SemanticChatMemorySkill(
|
||||
IOptions<PromptsOptions> promptOptions)
|
||||
{
|
||||
this._promptOptions = promptOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query relevant memories based on the query.
|
||||
/// </summary>
|
||||
/// <param name="query">Query to match.</param>
|
||||
/// <param name="context">The SKContext</param>
|
||||
/// <returns>A string containing the relevant memories.</returns>
|
||||
[SKFunction, Description("Query chat memories")]
|
||||
public async Task<string> QueryMemoriesAsync(
|
||||
[Description("Query to match.")] string query,
|
||||
[Description("Chat ID to query history from")] string chatId,
|
||||
[Description("Maximum number of tokens")] int tokenLimit,
|
||||
ISemanticTextMemory textMemory)
|
||||
{
|
||||
var remainingToken = tokenLimit;
|
||||
|
||||
// Search for relevant memories.
|
||||
List<MemoryQueryResult> relevantMemories = new();
|
||||
foreach (var memoryName in this._promptOptions.MemoryMap.Keys)
|
||||
{
|
||||
var results = textMemory.SearchAsync(
|
||||
SemanticChatMemoryExtractor.MemoryCollectionName(chatId, memoryName),
|
||||
query,
|
||||
limit: 100,
|
||||
minRelevanceScore: this._promptOptions.SemanticMemoryMinRelevance);
|
||||
await foreach (var memory in results)
|
||||
{
|
||||
relevantMemories.Add(memory);
|
||||
}
|
||||
}
|
||||
|
||||
relevantMemories = relevantMemories.OrderByDescending(m => m.Relevance).ToList();
|
||||
|
||||
string memoryText = "";
|
||||
foreach (var memory in relevantMemories)
|
||||
{
|
||||
var tokenCount = Utilities.TokenCount(memory.Metadata.Text);
|
||||
if (remainingToken - tokenCount > 0)
|
||||
{
|
||||
memoryText += $"\n[{memory.Metadata.Description}] {memory.Metadata.Text}";
|
||||
remainingToken -= tokenCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(memoryText))
|
||||
{
|
||||
// No relevant memories found
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return $"Past memories (format: [memory type] <label>: <details>):\n{memoryText.Trim()}";
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.GitHubSkill.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pull request label.
|
||||
/// </summary>
|
||||
public class Label
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the label.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the label.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of the label.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Label"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the label.</param>
|
||||
/// <param name="name">The name of the label.</param>
|
||||
/// <param name="description">The description of the label.</param>
|
||||
public Label(long id, string name, string description)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
}
|
||||
}
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.GitHubSkill.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a GitHub Pull Request.
|
||||
/// </summary>
|
||||
public class PullRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the URL of the pull request
|
||||
/// </summary>
|
||||
[JsonPropertyName("url")]
|
||||
public System.Uri Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the unique identifier of the pull request
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of the pull request
|
||||
/// </summary>
|
||||
[JsonPropertyName("number")]
|
||||
public int Number { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state of the pull request
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public string State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the pull request is locked
|
||||
/// </summary>
|
||||
[JsonPropertyName("locked")]
|
||||
public bool Locked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title of the pull request
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user who created the pull request
|
||||
/// </summary>
|
||||
[JsonPropertyName("user")]
|
||||
public GitHubUser User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the labels associated with the pull request
|
||||
/// </summary>
|
||||
[JsonPropertyName("labels")]
|
||||
public List<Label> Labels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the pull request was last updated
|
||||
/// </summary>
|
||||
[JsonPropertyName("updated_at")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the pull request was closed
|
||||
/// </summary>
|
||||
[JsonPropertyName("closed_at")]
|
||||
public DateTime? ClosedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the pull request was merged
|
||||
/// </summary>
|
||||
[JsonPropertyName("merged_at")]
|
||||
public DateTime? MergedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PullRequest"/> class, representing a pull request on GitHub.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the pull request.</param>
|
||||
/// <param name="id">The unique identifier of the pull request.</param>
|
||||
/// <param name="number">The number of the pull request within the repository.</param>
|
||||
/// <param name="state">The state of the pull request, such as "open", "closed", or "merged".</param>
|
||||
/// <param name="locked">A value indicating whether the pull request is locked for comments or changes.</param>
|
||||
/// <param name="title">The title of the pull request.</param>
|
||||
/// <param name="user">The user who created the pull request.</param>
|
||||
/// <param name="labels">A list of labels assigned to the pull request.</param>
|
||||
/// <param name="updatedAt">The date and time when the pull request was last updated.</param>
|
||||
/// <param name="closedAt">The date and time when the pull request was closed, or null if it is not closed.</param>
|
||||
/// <param name="mergedAt">The date and time when the pull request was merged, or null if it is not merged.</param>
|
||||
public PullRequest(
|
||||
System.Uri url,
|
||||
int id,
|
||||
int number,
|
||||
string state,
|
||||
bool locked,
|
||||
string title,
|
||||
GitHubUser user,
|
||||
List<Label> labels,
|
||||
DateTime updatedAt,
|
||||
DateTime? closedAt,
|
||||
DateTime? mergedAt
|
||||
)
|
||||
{
|
||||
this.Url = url;
|
||||
this.Id = id;
|
||||
this.Number = number;
|
||||
this.State = state;
|
||||
this.Locked = locked;
|
||||
this.Title = title;
|
||||
this.User = user;
|
||||
this.Labels = labels;
|
||||
this.UpdatedAt = updatedAt;
|
||||
this.ClosedAt = closedAt;
|
||||
this.MergedAt = mergedAt;
|
||||
}
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.GitHubSkill.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a GitHub Repo.
|
||||
/// </summary>
|
||||
public class Repo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the repo
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the full name of the repo
|
||||
/// </summary>
|
||||
[JsonPropertyName("full_name")]
|
||||
public string FullName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Repo"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the repository, e.g. "dotnet/runtime".</param>
|
||||
/// <param name="fullName">The full name of the repository, e.g. "Microsoft/dotnet/runtime".</param>
|
||||
public Repo(string name, string fullName)
|
||||
{
|
||||
this.Name = name;
|
||||
this.FullName = fullName;
|
||||
}
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.GitHubSkill.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user on GitHub.
|
||||
/// </summary>
|
||||
public class GitHubUser
|
||||
{
|
||||
/// <summary>
|
||||
/// The user's name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's email address.
|
||||
/// </summary>
|
||||
[JsonPropertyName("email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's numeric ID.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's type, e.g. User or Organization.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user is a site admin.
|
||||
/// </summary>
|
||||
[JsonPropertyName("site_admin")]
|
||||
public bool SiteAdmin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the User class.
|
||||
/// </summary>
|
||||
/// <param name="name">The user's name.</param>
|
||||
/// <param name="email">The user's email address.</param>
|
||||
/// <param name="id">The user's numeric ID.</param>
|
||||
/// <param name="type">The user's type.</param>
|
||||
/// <param name="siteAdmin">Whether the user is a site admin.</param>
|
||||
public GitHubUser(string name, string email, int id, string type, bool siteAdmin)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Email = email;
|
||||
this.Id = id;
|
||||
this.Type = type;
|
||||
this.SiteAdmin = siteAdmin;
|
||||
}
|
||||
}
|
||||
-5935
File diff suppressed because it is too large
Load Diff
-26
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.JiraSkill.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the Author of a comment.
|
||||
/// </summary>
|
||||
public class CommentAuthor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Comment Author's display name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("displayName")]
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CommentAuthor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="displayName">Name of Author</param>
|
||||
public CommentAuthor(string displayName)
|
||||
{
|
||||
this.DisplayName = displayName;
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.JiraSkill.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a the list of comments that make up a CommentResponse.
|
||||
/// </summary>
|
||||
public class CommentResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of all comments contained in this comment response.
|
||||
/// </summary>
|
||||
[JsonPropertyName("comments")]
|
||||
public IEnumerable<IndividualComments> AllComments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CommentResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="allComments">List of all comments on the Issue.</param>
|
||||
public CommentResponse(IEnumerable<IndividualComments> allComments)
|
||||
{
|
||||
this.AllComments = allComments;
|
||||
}
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.JiraSkill.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an individual comment on an issue in jira.
|
||||
/// </summary>
|
||||
public class IndividualComments
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the body of the comment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("body")]
|
||||
public string Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the author name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("author")]
|
||||
public CommentAuthor Author { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IndividualComments"/> class.
|
||||
/// </summary>
|
||||
/// <param name="body">The actual content of the comment.</param>
|
||||
/// <param name="author">Author of the comment.</param>
|
||||
public IndividualComments(string body, CommentAuthor author)
|
||||
{
|
||||
this.Body = body;
|
||||
this.Author = author;
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.JiraSkill.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a the trimmed down response for retrieving an issue from jira.
|
||||
/// </summary>
|
||||
public class IssueResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the GUID of the issue.
|
||||
/// </summary>
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the issue key, which is a readable id different from the GUID above.
|
||||
/// </summary>
|
||||
[JsonPropertyName("key")]
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the url of the issue.
|
||||
/// </summary>
|
||||
[JsonPropertyName("self")]
|
||||
public string Self { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Fields describing the IssueResponse.
|
||||
/// </summary>
|
||||
[JsonPropertyName("fields")]
|
||||
public IssueResponseFields Fields { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IssueResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The GUID of the Issue.</param>
|
||||
/// <param name="key">The readable id of the Issue.</param>
|
||||
/// <param name="self">The url of the Issue.</param>
|
||||
/// <param name="fields">The Fields that make up the response body.</param>
|
||||
public IssueResponse(string id, string key, string self, IssueResponseFields fields)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Key = key;
|
||||
this.Self = self;
|
||||
this.Fields = fields;
|
||||
}
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills.OpenApiSkills.JiraSkill.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the fields that make up an IssueResponse.
|
||||
/// </summary>
|
||||
public class IssueResponseFields
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the label.
|
||||
/// </summary>
|
||||
[JsonPropertyName("statuscategorychangedate")]
|
||||
public string StatusCategoryChangeDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the label.
|
||||
/// </summary>
|
||||
[JsonPropertyName("summary")]
|
||||
public string Summary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of the label.
|
||||
/// </summary>
|
||||
[JsonPropertyName("parent")]
|
||||
public IssueResponse Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of the label.
|
||||
/// </summary>
|
||||
[JsonPropertyName("comment")]
|
||||
public CommentResponse CommentResponse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IssueResponseFields"/> class.
|
||||
/// </summary>
|
||||
/// <param name="statusCategoryChangeDate">The date time the issue was last changed.</param>
|
||||
/// <param name="summary">The Summary of the issue.</param>
|
||||
/// <param name="parent">The Parent of the issue.</param>
|
||||
/// <param name="commentResponse">List of all comments on the issue.</param>
|
||||
public IssueResponseFields(string statusCategoryChangeDate, string summary, IssueResponse parent, CommentResponse commentResponse)
|
||||
{
|
||||
this.StatusCategoryChangeDate = statusCategoryChangeDate;
|
||||
this.Summary = summary;
|
||||
this.Parent = parent;
|
||||
this.CommentResponse = commentResponse;
|
||||
}
|
||||
}
|
||||
-1251
File diff suppressed because it is too large
Load Diff
@@ -1,26 +0,0 @@
|
||||
# OpenAPI Skills
|
||||
|
||||
## GitHubSkill
|
||||
The OpenAPI spec at `./GitHubSkill/openapi.json` defines the APIs for GitHub operations
|
||||
[List Pull Requests](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests) and
|
||||
[Get Pull Request](https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request).
|
||||
This description is extracted from GitHub's official OpenAPI descriptions for their REST APIs, which can be found in
|
||||
[this repository](https://github.com/github/rest-api-description/blob/main/descriptions/ghec/ghec.2022-11-28.json).
|
||||
|
||||
## KlarnaSkill
|
||||
The OpenAPI spec at `./KlarnaSkill/openapi.json` defines the APIs for Klarna Shopping's ChatGPT AI plugin operations.
|
||||
This definition was retrieved using Klarna's official ChatGPT plugin hosted at https://www.klarna.com/.well-known/ai-plugin.json.
|
||||
Serving the OpenAPI definition from the repo is a workaround for Klarna's ChatGPT plugin sometimes returning a 403 when requested from CopilotChat.
|
||||
|
||||
## JiraSkill
|
||||
The Power Platform Connector/OpenAPI spec at `./JiraSkill/openapi.json` defines the APIs for Jira's operations.
|
||||
This definition was retrieved using the Jira Power Platform Certified Connector OpenAPI definition, version 2.0 (not version 3.0), hosted at https://github.com/microsoft/PowerPlatformConnectors/blob/dev/certified-connectors/JIRA/apiDefinition.swagger.json .
|
||||
Serving the OpenAPI definition from the repo is a workaround to use version 2.0 for the swagger document which is the version currently supported by the semantic kernel.
|
||||
This version however doesn't follow OpenAPI specification for all of its operations.
|
||||
For example CreateIssueV2, its body param does not describe properties and so we can't build the body automatically.
|
||||
Version 3.0 of jira's swagger document is hosted at https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json.
|
||||
|
||||
# Model Classes
|
||||
|
||||
OpenApi skills return responses as json structures, these responses can vary based on the functions that are eventually invoked. Some responses are very big and go over the token limit of the underlying language model. To work around the token limit we can specifically prune the json response before passing that information back to the language model.
|
||||
The Model classes under `.\webapi\Skills\OpenApiSkills\<SkillName>\Model\` determine which json properties are picked for deserializing the json response; Only the json properties defined via the Model classes remain in the trimmed response passed back to the language model.
|
||||
@@ -1,32 +0,0 @@
|
||||
//Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.SemanticKernel.Connectors.AI.OpenAI.Tokenizers;
|
||||
using Microsoft.SemanticKernel.Orchestration;
|
||||
|
||||
namespace SemanticKernel.Service.CopilotChat.Skills;
|
||||
|
||||
/// <summary>
|
||||
/// Utility methods for skills.
|
||||
/// </summary>
|
||||
internal static class Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new context with a clone of the variables from the given context.
|
||||
/// This is useful when you want to modify the variables in a context without
|
||||
/// affecting the original context.
|
||||
/// </summary>
|
||||
/// <param name="context">The context to copy.</param>
|
||||
/// <returns>A new context with a clone of the variables.</returns>
|
||||
internal static SKContext CopyContextWithVariablesClone(SKContext context)
|
||||
=> new(
|
||||
context.Variables.Clone(),
|
||||
context.Memory,
|
||||
context.Skills,
|
||||
context.Log,
|
||||
context.CancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the number of tokens in a string.
|
||||
/// </summary>
|
||||
internal static int TokenCount(string text) => GPT3Tokenizer.Encode(text).Count;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user