diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..e002d7ebc
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+**/node_modules
+**/bin
+**/obj
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
index 1a05f3eb4..6021d364e 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,6 +8,10 @@ root = true
indent_style = space
end_of_line = lf
+# Bash scripts
+[*.sh]
+indent_size = 2
+
# XML project files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
indent_size = 2
diff --git a/.github/_typos.toml b/.github/_typos.toml
index 8298df765..779f5a254 100644
--- a/.github/_typos.toml
+++ b/.github/_typos.toml
@@ -15,6 +15,7 @@ extend-exclude = [
"GPT3TokenizerTests.cs",
"CodeTokenizerTests.cs",
"test_code_tokenizer.py",
+ "CopilotChat.sln.DotSettings"
]
[default.extend-words]
diff --git a/.github/labeler.yml b/.github/labeler.yml
index c430e2da5..c60b4a01c 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,3 +1,29 @@
-# Add 'documentation' label to any '.md' files
+# Add 'webapp' label to any change within the 'webapp' directory
+webapp:
+ - changed-files:
+ - any-glob-to-any-file: ["webapp/**"]
+
+# Add 'webapi' label to any change within the 'webapi' directory
+webapi:
+ - changed-files:
+ - any-glob-to-any-file: ["webapi/**"]
+
+# Add 'dependencies' label to any change of the 'webapp/yarn.lock' file
+dependencies:
+ - changed-files:
+ - any-glob-to-any-file: ["webapp/yarn.lock"]
+
+# Add 'deployment' label to any change within the 'deploy' directory
+deployment:
+ - changed-files:
+ - any-glob-to-any-file: ["scripts/deploy/**"]
+
+# Add 'documentation' label to any change of '.md' files
documentation:
- - '**/*.md'
\ No newline at end of file
+ - changed-files:
+ - any-glob-to-any-file: ["**/*.md"]
+
+# Add 'github actions' label to any change within the '.github/workflows' directory
+"github actions":
+ - changed-files:
+ - any-glob-to-any-file: [".github/workflows/**"]
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index f5b95b7cf..b083a7b3c 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,6 +1,6 @@
### Motivation and Context
-
- [ ] The code builds clean without any errors or warnings
-- [ ] The PR follows the [Contribution Guidelines](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md#dev-scripts) raises no violations
+- [ ] The PR follows the [Contribution Guidelines](https://github.com/microsoft/chat-copilot/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/chat-copilot/blob/main/CONTRIBUTING.md#development-scripts) raises no violations
- [ ] All unit tests pass, and I have added new tests where possible
- [ ] I didn't break anyone :smile:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 6ae3a7a79..3e96860c2 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -6,9 +6,9 @@ name: "CodeQL"
on:
push:
- branches: [ "main", "experimental*", "feature*" ]
+ branches: ["main", "experimental*", "feature*"]
schedule:
- - cron: '17 11 * * 2'
+ - cron: "17 11 * * 2"
jobs:
analyze:
@@ -22,45 +22,44 @@ jobs:
strategy:
fail-fast: false
matrix:
- language: [ 'csharp', 'python' ]
+ language: ["csharp", "javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- - name: Checkout repository
- uses: actions/checkout@v3
+ - name: Checkout repository
+ uses: actions/checkout@v4
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
- # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
- # queries: security-extended,security-and-quality
+ # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+ # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
- # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+ # If the Autobuild fails above, remove it and uncomment the following three lines.
+ # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
- # If the Autobuild fails above, remove it and uncomment the following three lines.
- # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
+ # - run: |
+ # echo "Run, Build Application using script"
+ # ./location_of_script_within_repo/buildscript.sh
- # - run: |
- # echo "Run, Build Application using script"
- # ./location_of_script_within_repo/buildscript.sh
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
- with:
- category: "/language:${{matrix.language}}"
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/copilot-build-backend.yml b/.github/workflows/copilot-build-backend.yml
new file mode 100644
index 000000000..50b2c0961
--- /dev/null
+++ b/.github/workflows/copilot-build-backend.yml
@@ -0,0 +1,70 @@
+name: copilot-build-backend
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: ["main"]
+ paths:
+ - "webapi/**"
+ workflow_call:
+ outputs:
+ artifact:
+ description: "The name of the uploaded web api artifact."
+ value: ${{jobs.webapi.outputs.artifact}}
+
+permissions:
+ contents: read
+
+jobs:
+ webapi:
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - { dotnet: "8.0", configuration: Release, os: windows-latest }
+
+ runs-on: ${{ matrix.os }}
+
+ env:
+ NUGET_CERT_REVOCATION_MODE: offline
+
+ outputs:
+ artifact: ${{steps.artifactoutput.outputs.artifactname}}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ clean: true
+ fetch-depth: 0
+
+ - name: Install GitVersion
+ uses: gittools/actions/gitversion/setup@v2
+ with:
+ versionSpec: "5.x"
+
+ - name: Determine version
+ id: gitversion
+ uses: gittools/actions/gitversion/execute@v2
+
+ - name: Set version tag
+ id: versiontag
+ run: |
+ $VERSION_TAG = "${{ steps.gitversion.outputs.Major }}."
+ $VERSION_TAG += "${{ steps.gitversion.outputs.Minor }}."
+ $VERSION_TAG += "${{ steps.gitversion.outputs.CommitsSinceVersionSource }}"
+ echo $VERSION_TAG
+ Write-Output "versiontag=$VERSION_TAG" >> $env:GITHUB_OUTPUT
+
+ - name: Package Copilot Chat WebAPI
+ run: |
+ scripts\deploy\package-webapi.ps1 -Configuration Release -DotnetFramework net8.0 -TargetRuntime win-x64 -OutputDirectory ${{ github.workspace }}\scripts\deploy -Version ${{ steps.versiontag.outputs.versiontag }} -InformationalVersion "Built from commit ${{ steps.gitversion.outputs.ShortSha }} on $(Get-Date -Format 'yyyy-MM-dd')" -SkipFrontendFiles ('${{ github.event_name == 'pull_request' }}' -eq 'true')
+
+ - name: Upload package to artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: copilotchat-webapi-${{ steps.versiontag.outputs.versiontag }}
+ path: ${{ github.workspace }}\scripts\deploy\out\webapi.zip
+
+ - name: "Set outputs"
+ id: artifactoutput
+ run: Write-Output "artifactname=copilotchat-webapi-${{ steps.versiontag.outputs.versiontag }}" >> $env:GITHUB_OUTPUT
diff --git a/.github/workflows/copilot-build-frontend.yml b/.github/workflows/copilot-build-frontend.yml
new file mode 100644
index 000000000..9a5b5b577
--- /dev/null
+++ b/.github/workflows/copilot-build-frontend.yml
@@ -0,0 +1,35 @@
+name: copilot-build-frontend
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: ["main"]
+ paths:
+ - "webapp/**"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ webapp:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Use Node.js 18
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18
+ cache: "yarn"
+ cache-dependency-path: "webapp/yarn.lock"
+
+ - name: Run yarn install, yarn build, & yarn format
+ run: |
+ #!/usr/bin/env bash
+ set -e # exit with nonzero exit code if anything fails
+ echo "Running yarn install, yarn build, & yarn format"
+ yarn install --frozen-lockfile # install dependencies and ensure lockfile is unchanged.
+ yarn build # run build script
+ yarn format # run format script
+ working-directory: webapp
diff --git a/.github/workflows/copilot-build-images.yml b/.github/workflows/copilot-build-images.yml
new file mode 100644
index 000000000..698fb346d
--- /dev/null
+++ b/.github/workflows/copilot-build-images.yml
@@ -0,0 +1,80 @@
+name: copilot-build-images
+
+on:
+ workflow_call:
+ inputs:
+ REACT_APP_BACKEND_URI:
+ required: true
+ type: string
+ AZURE_FUNCTION_MASTER_KEY:
+ required: true
+ type: string
+env:
+ REGISTRY: ghcr.io
+
+jobs:
+ build-and-push-image:
+ name: Build and push images
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - file: ./docker/webapi/Dockerfile
+ image: ${{ github.repository }}-webapi
+ build-args: |
+
+ - file: ./docker/webapp/Dockerfile
+ image: ${{ github.repository }}-webapp
+ build-args: |
+
+ - file: ./docker/webapp/Dockerfile.nginx
+ image: ${{ github.repository }}-webapp-nginx
+ build-args: |
+ REACT_APP_BACKEND_URI=${{ inputs.REACT_APP_BACKEND_URI }}
+
+ - file: ./docker/plugins/web-searcher/Dockerfile
+ image: ${{ github.repository }}-web-searcher
+ build-args: |
+ AZURE_FUNCTION_MASTER_KEY=${{ inputs.AZURE_FUNCTION_MASTER_KEY }}
+
+ - file: ./docker/memorypipeline/Dockerfile
+ image: ${{ github.repository }}-memorypipeline
+ build-args: |
+
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Login container registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels)
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ matrix.image }}
+ tags: |
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}
+ type=semver,pattern={{major}}.{{minor}}
+
+ - name: Build and push image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ${{ matrix.file }}
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ build-args: ${{ matrix.build-args }}
diff --git a/.github/workflows/copilot-build-memorypipeline.yml b/.github/workflows/copilot-build-memorypipeline.yml
new file mode 100644
index 000000000..69efd66bc
--- /dev/null
+++ b/.github/workflows/copilot-build-memorypipeline.yml
@@ -0,0 +1,69 @@
+name: copilot-build-memorypipeline
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: ["main"]
+ paths:
+ - "memorypipeline/**"
+ workflow_call:
+ outputs:
+ artifact:
+ description: "The name of the uploaded memory pipeline artifact."
+ value: ${{jobs.memory-pipeline.outputs.artifact}}
+
+permissions:
+ contents: read
+
+jobs:
+ memory-pipeline:
+ runs-on: windows-latest
+
+ env:
+ NUGET_CERT_REVOCATION_MODE: offline
+
+ outputs:
+ artifact: ${{steps.artifactoutput.outputs.artifactname}}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ clean: true
+ fetch-depth: 0
+
+ - name: Install GitVersion
+ uses: gittools/actions/gitversion/setup@v2
+ with:
+ versionSpec: "5.x"
+
+ - name: Determine version
+ id: gitversion
+ uses: gittools/actions/gitversion/execute@v2
+
+ - name: Set version tag
+ id: versiontag
+ run: |
+ $VERSION_TAG = "${{ steps.gitversion.outputs.Major }}."
+ $VERSION_TAG += "${{ steps.gitversion.outputs.Minor }}."
+ $VERSION_TAG += "${{ steps.gitversion.outputs.CommitsSinceVersionSource }}"
+ echo $VERSION_TAG
+ Write-Output "versiontag=$VERSION_TAG" >> $env:GITHUB_OUTPUT
+
+ - name: Set .Net Core version
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Package Copilot Chat Memory Pipeline
+ run: |
+ scripts\deploy\package-memorypipeline.ps1 -Configuration Release -DotnetFramework net8.0 -TargetRuntime win-x64 -OutputDirectory ${{ github.workspace }}\scripts\deploy -Version ${{ steps.versiontag.outputs.versiontag }} -InformationalVersion "Built from commit ${{ steps.gitversion.outputs.ShortSha }} on $(Get-Date -Format "yyyy-MM-dd")"
+
+ - name: Upload package to artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: copilotchat-memorypipeline-${{ steps.versiontag.outputs.versiontag }}
+ path: ${{ github.workspace }}\scripts\deploy\out\memorypipeline.zip
+
+ - name: "Set outputs"
+ id: artifactoutput
+ run: Write-Output "artifactname=copilotchat-memorypipeline-${{ steps.versiontag.outputs.versiontag }}" >> $env:GITHUB_OUTPUT
diff --git a/.github/workflows/copilot-build-plugins.yml b/.github/workflows/copilot-build-plugins.yml
new file mode 100644
index 000000000..c7027f9e1
--- /dev/null
+++ b/.github/workflows/copilot-build-plugins.yml
@@ -0,0 +1,74 @@
+name: copilot-build-plugins
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: ["main"]
+ paths:
+ - "plugins/**"
+ workflow_call:
+ outputs:
+ artifact:
+ description: "The name of the uploaded plugin artifacts."
+ value: ${{jobs.plugins.outputs.artifact}}
+
+permissions:
+ contents: read
+
+jobs:
+ plugins:
+ runs-on: windows-latest
+
+ env:
+ NUGET_CERT_REVOCATION_MODE: offline
+
+ outputs:
+ artifact: ${{steps.artifactoutput.outputs.artifactname}}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ clean: true
+ fetch-depth: 0
+
+ - name: Install GitVersion
+ uses: gittools/actions/gitversion/setup@v2
+ with:
+ versionSpec: "5.x"
+
+ - name: Determine version
+ id: gitversion
+ uses: gittools/actions/gitversion/execute@v2
+
+ - name: Set version tag
+ id: versiontag
+ run: |
+ $VERSION_TAG = "${{ steps.gitversion.outputs.Major }}."
+ $VERSION_TAG += "${{ steps.gitversion.outputs.Minor }}."
+ $VERSION_TAG += "${{ steps.gitversion.outputs.CommitsSinceVersionSource }}"
+ echo $VERSION_TAG
+ Write-Output "versiontag=$VERSION_TAG" >> $env:GITHUB_OUTPUT
+
+ - name: Set .Net Core version
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Package Copilot Chat Plugins
+ run: |
+ scripts\deploy\package-plugins.ps1 `
+ -BuildConfiguration Release `
+ -DotNetFramework net8.0 `
+ -OutputDirectory ${{ github.workspace }}\scripts\deploy `
+ -Version ${{ steps.versiontag.outputs.versiontag }} `
+ -InformationalVersion "Built from commit ${{ steps.gitversion.outputs.ShortSha }} on $(Get-Date -Format "yyyy-MM-dd")"
+
+ - name: Upload packages to artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: copilotchat-plugins-${{ steps.versiontag.outputs.versiontag }}
+ path: ${{ github.workspace }}\scripts\deploy\out\plugins
+
+ - name: "Set outputs"
+ id: artifactoutput
+ run: Write-Output "artifactname=copilotchat-plugins-${{ steps.versiontag.outputs.versiontag }}" >> $env:GITHUB_OUTPUT
diff --git a/.github/workflows/copilot-chat-package.yml b/.github/workflows/copilot-chat-package.yml
deleted file mode 100644
index 5b999b089..000000000
--- a/.github/workflows/copilot-chat-package.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-#
-# This workflow will package the Copilot Chat application for deployment.
-#
-
-name: copilot-chat-package
-
-on:
- pull_request:
- branches: [ "main" ]
- merge_group:
- branches: [ "main" ]
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- copilot-chat-package:
- 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)/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/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: ./deploy/publish
diff --git a/.github/workflows/copilot-run-integration-tests.yml b/.github/workflows/copilot-run-integration-tests.yml
new file mode 100644
index 000000000..9b00c4f87
--- /dev/null
+++ b/.github/workflows/copilot-run-integration-tests.yml
@@ -0,0 +1,47 @@
+name: copilot-run-integration-tests
+
+on:
+ workflow_dispatch:
+ inputs:
+ BACKEND_HOST:
+ required: true
+ type: string
+ ENVIRONMENT:
+ required: true
+ type: string
+ workflow_call:
+ inputs:
+ BACKEND_HOST:
+ required: true
+ type: string
+ ENVIRONMENT:
+ required: true
+ type: string
+
+permissions:
+ contents: read
+
+jobs:
+ tests:
+ environment: ${{inputs.ENVIRONMENT}}
+ name: Integration Testing
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Configure test environment
+ working-directory: integration-tests
+ env:
+ TestUsername: ${{secrets.COPILOT_CHAT_TEST_USER_ACCOUNT1}}
+ TestPassword: ${{secrets.COPILOT_CHAT_TEST_USER_PASSWORD1}}
+ run: |
+ dotnet user-secrets set "BaseServerUrl" "https://${{inputs.BACKEND_HOST}}.azurewebsites.net/"
+ dotnet user-secrets set "Authority" "https://login.microsoftonline.com/${{vars.APPLICATION_TENANT_ID}}"
+ dotnet user-secrets set "ClientID" ${{vars.APPLICATION_CLIENT_ID}}
+ dotnet user-secrets set "Scopes" "openid, offline_access, profile, api://${{vars.BACKEND_CLIENT_ID}}/access_as_user"
+ # dotnet user-secrets set "TestUsername" "$env:TestUsername"
+ # dotnet user-secrets set "TestPassword" "$env:TestPassword"
+
+ - name: Run integration tests
+ run: dotnet test --logger trx
diff --git a/.github/workflows/copilot-chat-tests.yml b/.github/workflows/copilot-test-e2e.yml
similarity index 58%
rename from .github/workflows/copilot-chat-tests.yml
rename to .github/workflows/copilot-test-e2e.yml
index a30324e4d..14751b39d 100644
--- a/.github/workflows/copilot-chat-tests.yml
+++ b/.github/workflows/copilot-test-e2e.yml
@@ -1,32 +1,36 @@
name: Copilot Chat Tests
on:
workflow_dispatch:
+ inputs:
+ ENVIRONMENT:
+ required: true
+ type: string
merge_group:
- branches: ["main"]
+ types: [checks_requested]
permissions:
contents: read
jobs:
- test:
+ e2e:
+ environment: ${{inputs.ENVIRONMENT}}
defaults:
run:
working-directory: webapp
runs-on: ubuntu-latest
- timeout-minutes: 30
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
- node-version: 16
+ node-version: 18
cache-dependency-path: webapp/yarn.lock
cache: "yarn"
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: 6.0.x
+ dotnet-version: 8.0.x
- name: Install dependencies
run: yarn install
@@ -38,38 +42,51 @@ jobs:
working-directory: 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"
+ dotnet user-secrets set "KernelMemory:Services:AzureOpenAIText:APIKey" "$AzureOpenAI__ApiKey"
+ dotnet user-secrets set "KernelMemory:Services:AzureOpenAIText:Endpoint" ${{vars.AZURE_OPENAI_ENDPOINT}}
+ dotnet user-secrets set "KernelMemory:Services:AzureOpenAIEmbedding:APIKey" "$AzureOpenAI__ApiKey"
+ dotnet user-secrets set "KernelMemory:Services:AzureOpenAIEmbedding:Endpoint" ${{vars.AZURE_OPENAI_ENDPOINT}}
+ dotnet user-secrets set "Authentication:Type" "AzureAd"
+ dotnet user-secrets set "Authentication:AzureAd:TenantId" ${{vars.APPLICATION_TENANT_ID}}
+ dotnet user-secrets set "Authentication:AzureAd:ClientId" ${{vars.BACKEND_CLIENT_ID}}
+ dotnet user-secrets set "Frontend:AadClientId" ${{vars.APPLICATION_CLIENT_ID}}
- name: Start service in background
working-directory: webapi
run: |
dotnet run > service-log.txt 2>&1 &
+
+ STARTED=false;
+
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';
+ STARTED=true;
break;
fi;
sleep 5;
done
+ if [ "$STARTED" = false ]; then
+ echo 'Service failed to start';
+ exit 1;
+ fi
+
- name: Run Playwright tests
+ timeout-minutes: 5
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_ACCOUNT1_INITIALS: ${{ secrets.COPILOT_CHAT_TEST_USER_ACCOUNT1_INITIALS }}
REACT_APP_TEST_USER_ACCOUNT2: ${{ secrets.COPILOT_CHAT_TEST_USER_ACCOUNT2 }}
- REACT_APP_TEST_USER_PASSWORD: ${{ secrets.COPILOT_CHAT_TEST_USER_PASSWORD }}
+ REACT_APP_TEST_USER_PASSWORD1: ${{ secrets.COPILOT_CHAT_TEST_USER_PASSWORD1 }}
+ 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 }}
@@ -80,14 +97,14 @@ jobs:
REACT_APP_TEST_GITHUB_REPOSITORY_NAME: ${{ secrets.COPILOT_CHAT_TEST_GITHUB_REPOSITORY_NAME }}
run: yarn playwright test
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: webapp/playwright-report/
retention-days: 30
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
if: always()
with:
name: service-log
diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml
new file mode 100644
index 000000000..2f56019f2
--- /dev/null
+++ b/.github/workflows/dotnet-format.yml
@@ -0,0 +1,75 @@
+name: dotnet-format
+
+on:
+ pull_request:
+ branches: ["main", "feature*"]
+ paths:
+ - "webapi/**"
+ - "memorypipeline/**"
+ - "tools/**"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ check-format:
+ runs-on: ubuntu-latest
+ env:
+ NUGET_CERT_REVOCATION_MODE: offline
+
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+ with:
+ clean: true
+
+ - name: Get changed files
+ id: changed-files
+ uses: jitterbit/get-changed-files@v1
+ continue-on-error: true
+
+ - name: No C# files changed
+ id: no-csharp
+ if: steps.changed-files.outputs.added_modified == ''
+ run: echo "No C# files changed"
+
+ # This step will loop over the changed files and find the nearest .csproj file for each one, then store the unique csproj files in a variable
+ - name: Find csproj files
+ id: find-csproj
+ run: |
+ csproj_files=()
+ if [[ ${{ steps.changed-files.outcome }} == 'success' ]]; then
+ for file in ${{ steps.changed-files.outputs.added_modified }}; do
+ echo "$file was changed"
+ dir="./$file"
+ while [[ $dir != "." && $dir != "/" && $dir != $GITHUB_WORKSPACE ]]; do
+ if find "$dir" -maxdepth 1 -name "*.csproj" -print -quit | grep -q .; then
+ csproj_files+=("$(find "$dir" -maxdepth 1 -name "*.csproj" -print -quit)")
+ break
+ fi
+
+ dir=$(echo ${dir%/*})
+ done
+ done
+ else
+ # if the changed-files step failed, run dotnet on the whole sln instead of specific projects
+ echo "Running dotnet format on whole solution"
+ csproj_files=$(find ./ -type f -name "*.sln" | tr '\n' ' ');
+ fi
+ csproj_files=($(printf "%s\n" "${csproj_files[@]}" | sort -u))
+ echo "Found ${#csproj_files[@]} unique csproj/sln files: ${csproj_files[*]}"
+ echo "csproj_files=${csproj_files[*]}" >> $GITHUB_OUTPUT
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Check formatting
+ if: steps.find-csproj.outputs.csproj_files != ''
+ run: |
+ for csproj in ${{ steps.find-csproj.outputs.csproj_files }}; do
+ echo "Checking formatting for $csproj"
+ dotnet format $csproj --verify-no-changes --verbosity diagnostic
+ done
diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml
new file mode 100644
index 000000000..dd5f87a14
--- /dev/null
+++ b/.github/workflows/label-pr.yml
@@ -0,0 +1,22 @@
+# This workflow will triage pull requests and apply a label based on the
+# paths that are modified in the pull request.
+#
+# To use this workflow, you will need to set up a .github/labeler.yml
+# file with configuration. For more information, see:
+# https://github.com/actions/labeler
+
+name: Label pull request
+on: [pull_request_target]
+
+jobs:
+ add_label:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+
+ steps:
+ - uses: actions/labeler@v5
+ with:
+ dot: true # allows for easier matching of paths
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/markdown-link-check-config.json b/.github/workflows/markdown-link-check-config.json
index 777edfe3f..1044cc6e5 100644
--- a/.github/workflows/markdown-link-check-config.json
+++ b/.github/workflows/markdown-link-check-config.json
@@ -25,18 +25,15 @@
"pattern": "^https://localhost"
},
{
- "pattern": "https://github.com/microsoft/copilot-chat/releases"
+ "pattern": "^https://platform.openai.com"
+ },
+ {
+ "pattern": "^https://stackoverflow.com"
}
],
"timeout": "20s",
"retryOn429": true,
"retryCount": 3,
"fallbackRetryDelay": "30s",
- "aliveStatusCodes": [
- 200,
- 206,
- 429,
- 500,
- 503
- ]
+ "aliveStatusCodes": [200, 206, 429, 500, 503]
}
diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml
index 00826fe75..4e02c7256 100644
--- a/.github/workflows/markdown-link-check.yml
+++ b/.github/workflows/markdown-link-check.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
# check out the latest version of the code
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
# Checks the status of hyperlinks in .md files in verbose mode
- name: Check links
diff --git a/.github/workflows/node-pr.yml b/.github/workflows/node-pr.yml
deleted file mode 100644
index 9b4066c26..000000000
--- a/.github/workflows/node-pr.yml
+++ /dev/null
@@ -1,70 +0,0 @@
-# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
-# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
-
-name: node-pr
-
-on:
- workflow_dispatch:
- pull_request:
- branches: ["main"]
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
-
-jobs:
- find-yarn-projects:
- runs-on: ubuntu-latest
- outputs:
- matrix: ${{ steps.set-yarn-folders.outputs.matrix }}
-
- steps:
- - uses: actions/checkout@v3
-
- - name: Find yarn projects
- id: set-yarn-folders
- # This step uses a bash script to find all subfolders that contain a yarn.lock file
- run: |
- #!/bin/bash
- set -e # exit with nonzero exit code if anything fails
- shopt -s globstar # enable globstar option to use ** for recursive matching
- yarndirs=()
- for lockfile in **/yarn.lock; do # loop over all yarn.lock files
- dir=$(dirname "$lockfile") # get the directory of the lock file
- echo "Found yarn project in $dir"
- yarndirs+=("$dir") # add the directory to the yarndirs array
- done
-
- echo "All yarn projects found: '${yarndirs[*]}'"
- yarndirs_json=$(echo -n "${yarndirs[*]%\n}" | jq -R -s -j --compact-output 'split(" ")')
- matrix_json="{\"node_version\":[18], \"yarn_folder\":$yarndirs_json}"
- echo "Setting output matrix to $matrix_json"
- echo "matrix=$matrix_json" >> $GITHUB_OUTPUT
-
- build:
- runs-on: ubuntu-latest
- needs: find-yarn-projects
-
- strategy:
- matrix: ${{ fromJson(needs.find-yarn-projects.outputs.matrix) }}
-
- steps:
- - uses: actions/checkout@v3
- - name: Use Node.js ${{ matrix.node_version }}
- uses: actions/setup-node@v3
- with:
- node-version: ${{ matrix.node_version }}
- cache: "yarn"
- cache-dependency-path: "**/yarn.lock"
-
- - name: Run yarn install & yarn build
- # This step runs yarn install and yarn build for each project.
- # The --frozen-lockfile option ensures that the dependencies are installed exactly as specified in the lock file.
- # The -cwd option sets the current working directory to the folder where the yarn.lock file is located.
- run: |
- #!/bin/bash
- set -e # exit with nonzero exit code if anything fails
- dir=${{ matrix.yarn_folder }} # get the directory of the lock file
- echo "Running yarn install and yarn build for $dir"
- yarn --cwd "$dir" install --frozen-lockfile # install dependencies
- yarn --cwd "$dir" build # run build script
diff --git a/.github/workflows/typos.yaml b/.github/workflows/typos.yaml
index 1d2b0df54..94931d48b 100644
--- a/.github/workflows/typos.yaml
+++ b/.github/workflows/typos.yaml
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Use custom config file
uses: crate-ci/typos@master
diff --git a/.github/workflows/update-version.sh b/.github/workflows/update-version.sh
deleted file mode 100644
index 5db3622f6..000000000
--- a/.github/workflows/update-version.sh
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/bin/bash
-
-POSITIONAL_ARGS=()
-
-while [[ $# -gt 0 ]]; do
- case $1 in
- -f|--file)
- file="$2"
- shift # past argument
- shift # past value
- ;;
- -p|--propsFile)
- propsFile="$2"
- shift # past argument
- shift # past value
- ;;
- -b|--buildAndRevisionNumber)
- buildAndRevisionNumber="$2"
- shift # past argument
- shift # past value
- ;;
- -*|--*)
- echo "Unknown option $1"
- exit 1
- ;;
- *)
- POSITIONAL_ARGS+=("$1") # save positional arg
- shift # past argument
- ;;
- esac
-done
-
-set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
-
-if [ -z "$file" ]; then
- echo "ERROR: Parameter file (-f|--file) not provided"
- exit 1;
-elif [ ! -f "$file" ]; then
- echo "ERROR: file ${file} not found"
- exit 1;
-fi
-
-if [ -n "$(cat $file | grep -i "false")" ]; then
- echo "Project is marked as NOT packable - skipping."
- exit 0;
-fi
-
-if [ -z "$propsFile" ]; then
- echo "ERROR: Parameter propsFile (-f|--file) not provided"
- exit 1;
-elif [ ! -f "$propsFile" ]; then
- echo "ERROR: propsFile ${file} not found"
- exit 1;
-fi
-
-if [ -z "$buildAndRevisionNumber" ]; then
- echo "ERROR: Parameter buildAndRevisionNumber (-b|--buildAndRevisionNumber) not provided"
- exit 1;
-fi
-
-propsVersionString=$(cat $propsFile | grep -i "");
-regex="([0-9.]*)<\/Version>"
-if [[ $propsVersionString =~ $regex ]]; then
- propsVersion=${BASH_REMATCH[1]}
-else
- echo "ERROR: Version tag not found in propsFile"
- exit 1;
-fi
-
-if [ -z "$propsVersion" ]; then
- echo "ERROR: Version tag not found in propsFile"
- exit 1;
-elif [[ ! "$propsVersion" =~ ^0.* ]]; then
- echo "ERROR: Version expected to start with 0. Actual: ${propsVersion}"
- exit 1;
-fi
-
-fullVersionString="${propsVersion}.${buildAndRevisionNumber}-preview"
-
-if [[ ! "$fullVersionString" =~ ^0.* ]]; then
- echo "ERROR: Version expected to start with 0. Actual: ${fullVersionString}"
- exit 1;
-fi
-
-echo "==== Project: ${file} ====";
-echo "propsFile = ${propsFile}"
-echo "buildAndRevisionNumber = ${buildAndRevisionNumber}"
-echo "version prefix from propsFile = ${propsVersion}"
-echo "full version string: ${fullVersionString}"
-
-versionInProj=$(cat $file | grep -i "");
-if [ -n "$versionInProj" ]; then
- # Version tag already exists in the csproj. Let's replace it.
- echo "Updating version tag..."
- content=$(cat $file | sed --expression="s/\([0-9]*.[0-9]*\)<\/Version>/$fullVersionString<\/Version>/g");
-else
- # Version tag not found in the csproj. Let's add it.
- echo "Project is packable - adding version tag..."
- content=$(cat $file | sed --expression="s/<\/Project>/$fullVersionString<\/Version><\/PropertyGroup><\/Project>/g");
-fi
-
-if [ $? -ne 0 ]; then exit 1; fi
-echo "$content" && echo "$content" > $file;
-if [ $? -ne 0 ]; then exit 1; fi
-
-echo "DONE";
-echo "";
diff --git a/.gitignore b/.gitignore
index 530b14105..bba535498 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,8 @@ dotnet/.config
*.user
*.userosscache
*.sln.docstates
+webapi/data/*.json
+__dev/
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
@@ -410,6 +412,7 @@ FodyWeavers.xsd
*.pem
.env
+.env.production
certs/
launchSettings.json
config.development.yaml
@@ -476,5 +479,24 @@ playwright-report/
# Static Web App deployment config
swa-cli.config.json
-**/copilot-chat-app/webapp/build
-**/copilot-chat-app/webapp/node_modules
\ No newline at end of file
+webapp/build/
+webapp/node_modules/
+
+# Custom plugin files used in webapp for testing
+webapp/public/.well-known*
+
+# Auto-generated solution file from Visual Studio
+webapi/CopilotChatWebApi.sln
+
+# Files created for deployments
+/deploy/
+
+# Tesseract OCR language data files
+*.traineddata
+
+# Semantic Memory local storage
+tmp/*
+
+# Custom plugins in default directories
+*/Skills/NativePlugins
+*/Skills/SemanticPlugins
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000..abd8d87a0
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,8 @@
+{
+ "recommendations": [
+ "ms-dotnettools.csharp",
+ "esbenp.prettier-vscode",
+ "dbaeumer.vscode-eslint",
+ "ms-semantic-kernel.semantic-kernel",
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..8495ea143
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,17 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+
+ {
+ "name": "Debug CopilotChatWebApi",
+ "type": "coreclr",
+ "preLaunchTask": "build (CopilotChatWebApi)",
+ "request": "launch",
+ "program": "${workspaceFolder}/webapi/bin/Debug/net6.0/CopilotChatWebApi.dll",
+ "cwd": "${workspaceFolder}/webapi",
+ "env": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..00085e410
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,54 @@
+{
+ "prettier.enable": true,
+ "editor.formatOnType": true,
+ "editor.formatOnSave": true,
+ "editor.formatOnPaste": true,
+ "[csharp]": {
+ "editor.defaultFormatter": "ms-dotnettools.csharp",
+ "editor.codeActionsOnSave": {
+ "source.fixAll": true
+ }
+ },
+ "editor.bracketPairColorization.enabled": true,
+ "editor.guides.bracketPairs": "active",
+ "javascript.updateImportsOnFileMove.enabled": "always",
+ "search.exclude": {
+ "**/node_modules": true,
+ "**/build": true
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.codeActionsOnSave": {
+ "source.organizeImports": true,
+ "source.fixAll": true
+ }
+ },
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.codeActionsOnSave": {
+ "source.organizeImports": true,
+ "source.fixAll": true
+ }
+ },
+ "typescript.updateImportsOnFileMove.enabled": "always",
+ "eslint.enable": true,
+ "eslint.lintTask.enable": true,
+ "eslint.workingDirectories": [
+ {
+ "mode": "auto"
+ }
+ ],
+ "files.associations": {
+ "*.json": "jsonc"
+ },
+ "files.exclude": {
+ "**/.git": true,
+ "**/.hg": true,
+ "**/CVS": true,
+ "**/.DS_Store": true,
+ "**/Thumbs.db": true,
+ "**.lock": true,
+ },
+ "dotnet.defaultSolution": "CopilotChat.sln",
+ "editor.tabSize": 2,
+ }
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index e5940b7ea..e7c9534aa 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -83,7 +83,7 @@
"type": "process",
"args": [
"build",
- "${workspaceFolder}/CopilotChatWebApi.csproj",
+ "${workspaceFolder}/webapi/CopilotChatWebApi.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"/property:DebugType=portable"
@@ -131,4 +131,4 @@
"password": true
}
]
-}
+}
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index db9b7d214..090b63e4a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,7 +11,7 @@ tips on how you can make reporting your issue as effective as possible.
### Where to Report
-New issues can be reported in our [list of issues](https://github.com/microsoft/copilot-chat/issues).
+New issues can be reported in our [list of issues](https://github.com/microsoft/chat-copilot/issues).
Before filing a new issue, please search the list of issues to make sure it does
not already exist.
@@ -91,7 +91,7 @@ We use and recommend the following workflow:
"issue-123" or "githubhandle-issue".
4. Make and commit your changes to your branch.
5. Add new tests corresponding to your change, if applicable.
-6. Run the relevant scripts in [the section below](https://github.com/microsoft/copilot-chat/blob/main/CONTRIBUTING.md#dev-scripts) to ensure that your build is clean and all tests are passing.
+6. Run the relevant scripts in [the section below](https://github.com/microsoft/chat-copilot/blob/main/CONTRIBUTING.md#dev-scripts) to ensure that your build is clean and all tests are passing.
7. Create a PR against the repository's **main** branch.
- State in the description what issue or improvement your change is addressing.
- Verify that all the Continuous Integration checks are passing.
diff --git a/CopilotChat.sln b/CopilotChat.sln
index 1135cca43..e79eb97a4 100644
--- a/CopilotChat.sln
+++ b/CopilotChat.sln
@@ -1,10 +1,21 @@
-
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
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CopilotChatMemoryPipeline", "memorypipeline\CopilotChatMemoryPipeline.csproj", "{EE1C907E-E90A-4AB5-9094-6CCE5F0DEDF8}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImportDocument", "tools\importdocument\ImportDocument.csproj", "{F67EF16F-9A8A-4DC6-A9A1-E64CDFE4F285}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChatCopilotIntegrationTests", "integration-tests\ChatCopilotIntegrationTests.csproj", "{0CD2CD95-536B-455F-B74A-772A455FA607}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CopilotChatShared", "shared\CopilotChatShared.csproj", "{94F12185-FAF9-43E3-B153-28A1708AC918}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebSearcher", "plugins\web-searcher\WebSearcher.csproj", "{F83C857D-3080-4DEA-B3D1-978E2BC64BFB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PluginShared", "plugins\shared\PluginShared.csproj", "{9D03913A-21FF-4D0A-9883-95C4B3D6F65A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +26,30 @@ Global
{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
+ {EE1C907E-E90A-4AB5-9094-6CCE5F0DEDF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EE1C907E-E90A-4AB5-9094-6CCE5F0DEDF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EE1C907E-E90A-4AB5-9094-6CCE5F0DEDF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EE1C907E-E90A-4AB5-9094-6CCE5F0DEDF8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F67EF16F-9A8A-4DC6-A9A1-E64CDFE4F285}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F67EF16F-9A8A-4DC6-A9A1-E64CDFE4F285}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F67EF16F-9A8A-4DC6-A9A1-E64CDFE4F285}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F67EF16F-9A8A-4DC6-A9A1-E64CDFE4F285}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0CD2CD95-536B-455F-B74A-772A455FA607}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0CD2CD95-536B-455F-B74A-772A455FA607}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0CD2CD95-536B-455F-B74A-772A455FA607}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0CD2CD95-536B-455F-B74A-772A455FA607}.Release|Any CPU.Build.0 = Release|Any CPU
+ {94F12185-FAF9-43E3-B153-28A1708AC918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {94F12185-FAF9-43E3-B153-28A1708AC918}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {94F12185-FAF9-43E3-B153-28A1708AC918}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {94F12185-FAF9-43E3-B153-28A1708AC918}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F83C857D-3080-4DEA-B3D1-978E2BC64BFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F83C857D-3080-4DEA-B3D1-978E2BC64BFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F83C857D-3080-4DEA-B3D1-978E2BC64BFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F83C857D-3080-4DEA-B3D1-978E2BC64BFB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9D03913A-21FF-4D0A-9883-95C4B3D6F65A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9D03913A-21FF-4D0A-9883-95C4B3D6F65A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9D03913A-21FF-4D0A-9883-95C4B3D6F65A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9D03913A-21FF-4D0A-9883-95C4B3D6F65A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CopilotChat.sln.DotSettings b/CopilotChat.sln.DotSettings
new file mode 100644
index 000000000..d1a28aa42
--- /dev/null
+++ b/CopilotChat.sln.DotSettings
@@ -0,0 +1,305 @@
+
+ False
+ False
+ True
+ True
+ FullFormat
+ True
+ True
+ True
+ True
+ True
+ SOLUTION
+ False
+ SUGGESTION
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ SUGGESTION
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ ERROR
+ True
+ Field, Property, Event, Method
+ True
+ True
+ True
+ NEXT_LINE
+ True
+ True
+ True
+ True
+ True
+ True
+ 1
+ 1
+ True
+ True
+ True
+ ALWAYS
+ True
+ True
+ False
+ 512
+ True
+ Copyright (c) Microsoft. All rights reserved.
+ ACS
+ AI
+ AIGPT
+ AMQP
+ API
+ BOM
+ CORS
+ DB
+ DI
+ GPT
+ GRPC
+ HMAC
+ HTTP
+ IM
+ IO
+ IOS
+ JSON
+ JWT
+ MQ
+ MQTT
+ MS
+ MSAL
+ OCR
+ OID
+ OK
+ OS
+ PR
+ QA
+ SHA
+ SK
+ SKHTTP
+ SSL
+ TTL
+ UI
+ UID
+ URL
+ XML
+ YAML
+ False
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="Async" Style="aaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="Async" Style="aaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy>
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local variables"><ElementKinds><Kind Name="LOCAL_VARIABLE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="Async" Style="aaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local functions"><ElementKinds><Kind Name="LOCAL_FUNCTION" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Methods"><ElementKinds><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Parameters"><ElementKinds><Kind Name="PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="Async" Style="aaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Properties"><ElementKinds><Kind Name="PROPERTY" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="Async" Style="AaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" /></Policy>
+
+ 2
+ False
+ True
+ Console
+ PushToShowHints
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ False
+ TRACE
+ 8201
+ Automatic
+ True
+ True
+ True
+ True
+ True
+ 2.0
+ InCSharpFile
+ pragma
+ True
+ #pragma warning disable CA0000 // reason
+
+#pragma warning restore CA0000
+ True
+ True
+ False
+ True
+ guid()
+ 0
+ True
+ True
+ False
+ False
+ True
+ 2.0
+ InCSharpFile
+ aaa
+ True
+ [Fact]
+public void It$SOMENAME$()
+{
+ // Arrange
+
+ // Act
+
+ // Assert
+
+}
+ True
+ True
+ MSFT copyright
+ True
+ 2.0
+ InCSharpFile
+ copy
+ // Copyright (c) Microsoft. All rights reserved.
+
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ DO_NOT_SHOW
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 000000000..197770b35
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,51 @@
+
+
+
+ 12
+
+
+ LatestMajor
+
+
+ enable
+
+
+ enable
+
+ false
+ All
+ CA1812,CA2234,CS1570,CS1572,CS1573,CS1574,CA1056,CA1716,CA1724,SKEXP0003,SKEXP0011,SKEXP0021,SKEXP0026,SKEXP0042,SKEXP0050,SKEXP0052,SKEXP0053,SKEXP0060,KMEXP02,SKEXP0040
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 000000000..f4b7b2d84
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,102 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 7d60cd93d..f6746fb6f 100644
--- a/README.md
+++ b/README.md
@@ -1,172 +1,411 @@
-# Copilot Chat Sample Application
-
-> 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.
-
-![UI Sample](images/UI-Sample.png)
-
-# 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 `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 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 `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 `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:
+**NOTE**: This is a **sample** application to help you understand how Semantic Kernel and AI can work in Web Applications. This sample is **NOT FOR PRODUCTION** deployments.
+
+# Chat Copilot Sample Application
+
+This sample allows you to build your own integrated large language model (LLM) chat copilot. The sample is built on Microsoft [Semantic Kernel](https://github.com/microsoft/semantic-kernel) and has three components:
+
+1. A frontend application [React web app](./webapp/)
+2. A backend REST API [.NET web API service](./webapi/)
+3. A [.NET worker service](./memorypipeline/) for processing semantic memory.
+
+These quick-start instructions run the sample locally. They can also be found on the official Chat Copilot Microsoft Learn documentation page for [getting started](https://learn.microsoft.com/semantic-kernel/chat-copilot/getting-started).
+
+To deploy the sample to Azure, please view [Deploying Chat Copilot](./scripts/deploy/README.md) after meeting the [requirements](#requirements) described below.
+
+> **IMPORTANT:** This sample is for educational purposes only and is not recommended for production deployments.
+
+> **IMPORTANT:** Each chat interaction will call Azure OpenAI/OpenAI which will use tokens that you may be billed for.
+
+![Chat Copilot answering a question](https://learn.microsoft.com/en-us/semantic-kernel/media/chat-copilot-in-action.gif)
+
+# Requirements
+
+You will need the following items to run the sample:
+
+- [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) _(via Setup install.\* script)_
+- [Node.js](https://nodejs.org/en/download) _(via Setup install.\* script)_
+- [Yarn](https://classic.yarnpkg.com/docs/install) _(via Setup install.\* script)_
+- [Git](https://www.git-scm.com/downloads)
+- AI Service (one of the following is required)
+
+| AI Service | Requirement |
+| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Azure OpenAI | - [Access](https://aka.ms/oai/access)
- [Resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource)
- [Deployed models](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model) (`gpt-4o` and `text-embedding-ada-002`)
- [Endpoint](https://learn.microsoft.com/azure/ai-services/openai/tutorials/embeddings?tabs=command-line#retrieve-key-and-endpoint)
- [API key](https://learn.microsoft.com/azure/ai-services/openai/tutorials/embeddings?tabs=command-line#retrieve-key-and-endpoint) |
+| OpenAI | - [Account](https://platform.openai.com/docs/overview)
- [API key](https://platform.openai.com/api-keys) |
+
+# Instructions
+
+## Windows
+
+1. Open PowerShell as an administrator.
+ > NOTE: Ensure that you have [PowerShell Core 6+](https://github.com/PowerShell/PowerShell) installed. This is different from the default PowerShell installed on Windows.
+1. Clone this repository
+ ```powershell
+ git clone https://github.com/microsoft/chat-copilot
+ ```
+1. Setup your environment.
+
+ The following is a script to help you install the dependencies required. Feel free to install `dotnet`, `nodejs`, and `yarn` with your method of choice or use this script.
+
+ ```powershell
+ cd \scripts\
+ .\Install.ps1
+ ```
+
+ > NOTE: This script will install `Chocolatey`, `dotnet-8.0-sdk`, `nodejs`, and `yarn`.
+
+ > NOTE: If you receive an error that the script is not digitally signed or cannot execute on the system, you may need to [change the execution policy](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3#change-the-execution-policy) (see list of [policies](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3#powershell-execution-policies) and [scopes](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3#execution-policy-scope)) or [unblock the script](https://learn.microsoft.com/powershell/module/microsoft.powershell.security/get-executionpolicy?view=powershell-7.3#example-4-unblock-a-script-to-run-it-without-changing-the-execution-policy).
+
+1. Configure Chat Copilot.
+
+ ```powershell
+ .\Configure.ps1 -AIService {AI_SERVICE} -APIKey {API_KEY} -Endpoint {AZURE_OPENAI_ENDPOINT}
+ ```
+
+ - `AI_SERVICE`: `AzureOpenAI` or `OpenAI`.
+ - `API_KEY`: The `API key` for Azure OpenAI or for OpenAI.
+ - `AZURE_OPENAI_ENDPOINT`: The Azure OpenAI resource `Endpoint` address. This is only required when using Azure OpenAI, omit `-Endpoint` if using OpenAI.
+
+ - > **IMPORTANT:** For `AzureOpenAI`, if you deployed models `gpt-4o` and `text-embedding-ada-002` with custom names (instead of the default names), also use the parameters:
+
+ ```powershell
+ -CompletionModel {DEPLOYMENT_NAME} -EmbeddingModel {DEPLOYMENT_NAME}
+ ```
+
+ Open the `.\Configure.ps1` script to see all of the available parameters.
+
+1. Run Chat Copilot locally. This step starts both the backend API and frontend application.
+
+ ```powershell
+ .\Start.ps1
+ ```
+
+ It may take a few minutes for Yarn packages to install on the first run.
+
+ > NOTE: Confirm pop-ups are not blocked and you are logged in with the same account used to register the application.
+
+ - (Optional) To start ONLY the backend:
+
+ ```powershell
+ .\Start-Backend.ps1
+ ```
+
+## Linux/macOS
+
+1. Open Bash as an Administrator.
+1. Clone this repository
+ ```bash
+ git clone https://github.com/microsoft/chat-copilot
+ ```
+1. Configure environment.
+
+ The following is a script to help you install the dependencies required. Feel free to install `dotnet`, `nodejs`, and `yarn` with your method of choice or use this script.
+
+ ```bash
+ cd /scripts/
+ ```
+
+ **Ubuntu/Debian Linux**
+
+ ```bash
+ ./install-apt.sh
+ ```
+
+ > NOTE: This script uses `apt` to install `dotnet-sdk-8.0`, `nodejs`, and `yarn`.
+
+ **macOS**
+
+ ```bash
+ ./install-brew.sh
+ ```
+
+ > NOTE: This script uses `homebrew` to install `dotnet-sdk`, `nodejs`, and `yarn`.
+
+1. Configure Chat Copilot.
+
+ 1. For OpenAI
+
```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
+ ./configure.sh --aiservice OpenAI --apikey {API_KEY}
```
- > 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.
+ - `API_KEY`: The `API key` for OpenAI.
+
+ 2. For Azure OpenAI
+
```bash
- REACT_APP_SK_API_KEY={Your API Key, should be the same as Authorization:ApiKey from appsettings.json}
+ ./configure.sh --aiservice AzureOpenAI \
+ --endpoint {AZURE_OPENAI_ENDPOINT} \
+ --apikey {API_KEY}
```
- 3. To build and run the front-end application, open a terminal and navigate to `webapp/` if not already, then run:
+ - `AZURE_OPENAI_ENDPOINT`: The Azure OpenAI resource `Endpoint` address.
+ - `API_KEY`: The `API key` for Azure OpenAI.
+
+ **IMPORTANT:** If you deployed models `gpt-4o` and `text-embedding-ada-002`
+ with custom names (instead of the default names), you need to specify
+ the deployment names with three additional parameters:
+
```bash
- yarn install
- yarn start
+ ./configure.sh --aiservice AzureOpenAI \
+ --endpoint {AZURE_OPENAI_ENDPOINT} \
+ --apikey {API_KEY} \
+ --completionmodel {DEPLOYMENT_NAME} \
+ --embeddingmodel {DEPLOYMENT_NAME}
```
- > 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.
+1. Run Chat Copilot locally. This step starts both the backend API and frontend application.
+
+ ```bash
+ ./start.sh
+ ```
+
+ It may take a few minutes for Yarn packages to install on the first run.
+
+ > NOTE: Confirm pop-ups are not blocked and you are logged in with the same account used to register the application.
+
+ - (Optional) To start ONLY the backend:
+
+ ```powershell
+ ./start-backend.sh
+ ```
+
+## (Optional) Run the [memory pipeline](./memorypipeline/README.md)
+
+By default, the webapi is configured to work without the memory pipeline for synchronous processing documents. To enable asynchronous document processing, you need to configure the webapi and the memory pipeline. Please refer to the [webapi README](./webapi/README.md) and the [memory pipeline README](./memorypipeline/README.md) for more information.
+
+## (Optional) Enable backend authentication via Azure AD
+
+By default, Chat Copilot runs locally without authentication, using a guest user profile. If you want to enable authentication with Azure Active Directory, follow the steps below.
+
+### Requirements
+
+- [Azure account](https://azure.microsoft.com/free)
+- [Azure AD Tenant](https://learn.microsoft.com/azure/active-directory/develop/quickstart-create-new-tenant)
+
+### Instructions
+
+1. Create an [application registration](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app) for the frontend web app, using the values below
+
+ - `Supported account types`: "_Accounts in this organizational directory only ({YOUR TENANT} only - Single tenant)_"
+ - `Redirect URI (optional)`: _Single-page application (SPA)_ and use _http://localhost:3000_.
+
+2. Create a second [application registration](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app) for the backend web api, using the values below:
+ - `Supported account types`: "_Accounts in this organizational directory only ({YOUR TENANT} only - Single tenant)_"
+ - Do **not** configure a `Redirect URI (optional)`
+
+> NOTE: Other account types can be used to allow multitenant and personal Microsoft accounts to use your application if you desire. Doing so may result in more users and therefore higher costs.
+
+> Take note of the `Application (client) ID` for both app registrations as you will need them in future steps.
+
+3. Expose an API within the second app registration
-2. Have fun!
- > **Note:** Each chat interaction will call Azure OpenAI/OpenAI which will use tokens that you may be billed for.
+ 1. Select _Expose an API_ from the menu
+
+ 2. Add an _Application ID URI_
+
+ 1. This will generate an `api://` URI
+
+ 2. Click _Save_ to store the generated URI
+
+ 3. Add a scope for `access_as_user`
+
+ 1. Click _Add scope_
+
+ 2. Set _Scope name_ to `access_as_user`
+
+ 3. Set _Who can consent_ to _Admins and users_
+
+ 4. Set _Admin consent display name_ and _User consent display name_ to `Access copilot chat as a user`
+
+ 5. Set _Admin consent description_ and _User consent description_ to `Allows the accesses to the Copilot chat web API as a user`
+
+ 4. Add the web app frontend as an authorized client application
+
+ 1. Click _Add a client application_
+
+ 2. For _Client ID_, enter the frontend's application (client) ID
+
+ 3. Check the checkbox under _Authorized scopes_
+
+ 4. Click _Add application_
+
+4. Add permissions to web app frontend to access web api as user
+
+ 1. Open app registration for web app frontend
+
+ 2. Go to _API Permissions_
+
+ 3. Click _Add a permission_
+
+ 4. Select the tab _APIs my organization uses_
+
+ 5. Choose the app registration representing the web api backend
+
+ 6. Select permissions `access_as_user`
+
+ 7. Click _Add permissions_
+
+5. Run the Configure script with additional parameters to set up authentication.
+
+ **Powershell**
+
+ ```powershell
+ .\Configure.ps1 -AiService {AI_SERVICE} -APIKey {API_KEY} -Endpoint {AZURE_OPENAI_ENDPOINT} -FrontendClientId {FRONTEND_APPLICATION_ID} -BackendClientId {BACKEND_APPLICATION_ID} -TenantId {TENANT_ID} -Instance {AZURE_AD_INSTANCE}
+ ```
+
+ **Bash**
+
+ ```bash
+ ./configure.sh --aiservice {AI_SERVICE} --apikey {API_KEY} --endpoint {AZURE_OPENAI_ENDPOINT} --frontend-clientid {FRONTEND_APPLICATION_ID} --backend-clientid {BACKEND_APPLICATION_ID} --tenantid {TENANT_ID} --instance {AZURE_AD_INSTANCE}
+ ```
+
+ - `AI_SERVICE`: `AzureOpenAI` or `OpenAI`.
+ - `API_KEY`: The `API key` for Azure OpenAI or for OpenAI.
+ - `AZURE_OPENAI_ENDPOINT`: The Azure OpenAI resource `Endpoint` address. This is only required when using Azure OpenAI, omit `-Endpoint` if using OpenAI.
+ - `FRONTEND_APPLICATION_ID`: The `Application (client) ID` associated with the application registration for the frontend.
+ - `BACKEND_APPLICATION_ID`: The `Application (client) ID` associated with the application registration for the backend.
+ - `TENANT_ID` : Your Azure AD tenant ID
+ - `AZURE_AD_INSTANCE` _(optional)_: The Azure AD cloud instance for the authenticating users. Defaults to `https://login.microsoftonline.com`.
+
+6. Run Chat Copilot locally. This step starts both the backend API and frontend application.
+
+ **Powershell**
+
+ ```powershell
+ .\Start.ps1
+ ```
+
+ **Bash**
+
+ ```bash
+ ./start.sh
+ ```
+
+## Optional Configuration: [Ms Graph API Plugin with On-Behalf-Of Flow](./plugins/OBO/README.md)
+
+This native plugin enables the execution of Microsoft Graph APIs using the On-Behalf-Of (OBO) flow with delegated permissions.
+
+The OBO flows is used to ensure that the backend APIs are consumed with the identity of the user, not the managed identity or service principal of the middle-tier application (in this case the WebApi).
+
+Also, this ensures that consent is given, so that the client app (WebApp) can call the middle-tier app (WebApi), and the middle-tier app has permission to call the back-end resource (MSGraph).
+
+This sample does not implement incremental consent in the UI so all the Graph scopes to be used need to have "Administrator Consent" given in the middle-tier app registration.
+
+More information in the [OBO readme.md](./plugins/OBO/README.md).
+
+### Requirements
+
+Backend authentication via Azure AD must be enabled. Detailed instructions for enabling backend authentication are provided below.
+
+### Limitations
+
+- Currently, the plugin only supports GET operations. Future updates may add support for other types of operations.
+- Graph queries that return large results, may reach the token limit for the AI model, producing an error.
+- Incremental consent is not implemented in this sample.
# Troubleshooting
-## 1. Unable to load chats. Details: interaction_in_progress: Interaction is currently in progress.
+1. **_Issue:_** Unable to load chats.
+
+ _Details_: interaction*in_progress: Interaction is currently in progress.*
+
+ _Explanation_: The WebApp can display this error when the application is configured for a different AAD tenant from the browser, (e.g., personal/MSA account vs work/school account).
+
+ _Solution_: Either use a private/incognito browser tab or clear your browser credentials/cookies. Confirm you are logged in with the same account used to register the application.
+
+2. **_Issue:_**: Challenges using text completion models, such as `text-davinci-003`
+
+ _Solution_: For OpenAI, see [model endpoint compatibility](https://platform.openai.com/docs/models/model-endpoint-compatibility) for
+ the complete list of current models supporting chat completions. For Azure OpenAI, see [model summary table and region availability](https://learn.microsoft.com/azure/ai-services/openai/concepts/models#model-summary-table-and-region-availability).
+
+3. **_Issue:_** Localhost SSL certificate errors / CORS errors
+
+ ![Cert-Issue](https://github.com/microsoft/chat-copilot/assets/64985898/e9072af1-e43c-472d-bebc-d0082d0c9180)
+
+ _Explanation_: Your browser may be blocking the frontend access to the backend while waiting for your permission to connect.
+
+ _Solution_:
+
+ 1. Confirm the backend service is running. Open a web browser and navigate to `https://localhost:40443/healthz`
+ - You should see a confirmation message: `Healthy`
+ - If your browser asks you to acknowledge the risks of visiting an insecure website, you must acknowledge this before the frontend can connect to the backend server.
+ 2. Navigate to `http://localhost:3000` or refresh the page to use the Chat Copilot application.
+
+4. **_Issue:_** Yarn is not working.
+
+ _Explanation_: You may have the wrong Yarn version installed such as v2.x+.
+
+ _Solution_: Use the classic version.
+
+ ```bash
+ npm install -g yarn
+ yarn set version classic
+ ```
+
+5. **_Issue:_** Missing `/usr/share/dotnet/host/fxr` folder.
+
+ _Details_: "A fatal error occurred. The folder [/usr/share/dotnet/host/fxr] does not exist" when running dotnet commands on Linux.
+
+ _Explanation_: 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. ([Source: StackOverflow](https://stackoverflow.com/questions/73753672/a-fatal-error-occurred-the-folder-usr-share-dotnet-host-fxr-does-not-exist))
+
+ _Solution_:
+
+ ```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-8.0;
+ ```
+
+# A note on branches
-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.
+Every release is associated with a release branch. For instance, release [v0.9](https://github.com/microsoft/chat-copilot/releases/tag/v0.9) is on a branch called [0.9](https://github.com/microsoft/chat-copilot/tree/0.9).
+Once a release is out, its branch will no longer be updated. The exception to this is the latest release branch, which will only receive bug fixes.
+This is to provide some stability to those for whom breaking changes and being on the bleeding edge (with the bugs it can entail) is not a desirable option.
-## 2. Issues using text completion models, such as `text-davinci-003`
+# Check out our other repos!
-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.
+If you would like to learn more about Semantic Kernel and AI, you may also be interested in other repos the Semantic Kernel team supports:
-## 3. Localhost SSL certificate errors / CORS errors
+| Repo | Description |
+| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
+| [Semantic Kernel](https://github.com/microsoft/semantic-kernel) | A lightweight SDK that integrates cutting-edge LLM technology quickly and easily into your apps. |
+| [Semantic Kernel Docs](https://github.com/MicrosoftDocs/semantic-kernel-docs) | The home for Semantic Kernel documentation that appears on the Microsoft learn site. |
+| [Semantic Kernel Starters](https://github.com/microsoft/semantic-kernel-starters) | Starter projects for Semantic Kernel to make it easier to get started. |
+| [Semantic Memory](https://github.com/microsoft/semantic-memory) | A service that allows you to create pipelines for ingesting, storing, and querying knowledge. |
-![](images/Cert-Issue.png)
+## Join the community
-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:
+We welcome your contributions and suggestions to the Chat Copilot Sample App! One of the easiest
+ways to participate is to engage in discussions in the GitHub repository.
+Bug reports and fixes are welcome!
-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.
+To learn more and get started:
-## 4. Have Yarn version 2.x or 3.x
+- Read the [documentation](https://learn.microsoft.com/semantic-kernel/chat-copilot/)
+- Join the [Discord community](https://aka.ms/SKDiscord)
+- [Contribute](CONTRIBUTING.md) to the project
+- Follow the team on our [blog](https://aka.ms/sk/blog)
-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.
+## Code of Conduct
-```shell
-npm install -g yarn
-yarn set version classic
-```
+This project has adopted the
+[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
+For more information see the
+[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
+or contact [opencode@microsoft.com](mailto:opencode@microsoft.com)
+with any additional questions or comments.
-You can confirm the active Yarn version by running `yarn --version`.
+## License
-# Additional resources
+Copyright (c) Microsoft Corporation. All rights reserved.
-1. [Import Document Application](./importdocument/README.md): Import a document to the memory store.
+Licensed under the [MIT](LICENSE) license.
diff --git a/SUPPORT.md b/SUPPORT.md
index eaf439aec..fe7dc1820 100644
--- a/SUPPORT.md
+++ b/SUPPORT.md
@@ -1,13 +1,3 @@
-# TODO: The maintainer of this repo has not yet edited this file
-
-**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project?
-
-- **No CSS support:** Fill out this template with information about how to file issues and get help.
-- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps.
-- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide.
-
-*Then remove this first heading from this SUPPORT.MD file before publishing your repo.*
-
# Support
## How to file issues and get help
@@ -16,10 +6,6 @@ This project uses GitHub Issues to track bugs and feature requests. Please searc
issues before filing new issues to avoid duplicates. For new issues, file your bug or
feature request as a new Issue.
-For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE
-FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER
-CHANNEL. WHERE WILL YOU HELP PEOPLE?**.
-
## Microsoft Support Policy
Support for this **PROJECT or PRODUCT** is limited to the resources listed above.
diff --git a/deploy/README.md b/deploy/README.md
deleted file mode 100644
index b58b25263..000000000
--- a/deploy/README.md
+++ /dev/null
@@ -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:
-
-[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoft%2Fcopilot-chat%2Fmain%2Fdeploy%2Fmain.json)
-
-> This will automatically deploy the most recent release of CopilotChat backend binaries ([link](https://github.com/microsoft/copilot-chat/releases)).
-
-> 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')
-```
-
diff --git a/deploy/deploy-webapp.ps1 b/deploy/deploy-webapp.ps1
deleted file mode 100644
index e9997946b..000000000
--- a/deploy/deploy-webapp.ps1
+++ /dev/null
@@ -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."
diff --git a/deploy/deploy-webapp.sh b/deploy/deploy-webapp.sh
deleted file mode 100644
index 13b49c6e4..000000000
--- a/deploy/deploy-webapp.sh
+++ /dev/null
@@ -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."
\ No newline at end of file
diff --git a/deploy/main.bicep b/deploy/main.bicep
deleted file mode 100644
index 7307dea84..000000000
--- a/deploy/main.bicep
+++ /dev/null
@@ -1,708 +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
- }
-}
-
-resource appServiceWebConfig 'Microsoft.Web/sites/config@2022-09-01' = {
- parent: appServiceWeb
- name: '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: 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
diff --git a/deploy/main.json b/deploy/main.json
deleted file mode 100644
index 1f7539631..000000000
--- a/deploy/main.json
+++ /dev/null
@@ -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'))]"
- }
- }
-}
\ No newline at end of file
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
new file mode 100644
index 000000000..dd4255db4
--- /dev/null
+++ b/docker/docker-compose.yaml
@@ -0,0 +1,139 @@
+# To start docker-compose:
+# 1. Copy the example .env file for webapi:
+# cp ../docker/webapi/.env.example ../docker/webapi/.env
+# 2. Edit the .env file to add the corresponding configuration values.
+# 3. Copy the example .env file for web searcher plugin:
+# cp ../docker/plugins/web-searcher/.env.example ../docker/plugins/web-searcher/.env
+# 4. Edit the .env file for web searcher plugin to add the corresponding configuration values.
+# 5. Copy the example .env file for memorypipeline:
+# cp ../docker/memorypipeline/.env.example ../docker/memorypipeline/.env
+# 6. Edit the .env file for memmorypipeline to add the corresponding configuration values.
+# 7. With the .env files populated, you can now start docker-compose by running "docker-compose up --build"
+
+version: "3"
+services:
+ chat-copilot-webapp-nginx:
+ image: chat-copilot-webapp-nginx
+ build:
+ context: ..
+ dockerfile: docker/webapp/Dockerfile.nginx
+ args:
+ - REACT_APP_BACKEND_URI=http://localhost:3080
+ ports:
+ - 3000:3000
+ depends_on:
+ chat-copilot-webapi:
+ condition: service_started
+ # Alternatively use webapi image to serve frontend files
+ # chat-copilot-webapp:
+ # image: chat-copilot-webapp
+ # build:
+ # context: ..
+ # dockerfile: docker/webapp/Dockerfile
+ # ports:
+ # - 3000:3000
+ # environment:
+ # - REACT_APP_BACKEND_URI=http://localhost:3080
+ # depends_on:
+ # chat-copilot-webapi:
+ # condition: service_started
+ chat-copilot-webapi:
+ image: chat-copilot-webapi
+ build:
+ context: ..
+ dockerfile: docker/webapi/Dockerfile
+ ports:
+ - 3080:8080
+ env_file:
+ - webapi/.env
+ environment:
+ - Authentication__Type=AzureAd
+ - KernelMemory__Services__Qdrant__Endpoint=http://qdrant:6333
+ - KernelMemory__Services__Qdrant__APIKey=chat-copilot
+ - KernelMemory__Services__RabbitMq__Host=rabbitmq
+ - KernelMemory__Services__RabbitMq__Port=5672
+ - KernelMemory__Services__RabbitMq__Username=chat-copilot
+ - KernelMemory__Services__RabbitMq__Password=chat-copilot
+ - KernelMemory__DocumentStorageType=AzureBlobs
+ - KernelMemory__ImageOcrType=Tesseract
+ - KernelMemory__TextGeneratorType=AzureOpenAI
+ - KernelMemory__DataIngestion__OrchestrationType=Distributed
+ - KernelMemory__DataIngestion__DistributedOrchestration__QueueType=RabbitMQ
+ - KernelMemory__DataIngestion__VectorDbTypes__0=Qdrant
+ - KernelMemory__DataIngestion__EmbeddingGeneratorTypes__0=AzureOpenAI
+ - KernelMemory__Retrieval__EmbeddingGeneratorType=AzureOpenAI
+ - KernelMemory__Retrieval__VectorDbType=Qdrant
+ - Plugins__1__Name=WebSearcher
+ - Plugins__1__ManifestDomain=http://web-searcher
+ - Plugins__1__Key=chat-copilot
+ depends_on:
+ qdrant:
+ condition: service_started
+ rabbitmq:
+ condition: service_healthy
+ web-searcher:
+ condition: service_healthy
+ qdrant:
+ image: qdrant/qdrant
+ ports:
+ - 6333:6333
+ environment:
+ - QDRANT__SERVICE__API_KEY=chat-copilot
+ - QDRANT__LOG_LEVEL=INFO
+ rabbitmq:
+ image: rabbitmq:management
+ ports:
+ - 5672:5672
+ - 15672:15672
+ environment:
+ - RABBITMQ_DEFAULT_USER=chat-copilot
+ - RABBITMQ_DEFAULT_PASS=chat-copilot
+ healthcheck:
+ test: rabbitmq-diagnostics check_port_connectivity
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ web-searcher:
+ image: web-searcher
+ build:
+ context: ..
+ dockerfile: docker/plugins/web-searcher/Dockerfile
+ args:
+ - AZURE_FUNCTION_MASTER_KEY=chat-copilot
+ env_file:
+ - plugins/web-searcher/.env
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost/.well-known/ai-plugin.json"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ chat-copilot-memorypipeline:
+ image: chat-copilot-memorypipeline
+ build:
+ context: ..
+ dockerfile: docker/memorypipeline/Dockerfile
+ ports:
+ - 3280:80
+ env_file:
+ - memorypipeline/.env
+ environment:
+ - KernelMemory__Services__Qdrant__Endpoint=http://qdrant:6333
+ - KernelMemory__Services__Qdrant__APIKey=chat-copilot
+ - KernelMemory__Services__RabbitMq__Host=rabbitmq
+ - KernelMemory__Services__RabbitMq__Port=5672
+ - KernelMemory__Services__RabbitMq__Username=chat-copilot
+ - KernelMemory__Services__RabbitMq__Password=chat-copilot
+ - KernelMemory__DocumentStorageType=AzureBlobs
+ - KernelMemory__ImageOcrType=Tesseract
+ - KernelMemory__TextGeneratorType=AzureOpenAI
+ - KernelMemory__DataIngestion__OrchestrationType=Distributed
+ - KernelMemory__DataIngestion__DistributedOrchestration__QueueType=RabbitMQ
+ - KernelMemory__DataIngestion__VectorDbTypes__0=Qdrant
+ - KernelMemory__DataIngestion__EmbeddingGeneratorTypes__0=AzureOpenAI
+ - KernelMemory__Retrieval__EmbeddingGeneratorType=AzureOpenAI
+ - KernelMemory__Retrieval__VectorDbType=Qdrant
+ depends_on:
+ qdrant:
+ condition: service_started
+ rabbitmq:
+ condition: service_healthy
diff --git a/docker/memorypipeline/.env.example b/docker/memorypipeline/.env.example
new file mode 100644
index 000000000..6d37f0869
--- /dev/null
+++ b/docker/memorypipeline/.env.example
@@ -0,0 +1,13 @@
+# Azure OpenAI embedding settings
+KernelMemory__Services__AzureOpenAIEmbedding__Deployment=
+KernelMemory__Services__AzureOpenAIEmbedding__Endpoint=
+KernelMemory__Services__AzureOpenAIEmbedding__APIKey=
+
+# Azure OpenAI text settings
+KernelMemory__Services__AzureOpenAIText__Deployment=
+KernelMemory__Services__AzureOpenAIText__Endpoint=
+KernelMemory__Services__AzureOpenAIText__APIKey=
+
+# Azure blob
+KernelMemory__Services__AzureBlobs__Auth=
+KernelMemory__Services__AzureBlobs__ConnectionString=
\ No newline at end of file
diff --git a/docker/memorypipeline/Dockerfile b/docker/memorypipeline/Dockerfile
new file mode 100644
index 000000000..904774443
--- /dev/null
+++ b/docker/memorypipeline/Dockerfile
@@ -0,0 +1,28 @@
+# docker build -f docker/webapi/Dockerfile -t chat-copilot-memorypipeline .
+
+# builder
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS builder
+WORKDIR /source
+# generate dev-certs for https
+RUN dotnet dev-certs https
+# copy everything else and build app
+COPY memorypipeline memorypipeline
+COPY shared shared
+RUN cd memorypipeline && \
+ dotnet restore --use-current-runtime && \
+ apt update && apt install -y wget && \
+ dotnet publish --use-current-runtime --self-contained false --no-restore -o /app && \
+ mkdir /app/data && \
+ wget -P /app/data https://raw.githubusercontent.com/tesseract-ocr/tessdata/main/eng.traineddata
+
+
+# final stage/image
+FROM mcr.microsoft.com/dotnet/aspnet:8.0
+WORKDIR /app
+COPY --from=builder /app .
+COPY --from=builder /root/.dotnet/corefx/cryptography/x509stores/my/* /root/.dotnet/corefx/cryptography/x509stores/my/
+RUN apt update && \
+ apt install -y libleptonica-dev libtesseract-dev libc6-dev libjpeg62-turbo-dev libgdiplus && \
+ ln -s /usr/lib/x86_64-linux-gnu/liblept.so.5 x64/libleptonica-1.82.0.so && \
+ ln -s /usr/lib/x86_64-linux-gnu/libtesseract.so.4.0.1 x64/libtesseract50.so
+ENTRYPOINT ["./CopilotChatMemoryPipeline"]
\ No newline at end of file
diff --git a/docker/plugins/web-searcher/.env.example b/docker/plugins/web-searcher/.env.example
new file mode 100644
index 000000000..d4fd11320
--- /dev/null
+++ b/docker/plugins/web-searcher/.env.example
@@ -0,0 +1 @@
+PluginConfig__BingApiKey=
\ No newline at end of file
diff --git a/docker/plugins/web-searcher/Dockerfile b/docker/plugins/web-searcher/Dockerfile
new file mode 100644
index 000000000..efd286109
--- /dev/null
+++ b/docker/plugins/web-searcher/Dockerfile
@@ -0,0 +1,23 @@
+# docker build -f docker/plugins/web-searcher/Dockerfile -t web-searcher .
+
+# builder
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS installer-env
+ARG AZURE_FUNCTION_MASTER_KEY
+WORKDIR /source
+COPY plugins/shared shared
+COPY plugins/web-searcher web-searcher
+RUN cd /source/web-searcher && \
+ mkdir -p /home/site/wwwroot && \
+ dotnet publish *.csproj --output /home/site/wwwroot && \
+ mkdir -p /azure-functions-host/Secrets/ && \
+ echo "{\"masterKey\":{\"name\":\"master\",\"value\":\"$AZURE_FUNCTION_MASTER_KEY\",\"encrypted\":false},\"functionKeys\":[]}" > /azure-functions-host/Secrets/host.json
+
+# final stage/image
+FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4.0-dotnet-isolated8.0-appservice
+ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
+ AzureFunctionsJobHost__Logging__Console__IsEnabled=true \
+ AzureWebJobsSecretStorageType=files
+COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"]
+COPY --from=installer-env ["/azure-functions-host/Secrets", "/azure-functions-host/Secrets"]
+
+
diff --git a/docker/webapi/.env.example b/docker/webapi/.env.example
new file mode 100644
index 000000000..65e911592
--- /dev/null
+++ b/docker/webapi/.env.example
@@ -0,0 +1,23 @@
+# Backend authentication via Azure AD
+# Refer to https://github.com/microsoft/chat-copilot#optional-enable-backend-authentication-via-azure-ad
+Authentication__AzureAd__ClientId=
+Authentication__AzureAd__TenantId=
+Frontend__AadClientId=
+
+# Azure speech settings
+AzureSpeech__Region=
+AzureSpeech__Key=
+
+# Azure OpenAI embedding settings
+KernelMemory__Services__AzureOpenAIEmbedding__Deployment=
+KernelMemory__Services__AzureOpenAIEmbedding__Endpoint=
+KernelMemory__Services__AzureOpenAIEmbedding__APIKey=
+
+# Azure OpenAI text settings
+KernelMemory__Services__AzureOpenAIText__Deployment=
+KernelMemory__Services__AzureOpenAIText__Endpoint=
+KernelMemory__Services__AzureOpenAIText__APIKey=
+
+# Azure blobs settings
+KernelMemory__Services__AzureBlobs__Auth=
+KernelMemory__Services__AzureBlobs__ConnectionString=
\ No newline at end of file
diff --git a/docker/webapi/Dockerfile b/docker/webapi/Dockerfile
new file mode 100644
index 000000000..2b1f257cf
--- /dev/null
+++ b/docker/webapi/Dockerfile
@@ -0,0 +1,27 @@
+# docker build -f docker/webapi/Dockerfile -t chat-copilot-webapi .
+
+# builder
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS builder
+WORKDIR /source
+# generate dev-certs for https
+RUN dotnet dev-certs https
+# copy everything else and build app
+COPY webapi webapi
+COPY shared shared
+RUN cd webapi && \
+ dotnet restore --use-current-runtime && \
+ apt update && apt install -y wget && \
+ wget -P data https://raw.githubusercontent.com/tesseract-ocr/tessdata/main/eng.traineddata && \
+ dotnet publish --use-current-runtime --self-contained false --no-restore -o /app
+
+# final stage/image
+FROM mcr.microsoft.com/dotnet/aspnet:8.0
+ENV Kestrel__Endpoints__Http__Url=http://0.0.0.0:8080
+WORKDIR /app
+COPY --from=builder /app .
+COPY --from=builder /root/.dotnet/corefx/cryptography/x509stores/my/* /root/.dotnet/corefx/cryptography/x509stores/my/
+RUN apt update && \
+ apt install -y libleptonica-dev libtesseract-dev libc6-dev libjpeg62-turbo-dev libgdiplus && \
+ ln -s /usr/lib/x86_64-linux-gnu/liblept.so.5 x64/libleptonica-1.82.0.so && \
+ ln -s /usr/lib/x86_64-linux-gnu/libtesseract.so.4.0.1 x64/libtesseract50.so
+ENTRYPOINT ["./CopilotChatWebApi"]
\ No newline at end of file
diff --git a/docker/webapp/Dockerfile b/docker/webapp/Dockerfile
new file mode 100644
index 000000000..3fc4671e0
--- /dev/null
+++ b/docker/webapp/Dockerfile
@@ -0,0 +1,19 @@
+# docker build -f docker/webapp/Dockerfile -t chat-copilot-webapp .
+
+# builder
+FROM node:lts-alpine as builder
+WORKDIR /app
+COPY webapp/ .
+RUN yarn install \
+ --prefer-offline \
+ --frozen-lockfile \
+ --non-interactive \
+ --production=false
+
+# final stage/image
+FROM node:lts-alpine
+WORKDIR /app
+COPY --from=builder /app .
+ENV HOST 0.0.0.0
+EXPOSE 3000
+ENTRYPOINT [ "yarn", "start" ]
\ No newline at end of file
diff --git a/docker/webapp/Dockerfile.nginx b/docker/webapp/Dockerfile.nginx
new file mode 100644
index 000000000..505a34234
--- /dev/null
+++ b/docker/webapp/Dockerfile.nginx
@@ -0,0 +1,21 @@
+# source webapp/.env
+# docker build --build-arg REACT_APP_BACKEND_URI=$REACT_APP_BACKEND_URI -f docker/webapp/Dockerfile.nginx -t chat-copilot-webapp-nginx .
+
+# builder
+FROM node:lts-alpine as builder
+ARG REACT_APP_BACKEND_URI
+WORKDIR /app
+COPY webapp/ .
+RUN yarn install \
+ --prefer-offline \
+ --frozen-lockfile \
+ --non-interactive \
+ --production=false
+RUN yarn build
+
+# final stage/image
+FROM nginx:stable-alpine
+EXPOSE 3000
+RUN sed -i 's/80/3000/g' /etc/nginx/conf.d/default.conf
+COPY --from=builder /app/build /usr/share/nginx/html
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/images/Cert-Issue.png b/images/Cert-Issue.png
deleted file mode 100644
index c0ac3f822..000000000
Binary files a/images/Cert-Issue.png and /dev/null differ
diff --git a/images/Document-Memory-Sample-1.png b/images/Document-Memory-Sample-1.png
deleted file mode 100644
index 84ced7083..000000000
Binary files a/images/Document-Memory-Sample-1.png and /dev/null differ
diff --git a/images/Document-Memory-Sample-2.png b/images/Document-Memory-Sample-2.png
deleted file mode 100644
index 777ecba85..000000000
Binary files a/images/Document-Memory-Sample-2.png and /dev/null differ
diff --git a/images/UI-Sample.png b/images/UI-Sample.png
deleted file mode 100644
index 1bad26fe3..000000000
Binary files a/images/UI-Sample.png and /dev/null differ
diff --git a/importdocument/Program.cs b/importdocument/Program.cs
deleted file mode 100644
index 3149de2e6..000000000
--- a/importdocument/Program.cs
+++ /dev/null
@@ -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;
-
-///
-/// This console app imports a list of files to the CopilotChat WebAPI document memory store.
-///
-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>(name: "--files", description: "The files to import to document memory store.")
- {
- IsRequired = true,
- AllowMultipleArgumentsPerToken = true,
- };
-
- var chatCollectionOption = new Option(
- 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);
- }
-
- ///
- /// Acquires a user account from Azure AD.
- ///
- /// The App configuration.
- /// Sets the account to the first account found.
- /// Sets the access token to the first account found.
- /// True if the user account was acquired.
- private static async Task AcquireUserAccountAsync(
- Config config,
- Action setAccount,
- Action 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? 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;
- }
- }
-
- ///
- /// Conditionally imports a list of files to the Document Store.
- ///
- /// A list of files to import.
- /// Configuration.
- /// Save the extracted context to an isolated chat collection.
- private static async Task ImportFilesAsync(IEnumerable 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 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();
- }
- }
-
- ///
- /// Sends a POST request to the Document Store to upload a file for parsing.
- ///
- /// The multipart form data content to send.
- /// Configuration.
- 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}");
- }
- }
-}
diff --git a/importdocument/README.md b/importdocument/README.md
deleted file mode 100644
index 8ab490ed5..000000000
--- a/importdocument/README.md
+++ /dev/null
@@ -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):
-
- ![](../images/Document-Memory-Sample-1.png)
-
- With [Microsoft Responsible AI Standard v2 General Requirements.pdf](./sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf):
-
- ![](../images/Document-Memory-Sample-2.png)
\ No newline at end of file
diff --git a/importdocument/appsettings.json b/importdocument/appsettings.json
deleted file mode 100644
index c1aa3ade8..000000000
--- a/importdocument/appsettings.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Config": {
- "ClientId": "",
- "RedirectUri": "",
- "ServiceUri": "https://localhost:40443",
- "ApiKey": ""
- }
-}
\ No newline at end of file
diff --git a/integration-tests/.editorconfig b/integration-tests/.editorconfig
new file mode 100644
index 000000000..96b5cd531
--- /dev/null
+++ b/integration-tests/.editorconfig
@@ -0,0 +1,3 @@
+[**/**.cs]
+resharper_inconsistent_naming_highlighting = none
+dotnet_diagnostic.IDE1006.severity = none # No need for Async suffix on test names
diff --git a/integration-tests/ChatCopilotIntegrationTest.cs b/integration-tests/ChatCopilotIntegrationTest.cs
new file mode 100644
index 000000000..719e01c8f
--- /dev/null
+++ b/integration-tests/ChatCopilotIntegrationTest.cs
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Net.Http.Headers;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Identity.Client;
+using Xunit;
+
+namespace ChatCopilotIntegrationTests;
+
+///
+/// Base class for Chat Copilot integration tests
+///
+[Trait("Category", "Integration Tests")]
+#pragma warning disable CA1051
+public abstract class ChatCopilotIntegrationTest : IDisposable
+{
+ protected const string BaseUrlSettingName = "BaseServerUrl";
+ protected const string ClientIdSettingName = "ClientID";
+ protected const string AuthoritySettingName = "Authority";
+ protected const string UsernameSettingName = "TestUsername";
+ protected const string PasswordSettingName = "TestPassword";
+ protected const string ScopesSettingName = "Scopes";
+
+ protected readonly HttpClient HTTPClient;
+ protected readonly IConfigurationRoot Configuration;
+
+ protected ChatCopilotIntegrationTest()
+ {
+ // Load configuration
+ this.Configuration = new ConfigurationBuilder()
+ .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true)
+ .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true)
+ .AddEnvironmentVariables()
+ .AddUserSecrets()
+ .Build();
+
+ string? baseUrl = this.Configuration[BaseUrlSettingName];
+ Assert.False(string.IsNullOrEmpty(baseUrl));
+ Assert.True(baseUrl.EndsWith('/'));
+
+ this.HTTPClient = new HttpClient();
+ this.HTTPClient.BaseAddress = new Uri(baseUrl);
+ }
+
+ public void Dispose()
+ {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ this.HTTPClient.Dispose();
+ }
+ }
+
+ protected async Task SetUpAuthAsync()
+ {
+ string accesstoken = await this.GetUserTokenByPasswordAsync();
+ Assert.True(!string.IsNullOrEmpty(accesstoken));
+
+ this.HTTPClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accesstoken);
+ }
+
+ protected async Task GetUserTokenByPasswordAsync()
+ {
+ IPublicClientApplication app = PublicClientApplicationBuilder.Create(this.Configuration[ClientIdSettingName])
+ .WithAuthority(this.Configuration[AuthoritySettingName])
+ .Build();
+
+ string? scopeString = this.Configuration[ScopesSettingName];
+ Assert.NotNull(scopeString);
+
+ string[] scopes = scopeString.Split([',', ' '], StringSplitOptions.RemoveEmptyEntries);
+
+ var accounts = await app.GetAccountsAsync();
+
+ AuthenticationResult? result = null;
+
+ if (accounts.Any())
+ {
+ result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync();
+ }
+ else
+ {
+ result = await app.AcquireTokenByUsernamePassword(scopes, this.Configuration[UsernameSettingName], this.Configuration[PasswordSettingName]).ExecuteAsync();
+ }
+
+ return result?.AccessToken ?? string.Empty;
+ }
+}
diff --git a/integration-tests/ChatCopilotIntegrationTests.csproj b/integration-tests/ChatCopilotIntegrationTests.csproj
new file mode 100644
index 000000000..ebca6076c
--- /dev/null
+++ b/integration-tests/ChatCopilotIntegrationTests.csproj
@@ -0,0 +1,40 @@
+
+
+
+ net8.0
+ true
+ 81136671-a63f-4f20-bd0e-a65b2561999a
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/integration-tests/ChatTests.cs b/integration-tests/ChatTests.cs
new file mode 100644
index 000000000..c03a2dbd1
--- /dev/null
+++ b/integration-tests/ChatTests.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Net.Http.Json;
+using System.Text.Json;
+using CopilotChat.WebApi.Models.Request;
+using CopilotChat.WebApi.Models.Response;
+using Xunit;
+using static CopilotChat.WebApi.Models.Storage.CopilotChatMessage;
+
+namespace ChatCopilotIntegrationTests;
+
+public class ChatTests : ChatCopilotIntegrationTest
+{
+ private static readonly JsonSerializerOptions jsOpts = new() { PropertyNameCaseInsensitive = true };
+
+ [Fact]
+ public async void ChatMessagePostSucceedsWithValidInput()
+ {
+ await this.SetUpAuthAsync();
+
+ // Create chat session
+ var createChatParams = new CreateChatParameters() { Title = nameof(this.ChatMessagePostSucceedsWithValidInput) };
+ HttpResponseMessage response = await this.HTTPClient.PostAsJsonAsync("chats", createChatParams);
+ response.EnsureSuccessStatusCode();
+
+ var contentStream = await response.Content.ReadAsStreamAsync();
+ var createChatResponse = await JsonSerializer.DeserializeAsync(contentStream, jsOpts);
+ Assert.NotNull(createChatResponse);
+
+ // Ask something to the bot
+ var ask = new Ask
+ {
+ Input = "Who is Satya Nadella?",
+ Variables = new KeyValuePair[] { new("MessageType", ChatMessageType.Message.ToString()) }
+ };
+ response = await this.HTTPClient.PostAsJsonAsync($"chats/{createChatResponse.ChatSession.Id}/messages", ask);
+ response.EnsureSuccessStatusCode();
+
+ contentStream = await response.Content.ReadAsStreamAsync();
+ var askResult = await JsonSerializer.DeserializeAsync(contentStream, jsOpts);
+ Assert.NotNull(askResult);
+ Assert.False(string.IsNullOrEmpty(askResult.Value));
+
+ // Clean up
+ response = await this.HTTPClient.DeleteAsync($"chats/{createChatResponse.ChatSession.Id}").ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ }
+}
diff --git a/integration-tests/HealthzTests.cs b/integration-tests/HealthzTests.cs
new file mode 100644
index 000000000..d3c77ba06
--- /dev/null
+++ b/integration-tests/HealthzTests.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Xunit;
+
+namespace ChatCopilotIntegrationTests;
+
+///
+/// Class for testing the healthcheck endpoint
+///
+public class HealthzTests : ChatCopilotIntegrationTest
+{
+ [Fact]
+ public async void HealthzSuccessfullyReturns()
+ {
+ HttpResponseMessage response = await this.HTTPClient.GetAsync("healthz");
+
+ response.EnsureSuccessStatusCode();
+ }
+}
diff --git a/integration-tests/README.md b/integration-tests/README.md
new file mode 100644
index 000000000..6674822af
--- /dev/null
+++ b/integration-tests/README.md
@@ -0,0 +1,55 @@
+# Chat Copilot Integration Tests
+
+## Requirements
+
+1. A running instance of the Chat Copilot's [backend](../webapi/README.md).
+
+## Setup
+
+### Option 1: Use Secret Manager
+
+Integration tests require the URL of the backend instance.
+
+We suggest using the .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets)
+to avoid the risk of leaking secrets into the repository, branches and pull requests.
+
+Values set using the Secret Manager will override the settings set in the `testsettings.development.json` file and in environment variables.
+
+To set your secrets with Secret Manager:
+
+```ps
+cd integration-tests
+
+dotnet user-secrets init
+dotnet user-secrets set "BaseUrl" "https://your-backend-address/"
+```
+
+### Option 2: Use a Configuration File
+
+1. Create a `testsettings.development.json` file next to `testsettings.json`. This file will be ignored by git,
+ the content will not end up in pull requests, so it's safe for personal settings. Keep the file safe.
+2. Edit `testsettings.development.json` and
+ 1. Set your base address - **make sure it ends with a trailing '/' **
+
+For example:
+
+```json
+{
+ "BaseUrl": "https://localhost:40443/"
+}
+```
+
+### Option 3: Use Environment Variables
+You may also set the test settings in your environment variables. The environment variables will override the settings in the `testsettings.development.json` file.
+
+- bash:
+
+```bash
+export BaseUrl="https://localhost:40443/"
+```
+
+- PowerShell:
+
+```ps
+$env:BaseUrl = "https://localhost:40443/"
+```
diff --git a/integration-tests/ServiceInfoTests.cs b/integration-tests/ServiceInfoTests.cs
new file mode 100644
index 000000000..1051b2060
--- /dev/null
+++ b/integration-tests/ServiceInfoTests.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using CopilotChat.WebApi.Models.Response;
+using CopilotChat.WebApi.Options;
+using Xunit;
+
+namespace ChatCopilotIntegrationTests;
+
+public class ServiceInfoTests : ChatCopilotIntegrationTest
+{
+ private static readonly JsonSerializerOptions jsonOptions = new() { PropertyNameCaseInsensitive = true };
+
+ [Fact]
+ public async void GetServiceInfo()
+ {
+ await this.SetUpAuthAsync();
+
+ HttpResponseMessage response = await this.HTTPClient.GetAsync("info/");
+ response.EnsureSuccessStatusCode();
+
+ var contentStream = await response.Content.ReadAsStreamAsync();
+ var objectFromResponse = await JsonSerializer.DeserializeAsync(contentStream, jsonOptions);
+
+ Assert.NotNull(objectFromResponse);
+ Assert.False(string.IsNullOrEmpty(objectFromResponse.MemoryStore.SelectedType));
+ Assert.False(string.IsNullOrEmpty(objectFromResponse.Version));
+ }
+
+ [Fact]
+ public async void GetAuthConfig()
+ {
+ HttpResponseMessage response = await this.HTTPClient.GetAsync("authConfig/");
+ response.EnsureSuccessStatusCode();
+
+ var contentStream = await response.Content.ReadAsStreamAsync();
+ var objectFromResponse = await JsonSerializer.DeserializeAsync(contentStream, jsonOptions);
+
+ Assert.NotNull(objectFromResponse);
+ Assert.Equal(ChatAuthenticationOptions.AuthenticationType.AzureAd.ToString(), objectFromResponse.AuthType);
+ Assert.Equal(this.Configuration[AuthoritySettingName], objectFromResponse.AadAuthority);
+ Assert.Equal(this.Configuration[ClientIdSettingName], objectFromResponse.AadClientId);
+ Assert.False(string.IsNullOrEmpty(objectFromResponse.AadApiScope));
+ }
+}
diff --git a/integration-tests/SpeechTokenTests.cs b/integration-tests/SpeechTokenTests.cs
new file mode 100644
index 000000000..b3fbd6a37
--- /dev/null
+++ b/integration-tests/SpeechTokenTests.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using CopilotChat.WebApi.Models.Response;
+using Xunit;
+
+namespace ChatCopilotIntegrationTests;
+
+public class SpeechTokenTests : ChatCopilotIntegrationTest
+{
+ private static readonly JsonSerializerOptions jsonOpts = new() { PropertyNameCaseInsensitive = true };
+
+ [Fact]
+ public async void GetSpeechToken()
+ {
+ await this.SetUpAuthAsync();
+
+ HttpResponseMessage response = await this.HTTPClient.GetAsync("speechToken/");
+ response.EnsureSuccessStatusCode();
+
+ var contentStream = await response.Content.ReadAsStreamAsync();
+ var speechTokenResponse = await JsonSerializer.DeserializeAsync(contentStream, jsonOpts);
+
+ Assert.NotNull(speechTokenResponse);
+ Assert.True((speechTokenResponse.IsSuccess == true && !string.IsNullOrEmpty(speechTokenResponse.Token)) ||
+ speechTokenResponse.IsSuccess == false);
+ }
+}
diff --git a/integration-tests/StaticFiles.cs b/integration-tests/StaticFiles.cs
new file mode 100644
index 000000000..42c160ff1
--- /dev/null
+++ b/integration-tests/StaticFiles.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Xunit;
+
+namespace ChatCopilotIntegrationTests;
+
+public class StaticFiles : ChatCopilotIntegrationTest
+{
+ [Fact]
+ public async void GetStaticFiles()
+ {
+ HttpResponseMessage response = await this.HTTPClient.GetAsync("index.html");
+ response.EnsureSuccessStatusCode();
+ Assert.True(response.Content.Headers.ContentLength > 1);
+
+ response = await this.HTTPClient.GetAsync("favicon.ico");
+ response.EnsureSuccessStatusCode();
+ Assert.True(response.Content.Headers.ContentLength > 1);
+ }
+}
diff --git a/integration-tests/testsettings.json b/integration-tests/testsettings.json
new file mode 100644
index 000000000..2ec088ac6
--- /dev/null
+++ b/integration-tests/testsettings.json
@@ -0,0 +1,8 @@
+{
+ "BaseServerUrl": "https://localhost:40443/",
+ "ClientID": "YOUR_FRONTEND_CLIEND_ID",
+ "Authority": "https://login.microsoftonline.com/YOUR_TENANT_ID",
+ "TestUsername": "YOUR_TEST_USERNAME",
+ "TestPassword": "YOUR_TEST_PASSWORD", // Take care not to check your password in
+ "Scopes": "openid, offline_access, profile, api://YOUR_BACKEND_CLIENT_ID/access_as_user"
+}
diff --git a/memorypipeline/CopilotChatMemoryPipeline.csproj b/memorypipeline/CopilotChatMemoryPipeline.csproj
new file mode 100644
index 000000000..b0133d9aa
--- /dev/null
+++ b/memorypipeline/CopilotChatMemoryPipeline.csproj
@@ -0,0 +1,19 @@
+
+
+
+ CopilotChat.MemoryPipeline
+ net8.0
+ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/memorypipeline/Program.cs b/memorypipeline/Program.cs
new file mode 100644
index 000000000..3c4c50a02
--- /dev/null
+++ b/memorypipeline/Program.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using CopilotChat.Shared;
+using Microsoft.KernelMemory;
+using Microsoft.KernelMemory.Diagnostics;
+
+// ********************************************************
+// ************** SETUP ***********************************
+// ********************************************************
+
+var builder = WebApplication.CreateBuilder();
+
+IKernelMemory memory =
+ new KernelMemoryBuilder(builder.Services)
+ .FromAppSettings()
+ .Build();
+
+builder.Services.AddSingleton(memory);
+
+builder.Services.AddApplicationInsightsTelemetry();
+
+var app = builder.Build();
+
+DateTimeOffset start = DateTimeOffset.UtcNow;
+
+// Simple ping endpoint
+app.MapGet("/", () =>
+{
+ var uptime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - start.ToUnixTimeSeconds();
+ var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
+ var message = $"Memory pipeline is running. Uptime: {uptime} secs.";
+ if (!string.IsNullOrEmpty(environment))
+ {
+ message += $" Environment: {environment}";
+ }
+
+ return Results.Ok(message);
+});
+
+// ********************************************************
+// ************** START ***********************************
+// ********************************************************
+
+app.Logger.LogInformation(
+ "Starting Chat Copilot Memory pipeline service, .NET Env: {0}, Log Level: {1}",
+ Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"),
+ app.Logger.GetLogLevelName());
+
+app.Run();
diff --git a/memorypipeline/README.md b/memorypipeline/README.md
new file mode 100644
index 000000000..cdee60d52
--- /dev/null
+++ b/memorypipeline/README.md
@@ -0,0 +1,108 @@
+# Chat Copilot Memory Pipeline
+
+> **!IMPORTANT**
+> This sample is for educational purposes only and is not recommended for production deployments.
+
+> **IMPORTANT:** The pipeline will call Azure OpenAI/OpenAI which will use tokens that you may be billed for.
+
+## Introduction
+
+### Memory
+
+One of the exciting features of the Chat Copilot 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 Chat Copilot to have up-to-date knowledge of specific contexts, such as enterprise and personal data.
+
+### Memory pipeline in Chat Copilot
+
+Chat copilot integrates [Kernel Memory](https://github.com/microsoft/kernel-memory) as the memory solution provider. The memory pipeline is designed to be run as an asynchronous service. If you are expecting to import big documents that can require minutes to process or planning to carry long conversations with the bot, then you can deploy the memory pipeline as a separate service along with the [chat copilot webapi](https://github.com/microsoft/chat-copilot/tree/main/webapi).
+
+### Configuration
+
+(Optional) Before you get started, make sure you have the following requirements in place:
+
+- [An Azure Subscription](https://azure.microsoft.com/en-us/free/)
+- [Docker Desktop](https://www.docker.com/products/docker-desktop)
+
+#### Webapi
+
+Please refer to the [webapi README](../webapi/README.md).
+
+#### Memorypipeline
+
+The memorypipeline is only needed when `KernelMemory:DataIngestion:OrchestrationType` is set to `Distributed` in [../webapi/appsettings.json](./appsettings.json).
+
+- Content Storage: storage solution to save the original contents. Available options:
+ - AzureBlobs
+ - SimpleFileStorage: stores data on your local file system.
+- [Message Queue](https://learn.microsoft.com/en-us/azure/storage/queues/storage-queues-introduction): asynchronous service to service communication. Available options:
+ - AzureQueue
+ - RabbitMQ
+ - SimpleQueues: stores messages on your local file system.
+- [Vector database](https://learn.microsoft.com/en-us/semantic-kernel/memories/vector-db): storage solution for high-dimensional vectors, aka [embeddings](https://github.com/microsoft/semantic-kernel/blob/main/docs/EMBEDDINGS.md). Available options:
+ - [AzureAISearch](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search)
+ - [Qdrant](https://github.com/qdrant/qdrant)
+ - SimpleVectorDb
+ - TextFile: stores vectors on your local file system.
+ - Volatile: stores vectors in RAM.
+ > Note that do not configure the memory pipeline to use Volatile. Use volatile in the webapi only when its `KernelMemory:DataIngestion:OrchestrationType` is set to `InProcess`.
+
+##### AzureBlobs & AzureQueue
+
+> Note: Make sure to use the same resource for both the webapi and memorypipeline.
+
+1. Create a storage [account](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-create?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&tabs=azure-portal).
+2. Find the **connection string** under **Access keys** on the portal.
+3. Run the following to set up the authentication to the resources:
+ ```bash
+ dotnet user-secrets set KernelMemory:Services:AzureBlobs:Auth ConnectionString
+ dotnet user-secrets set KernelMemory:Services:AzureBlobs:ConnectionString [your secret]
+ dotnet user-secrets set KernelMemory:Services:AzureQueue:Auth ConnectionString
+ dotnet user-secrets set KernelMemory:Services:AzureQueue:ConnectionString [your secret]
+ ```
+
+##### [Azure Cognitive Search](https://learn.microsoft.com/en-us/azure/search/search-what-is-azure-search)
+
+> Note: Make sure to use the same resource for both the webapi and memorypipeline.
+
+1. Create a [search service](https://learn.microsoft.com/en-us/azure/search/search-create-service-portal).
+2. Find the **Url** under **Overview** and the **key** under **Keys** on the portal.
+3. Run the following to set up the authentication to the resources:
+ ```bash
+ dotnet user-secrets set KernelMemory:Services:AzureAISearch:Endpoint [your secret]
+ dotnet user-secrets set KernelMemory:Services:AzureAISearch:APIKey [your secret]
+ ```
+
+##### RabbitMQ
+
+> Note: Make sure to use the same queue for both webapi and memorypipeline
+
+Run the following:
+
+```
+docker run -it --rm --name rabbitmq \
+ -e RABBITMQ_DEFAULT_USER=user \
+ -e RABBITMQ_DEFAULT_PASS=password \
+ -p 5672:5672 \
+ rabbitmq:3
+```
+
+##### Qdrant
+
+> Note: Make sure to use the same vector storage for both webapi and memorypipeline
+
+```
+docker run -it --rm --name qdrant \
+-p 6333:6333 \
+qdrant/qdrant
+```
+
+> To stop the container, in another terminal window run
+
+```
+docker container stop [name]
+docker container rm [name]
+```
diff --git a/memorypipeline/appsettings.json b/memorypipeline/appsettings.json
new file mode 100644
index 000000000..6f1755a79
--- /dev/null
+++ b/memorypipeline/appsettings.json
@@ -0,0 +1,227 @@
+{
+ //
+ // Kernel Memory configuration - https://github.com/microsoft/kernel-memory
+ // - DocumentStorageType is the storage configuration for memory transfer: "AzureBlobs" or "SimpleFileStorage"
+ // - TextGeneratorType is the AI completion service configuration: "AzureOpenAIText" or "OpenAI"
+ // - ImageOcrType is the image OCR configuration: "None" or "AzureAIDocIntel" or "Tesseract"
+ // - DataIngestion is the configuration section for data ingestion pipelines.
+ // - Retrieval is the configuration section for memory retrieval.
+ // - Services is the configuration sections for various memory settings.
+ //
+ "KernelMemory": {
+ "DocumentStorageType": "SimpleFileStorage",
+ "TextGeneratorType": "AzureOpenAIText",
+ "ImageOcrType": "None",
+ // Data ingestion pipelines configuration.
+ // - OrchestrationType is the pipeline orchestration configuration : "InProcess" or "Distributed"
+ // InProcess: in process .NET orchestrator, synchronous/no queues
+ // Distributed: asynchronous queue based orchestrator
+ // - DistributedOrchestration is the detailed configuration for OrchestrationType=Distributed
+ // - EmbeddingGeneratorTypes is the list of embedding generator types
+ // - MemoryDbTypes is the list of vector database types
+ "DataIngestion": {
+ "OrchestrationType": "Distributed",
+ //
+ // Detailed configuration for OrchestrationType=Distributed.
+ // - QueueType is the queue configuration: "AzureQueue" or "RabbitMQ" or "SimpleQueues"
+ //
+ "DistributedOrchestration": {
+ "QueueType": "SimpleQueues"
+ },
+ // Multiple generators can be used, e.g. for data migration, A/B testing, etc.
+ "EmbeddingGeneratorTypes": [
+ "AzureOpenAIEmbedding"
+ ],
+ // Vectors can be written to multiple storages, e.g. for data migration, A/B testing, etc.
+ "MemoryDbTypes": [
+ "SimpleVectorDb"
+ ]
+ },
+ //
+ // Memory retrieval configuration - A single EmbeddingGenerator and VectorDb.
+ // - MemoryDbType: Vector database configuration: "SimpleVectorDb" or "AzureAISearch" or "Qdrant"
+ // - EmbeddingGeneratorType: Embedding generator configuration: "AzureOpenAIEmbedding" or "OpenAI"
+ //
+ "Retrieval": {
+ "MemoryDbType": "SimpleVectorDb",
+ "EmbeddingGeneratorType": "AzureOpenAIEmbedding"
+ },
+ //
+ // Configuration for the various services used by kernel memory and semantic kernel.
+ // Section names correspond to type specified in KernelMemory section. All supported
+ // sections are listed below for reference. Only referenced sections are required.
+ //
+ "Services": {
+ //
+ // File based storage for local/development use.
+ // - Directory is the location where files are stored.
+ //
+ "SimpleFileStorage": {
+ "Directory": "../tmp/cache"
+ },
+ //
+ // File based queue for local/development use.
+ // - Directory is the location where messages are stored.
+ //
+ "SimpleQueues": {
+ "Directory": "../tmp/queues"
+ },
+ //
+ // File based vector database for local/development use.
+ // - StorageType is the storage configuration: "Disk" or "Volatile"
+ // - Directory is the location where data is stored.
+ //
+ "SimpleVectorDb": {
+ "StorageType": "Disk",
+ "Directory": "../tmp/database"
+ },
+ //
+ // Azure blob storage for the memory pipeline
+ // - Auth is the authentication type: "ConnectionString" or "AzureIdentity".
+ // - ConnectionString is the connection string for the Azure Storage account and only utilized when Auth=ConnectionString.
+ // - Account is the name of the Azure Storage account and only utilized when Auth=AzureIdentity.
+ // - Container is the name of the Azure Storage container used for file storage.
+ // - EndpointSuffix is used only for country clouds.
+ //
+ "AzureBlobs": {
+ "Auth": "ConnectionString",
+ //"ConnectionString": "", // dotnet user-secrets set "KernelMemory:Services:AzureBlobs:ConnectionString" "MY_AZUREBLOB_CONNECTIONSTRING"
+ //"Account": "",
+ "Container": "chatmemory"
+ //"EndpointSuffix": "core.windows.net"
+ },
+ //
+ // Azure storage queue configuration for distributed memory pipeline
+ // - Auth is the authentication type: "ConnectionString" or "AzureIdentity".
+ // - ConnectionString is the connection string for the Azure Storage account and only utilized when Auth=ConnectionString.
+ // - Account is the name of the Azure Storage account and only utilized when Auth=AzureIdentity.
+ // - EndpointSuffix is used only for country clouds.
+ //
+ "AzureQueue": {
+ "Auth": "ConnectionString"
+ //"ConnectionString": "", // dotnet user-secrets set "KernelMemory:Services:AzureQueue:ConnectionString" "MY_AZUREQUEUE_CONNECTIONSTRING"
+ //"Account": "",
+ //"EndpointSuffix": "core.windows.net"
+ },
+ //
+ // RabbitMq queue configuration for distributed memory pipeline
+ // - Username is the RabbitMq user name.
+ // - Password is the RabbitMq use password
+ // - Host is the RabbitMq service host name or address.
+ // - Port is the RabbitMq service port.
+ //
+ "RabbitMq": {
+ //"Username": "user", // dotnet user-secrets set "KernelMemory:Services:RabbitMq:Username" "MY_RABBITMQ_USER"
+ //"Password": "", // dotnet user-secrets set "KernelMemory:Services:RabbitMq:Password" "MY_RABBITMQ_KEY"
+ "Host": "127.0.0.1",
+ "Port": "5672"
+ },
+ //
+ // Azure Cognitive Search configuration for semantic services.
+ // - Auth is the authentication type: "APIKey" or "AzureIdentity".
+ // - APIKey is the key generated to access the service.
+ // - Endpoint is the service endpoint url.
+ //
+ "AzureAISearch": {
+ "Auth": "ApiKey",
+ //"APIKey": "", // dotnet user-secrets set "KernelMemory:Services:AzureAISearch:APIKey" "MY_ACS_KEY"
+ "Endpoint": ""
+ },
+ //
+ // Qdrant configuration for semantic services.
+ // - APIKey is the key generated to access the service.
+ // - Endpoint is the service endpoint url.
+ //
+ "Qdrant": {
+ //"APIKey": "", // dotnet user-secrets set "KernelMemory:Services:Qdrant:APIKey" "MY_QDRANT_KEY"
+ "Endpoint": "http://127.0.0.1:6333"
+ },
+ //
+ // AI completion configuration for Azure AI services.
+ // - Auth is the authentication type: "APIKey" or "AzureIdentity".
+ // - APIKey is the key generated to access the service.
+ // - Endpoint is the service endpoint url.
+ // - Deployment is a completion model (e.g., gpt-4, gpt-4o).
+ // - APIType is the type of completion model: "ChatCompletion" or "TextCompletion".
+ // - MaxRetries is the maximum number of retries for a failed request.
+ //
+ "AzureOpenAIText": {
+ "Auth": "ApiKey",
+ //"APIKey": "", // dotnet user-secrets set "KernelMemory:Services:AzureOpenAIText:APIKey" "MY_AZUREOPENAI_KEY"
+ "Endpoint": "",
+ "Deployment": "gpt-4o",
+ "APIType": "ChatCompletion",
+ "MaxRetries": 10
+ },
+ //
+ // AI embedding configuration for Azure OpenAI services.
+ // - Auth is the authentication type: "APIKey" or "AzureIdentity".
+ // - APIKey is the key generated to access the service.
+ // - Endpoint is the service endpoint url.
+ // - Deployment is an embedding model (e.g., text-embedding-ada-002).
+ //
+ "AzureOpenAIEmbedding": {
+ "Auth": "ApiKey",
+ // "APIKey": "", // dotnet user-secrets set "KernelMemory:Services:AzureOpenAIEmbedding:APIKey" "MY_AZUREOPENAI_KEY"
+ "Endpoint": ".openai.azure.com/",
+ "Deployment": "text-embedding-ada-002"
+ },
+ //
+ // AI completion and embedding configuration for OpenAI services.
+ // - TextModel is a completion model (e.g., gpt-4, gpt-4o).
+ // - EmbeddingModelSet is an embedding model (e.g., "text-embedding-ada-002").
+ // - APIKey is the key generated to access the service.
+ // - OrgId is the optional OpenAI organization id/key.
+ // - MaxRetries is the maximum number of retries for a failed request.
+ //
+ "OpenAI": {
+ "TextModel": "gpt-3.5-turbo",
+ "EmbeddingModel": "text-embedding-ada-002",
+ //"APIKey": "", // dotnet user-secrets set "KernelMemory:Services:OpenAI:APIKey" "MY_OPENAI_KEY"
+ "OrgId": "",
+ "MaxRetries": 10
+ },
+ //
+ // Azure Form Recognizer configuration for memory pipeline OCR.
+ // - Auth is the authentication configuration: "APIKey" or "AzureIdentity".
+ // - APIKey is the key generated to access the service.
+ // - Endpoint is the service endpoint url.
+ //
+ "AzureAIDocIntel": {
+ "Auth": "APIKey",
+ //"APIKey": "", // dotnet user-secrets set "KernelMemory:Services:AzureAIDocIntel:APIKey" "MY_AZURE_AI_DOC_INTEL_KEY"
+ "Endpoint": ""
+ },
+ //
+ // Tesseract configuration for memory pipeline OCR.
+ // - Language is the language supported by the data file.
+ // - FilePath is the path to the data file.
+ //
+ // Note: When using Tesseract OCR Support (In order to upload image file formats such as png, jpg and tiff):
+ // 1. Obtain language data files here: https://github.com/tesseract-ocr/tessdata .
+ // 2. Add these files to your `data` folder or the path specified in the "FilePath" property and set the "Copy to Output Directory" value to "Copy if newer".
+ //
+ "Tesseract": {
+ "Language": "eng",
+ "FilePath": "./data"
+ }
+ }
+ },
+ // Logging configuration
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ },
+ "ApplicationInsights": {
+ "LogLevel": {
+ "Default": "Information"
+ }
+ }
+ },
+ "AllowedHosts": "*",
+ // Application Insights configuration
+ "ApplicationInsights": {
+ "ConnectionString": null
+ }
+}
\ No newline at end of file
diff --git a/plugins/OBO/README.md b/plugins/OBO/README.md
new file mode 100644
index 000000000..c550ca118
--- /dev/null
+++ b/plugins/OBO/README.md
@@ -0,0 +1,103 @@
+# Ms Graph plugin using On-Behalf-Of Flow for Ms Graph APIs
+
+This repository contains a sample Plugin that uses the On-Behalf-Of (OBO) flow to call Microsoft Graph APIs.
+
+In this document we will refer to the client app as the WebApp (src/webapp), the middle-tier app as the WebApi (src/webapi) and the backend resource as the Ms Graph Api.
+
+> **IMPORTANT:** This sample is for educational purposes only and is not recommended for production deployments.
+
+> **NOTE:** This plugin was implemented as a native Kernel function, in the WebAPI code. This is not an implementation of the OpenAI plugin spec.
+
+> **NOTE:** This plugin works better GTP-4 or GTP-4-Turbo as these models works better with the function model.
+
+## Prerequisites
+
+- Enable backend authentication via Azure AD as described in the main [`README.md`](../../README.md) file.
+
+## Setup Instructions
+
+1. **Add the WebApp to the "known client application list" in the WebApi app registration.**
+
+ - Go to the WebApp app registration in your tenant and copy the Application Id (Client ID).
+ - Go to the WebAPI app registration in your tenant.
+ - Click on "Manifest" option and add an entry for the `knownClientApplications` attribute using the Application Id (Client ID) of the WebApp registration as described in this [document](https://learn.microsoft.com/en-us/entra/identity-platform/reference-app-manifest#knownclientapplications-attribute)
+
+ - Save the manifest.
+
+2. **Give the WebApi the delegated permissions.**
+
+ - Go to the WebApi API app registration.
+ - Select the "API permissions" option.
+ - Click on "+ Add Permission" option and choose the "Microsoft Graph" option.
+ - Select "Delegated permission" and choose all the delegated permissions needed.
+ - Click on "Add Permissions".
+ - As the UI does not implement incremental consent, you need to grant "Admin Consent" to the new permissions added.
+
+3. **Create a Client Secret for the WebAPI app registration OBO Configuration.**
+
+ - In the WebAPI app registration click on "Certificates & Secrets".
+ - Create a new secret by clicking in the "+ New client secret", enter a description and the expiration days.
+ - Copy the Client Secret and the Application Id (Client ID) to use in the WebAPI appsetting configuration.
+
+4. **Change the WebAPI `appsettings.json` file.**
+ - Add your OBO configuration values in the OnBehalfOfAuth section as shown below. The ClientId must be the WebAPI Application Id (Client ID).
+
+```json
+ // OBO Configuration for Plugins
+ "OnBehalfOfAuth": {
+ "Authority": "https://login.microsoftonline.com",
+ "TenantId": "[ENTER YOUR TENANT ID]",
+ "ClientId": "[ENTER YOUR CLIENT ID]",
+ "ClientSecret": "[ENTER YOUR CLIENT SECRET]"
+ }
+```
+
+5. Change the scope for the Ms Graph Obo plugin in the WebApp code
+
+ - As the UI does not implement incremental consent, you need to configure the WebApp to use the [.default scope](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent). The scope name is formed by the Application ID of the WebAPI app registration so you need to update it with the WebApi Application ID (Client ID).
+
+ - Change the Constants.ts file located in the webapp/src folder, add the msGraphOboScopes entry with the WebApi Application Id, as shown below:
+
+ ```typescript
+ plugins: {
+ msGraphOboScopes: ['api://[ENTER THE WE API APPLICATION ID]/.default'],
+ }
+ ```
+
+## Test Instructions
+
+1. Login to the app
+
+![Login Step](./test-step-1.png)
+
+2. Enable Ms Graph OBO Plugin
+
+![Plugin Step](./test-step-2.png)
+
+![Plugin Step 2](./test-step-3.png)
+
+![Plugin Step 3](./test-step-4.png)
+
+3. Update the Persona Meta Prompt with the following text:
+
+ ```text
+ This is a chat between an intelligent AI bot named Copilot and one or more participants. SK stands for Semantic Kernel, the AI platform used to build the bot. The AI was trained on data through 2021 and is not aware of events that have occurred since then. The bot has the ability to call Graph APIs using the MS Graph OBO tool to fetch real-time data. The user must first enable the plugin. To call a Graph API, the bot would call the \\"CallGraphApiTasksAsync\\" function, and provide the Graph API URL with the ODATA query and its required scopes as a list as arguments. The plugin will automatically handle authentication. Otherwise, the bot has no ability to access data on the Internet, so it should not claim that it can or say that it will go and look things up. Try to be concise with your answers, though it is not required. Knowledge cutoff: {{$knowledgeCutoff}} / Current date: {{TimePlugin.Now}}.
+ ```
+
+![Persona Step 1](./test-step-5.png)
+
+4. Run a prompt to check if the bot understands that can can a graph API and then ask to run a query by providing a sample
+
+- Hi! Can you call a graph API for me?
+
+- Please get the list of applications in my tenant.
+ You can call the Graph API: `https://graph.microsoft.com/v1.0/applications$select=appId,identifierUris,displayName,publisherDomain,signInAudience`
+ Required scope: Application.Read.All
+
+![Check Step 1](./test-step-6.png)
+
+5. After the sample prompt the bot will execute any graph api query without the need of indicating the graph api, odata query or scopes
+
+- Please get the ObjectID of my user
+
+![Check Step 2](./test-step-7.png)
diff --git a/plugins/OBO/test-step-1.png b/plugins/OBO/test-step-1.png
new file mode 100644
index 000000000..6218369f8
Binary files /dev/null and b/plugins/OBO/test-step-1.png differ
diff --git a/plugins/OBO/test-step-2.png b/plugins/OBO/test-step-2.png
new file mode 100644
index 000000000..dd4e6f1d5
Binary files /dev/null and b/plugins/OBO/test-step-2.png differ
diff --git a/plugins/OBO/test-step-3.png b/plugins/OBO/test-step-3.png
new file mode 100644
index 000000000..46628d5b8
Binary files /dev/null and b/plugins/OBO/test-step-3.png differ
diff --git a/plugins/OBO/test-step-4.png b/plugins/OBO/test-step-4.png
new file mode 100644
index 000000000..ebe0ada50
Binary files /dev/null and b/plugins/OBO/test-step-4.png differ
diff --git a/plugins/OBO/test-step-5.png b/plugins/OBO/test-step-5.png
new file mode 100644
index 000000000..426e9b62f
Binary files /dev/null and b/plugins/OBO/test-step-5.png differ
diff --git a/plugins/OBO/test-step-6.png b/plugins/OBO/test-step-6.png
new file mode 100644
index 000000000..690d39146
Binary files /dev/null and b/plugins/OBO/test-step-6.png differ
diff --git a/plugins/OBO/test-step-7.png b/plugins/OBO/test-step-7.png
new file mode 100644
index 000000000..e2ca9f3aa
Binary files /dev/null and b/plugins/OBO/test-step-7.png differ
diff --git a/plugins/README.md b/plugins/README.md
new file mode 100644
index 000000000..28ce8162a
--- /dev/null
+++ b/plugins/README.md
@@ -0,0 +1,51 @@
+# Plugins
+
+> **IMPORTANT:** This sample is for educational purposes only and is not recommended for production deployments.
+
+Plugins are cool! They allow Chat Copilot to talk to the internet. Read more about plugins here [Understanding AI plugins in Semantic Kernel](https://learn.microsoft.com/en-us/semantic-kernel/ai-orchestration/plugins/?tabs=Csharp) and here [ChatGPT Plugins](https://platform.openai.com/docs/plugins/introduction).
+
+## Available Plugins
+
+> These plugins in this project can be optionally deployed with the Chat Copilot [WebApi](../webapi/README.md)
+
+- [WebSearcher](./web-searcher/README.md): A plugin that allows the chat bot to perform Bing search.
+- More to come. Stay tuned!
+
+## Third Party plugins
+
+You can also configure Chat Copilot to use third party plugins.
+
+> All no-auth plugins will be supported.
+
+> All service-level-auth and user-level-auth plugins will be supported.
+
+> OAuth plugins will NOT be supported.
+
+Read more about plugin authentication here: [Plugin authentication](https://platform.openai.com/docs/plugins/authentication)
+
+## Plugin Configuration in Chat Copilot
+
+### Prerequisites
+
+1. The name of your plugin. This should be identical to the `NameForHuman` in your plugin manifest.
+ > Please refer to OpenAI for the [manifest requirements](https://platform.openai.com/docs/plugins/getting-started/plugin-manifest).
+2. Url of your plugin.
+ > This should be the root url to your API. Not the manifest url nor the OpenAPI spec url.
+3. (Optional) Key of the plugin if it requires one.
+
+### Local dev
+
+In `appsettings.json` or `appsettings.development.json` under `../webapi/`, add your plugin to the existing **Plugins** list with the required information.
+
+### Deployment
+
+1. Go to your webapi resource in Azure portal.
+2. Go to **Configuration** -> **Application settings**.
+3. Look for Plugins:[*index*]:\* in the names that has the largest index.
+4. Add the following names and their corresponding values:
+
+```
+Plugins[*index+1*]:Name
+Plugins[*index+1*]:Url
+Plugins[*index+1*]:Key (only if the plugin requires it)
+```
diff --git a/plugins/shared/PluginApi.cs b/plugins/shared/PluginApi.cs
new file mode 100644
index 000000000..5335aa732
--- /dev/null
+++ b/plugins/shared/PluginApi.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Plugins.PluginShared;
+
+///
+/// This class represents the plugin API specification.
+///
+public class PluginApi
+{
+ ///
+ /// The API specification
+ ///
+ public string Type { get; set; } = "openapi";
+
+ ///
+ /// URL used to fetch the specification
+ ///
+ public string Url { get; set; } = string.Empty;
+}
diff --git a/plugins/shared/PluginAuth.cs b/plugins/shared/PluginAuth.cs
new file mode 100644
index 000000000..445038ebf
--- /dev/null
+++ b/plugins/shared/PluginAuth.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Plugins.PluginShared;
+
+///
+/// This class represents the OpenAI plugin authentication schema.
+///
+public class PluginAuth
+{
+ ///
+ /// Tokens for API key authentication
+ ///
+ public class VerificationTokens
+ {
+ ///
+ /// The API key
+ ///
+ public string OpenAI { get; set; } = string.Empty;
+ }
+
+ ///
+ /// The authentication schema
+ /// Supported values: none, service_http, user_http
+ ///
+ public string Type { get; set; } = "none";
+
+ ///
+ /// Manifest schema version
+ ///
+ [JsonPropertyName("authorization_type")]
+ public string AuthorizationType { get; } = "bearer";
+
+ ///
+ /// Tokens for API key authentication
+ ///
+ [JsonPropertyName("verification_tokens")]
+ public VerificationTokens Tokens { get; set; } = new VerificationTokens();
+}
diff --git a/plugins/shared/PluginManifest.cs b/plugins/shared/PluginManifest.cs
new file mode 100644
index 000000000..0223233df
--- /dev/null
+++ b/plugins/shared/PluginManifest.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Plugins.PluginShared;
+
+///
+/// This class represents the OpenAI plugin manifest:
+/// https://platform.openai.com/docs/plugins/getting-started/plugin-manifest
+///
+public class PluginManifest
+{
+ ///
+ /// Manifest schema version
+ ///
+ [JsonPropertyName("schema_version")]
+ public string SchemaVersion { get; set; } = "v1";
+
+ ///
+ /// The name of the plugin that the model will use
+ ///
+ [JsonPropertyName("name_for_model")]
+ public string NameForModel { get; set; } = string.Empty;
+
+ ///
+ /// Human-readable name of the plugin
+ ///
+ [JsonPropertyName("name_for_human")]
+ public string NameForHuman { get; set; } = string.Empty;
+
+ ///
+ /// Description of the plugin that the model will use
+ ///
+ [JsonPropertyName("description_for_model")]
+ public string DescriptionForModel { get; set; } = string.Empty;
+
+ ///
+ /// Human-readable description of the plugin
+ ///
+ [JsonPropertyName("description_for_human")]
+ public string DescriptionForHuman { get; set; } = string.Empty;
+
+ ///
+ /// The authentication schema
+ ///
+ public PluginAuth Auth { get; set; } = new PluginAuth();
+
+ ///
+ /// The API specification
+ ///
+ public PluginApi Api { get; set; } = new PluginApi();
+
+ ///
+ /// URL used to fetch the logo
+ ///
+ [JsonPropertyName("logo_url")]
+ public string LogoUrl { get; set; } = string.Empty;
+
+ ///
+ /// Email contact for safety/moderation, support and deactivation
+ ///
+ [JsonPropertyName("contact_email")]
+ public string ContactEmail { get; set; } = string.Empty;
+
+ ///
+ /// Redirect URL for users to get more information about the plugin
+ ///
+ [JsonPropertyName("legal_info_url")]
+ public string LegalInfoUrl { get; set; } = string.Empty;
+
+ ///
+ /// "Bearer" or "Basic"
+ ///
+ public string HttpAuthorizationType { get; set; } = string.Empty;
+}
diff --git a/plugins/shared/PluginShared.csproj b/plugins/shared/PluginShared.csproj
new file mode 100644
index 000000000..1c648a957
--- /dev/null
+++ b/plugins/shared/PluginShared.csproj
@@ -0,0 +1,8 @@
+
+
+
+ net6.0
+ Plugins.PluginShared
+
+
+
\ No newline at end of file
diff --git a/plugins/web-searcher/Icons/bing.png b/plugins/web-searcher/Icons/bing.png
new file mode 100644
index 000000000..3da002c0c
Binary files /dev/null and b/plugins/web-searcher/Icons/bing.png differ
diff --git a/plugins/web-searcher/Models/BingSearchResponse.cs b/plugins/web-searcher/Models/BingSearchResponse.cs
new file mode 100644
index 000000000..02e2ebc95
--- /dev/null
+++ b/plugins/web-searcher/Models/BingSearchResponse.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json.Serialization;
+
+namespace Plugins.WebSearcher.Models;
+
+///
+/// Defines a webpage that is relevant to the query.
+///
+internal sealed class WebPage
+{
+ ///
+ /// The name of the webpage.
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// The URL to the webpage.
+ ///
+ [JsonPropertyName("url")]
+ public string Url { get; set; } = string.Empty;
+
+ ///
+ /// A snippet of text from the webpage that describes its contents.
+ ///
+ [JsonPropertyName("snippet")]
+ public string Snippet { get; set; } = string.Empty;
+}
+
+///
+/// Defines a list of relevant webpage links.
+///
+internal sealed class WebPages
+{
+ ///
+ /// A list of webpages that are relevant to the query.
+ ///
+ [JsonPropertyName("value")]
+ public WebPage[]? Value { get; set; }
+}
+
+///
+/// The Bing's top-level object for search requests that succeed.
+///
+internal sealed class BingSearchResponse
+{
+ ///
+ /// A list of webpages that are relevant to the search query.
+ ///
+ [JsonPropertyName("webPages")]
+ public WebPages? WebPages { get; set; }
+}
diff --git a/plugins/web-searcher/Models/PluginConfig.cs b/plugins/web-searcher/Models/PluginConfig.cs
new file mode 100644
index 000000000..2d46115d5
--- /dev/null
+++ b/plugins/web-searcher/Models/PluginConfig.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Plugins.WebSearcher.Models;
+
+///
+/// The plugin configuration.
+///
+public class PluginConfig
+{
+ ///
+ /// The Bing API base URL.
+ ///
+ public string BingApiBaseUrl { get; set; } = "https://api.bing.microsoft.com/v7.0/search";
+
+ ///
+ /// The Bing API key.
+ ///
+ public string BingApiKey { get; set; } = string.Empty;
+}
diff --git a/plugins/web-searcher/PluginEndpoint.cs b/plugins/web-searcher/PluginEndpoint.cs
new file mode 100644
index 000000000..020450683
--- /dev/null
+++ b/plugins/web-searcher/PluginEndpoint.cs
@@ -0,0 +1,192 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Net;
+using System.Text.Json;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Attributes;
+using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+using Microsoft.OpenApi.Models;
+using Plugins.PluginShared;
+using Plugins.WebSearcher.Models;
+
+namespace Plugins.WebSearcher;
+
+///
+/// Plugin endpoints
+///
+public class PluginEndpoint
+{
+ private readonly PluginConfig _config;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ public PluginEndpoint(PluginConfig config, ILogger logger)
+ {
+ this._config = config;
+ this._logger = logger;
+ }
+
+ ///
+ /// Gets the plugin manifest.
+ ///
+ /// The http request data.
+ /// The manifest in Json
+ [Function("WellKnownAIPluginManifest")]
+ public async Task WellKnownAIPluginManifestAsync(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = ".well-known/ai-plugin.json")]
+ HttpRequestData req)
+ {
+ var pluginManifest = new PluginManifest()
+ {
+ NameForModel = "WebSearcher",
+ NameForHuman = "WebSearcher",
+ DescriptionForModel = "Searches the web",
+ DescriptionForHuman = "Searches the web",
+ Auth = new PluginAuth()
+ {
+ Type = "user_http"
+ },
+ Api = new PluginApi()
+ {
+ Type = "openapi",
+ Url = $"{req.Url.Scheme}://{req.Url.Host}:{req.Url.Port}/swagger.json"
+ },
+ LogoUrl = $"{req.Url.Scheme}://{req.Url.Host}:{req.Url.Port}/.well-known/icon",
+ };
+
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ await response.WriteAsJsonAsync(pluginManifest);
+ return response;
+ }
+
+ ///
+ /// Gets the plugin's icon.
+ ///
+ /// The http request data.
+ /// The icon.
+ [Function("Icon")]
+ public async Task IconAsync(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = ".well-known/icon")]
+ HttpRequestData req)
+ {
+ if (!File.Exists("./Icons/bing.png"))
+ {
+ return req.CreateResponse(HttpStatusCode.NotFound);
+ }
+
+ using (var stream = new FileStream("./Icons/bing.png", FileMode.Open))
+ {
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "image/png");
+ await stream.CopyToAsync(response.Body);
+ return response;
+ }
+ }
+
+ ///
+ /// Search the web for the given query.
+ ///
+ /// The http request data.
+ /// A string representing the search result.
+ [OpenApiOperation(operationId: "Search", tags: ["WebSearchfunction"], Description = "Searches the web for the given query.")]
+ [OpenApiSecurity("function_key", SecuritySchemeType.ApiKey, Name = "x-functions-key", In = OpenApiSecurityLocationType.Header)]
+ [OpenApiParameter(name: "Query", In = ParameterLocation.Query, Required = true, Type = typeof(string), Description = "The query")]
+ [OpenApiParameter(name: "NumResults", In = ParameterLocation.Query, Required = true, Type = typeof(int), Description = "The maximum number of results to return")]
+ [OpenApiParameter(name: "Offset", In = ParameterLocation.Query, Required = false, Type = typeof(int), Description = "The number of results to skip")]
+ [OpenApiParameter(name: "Site", In = ParameterLocation.Query, Required = false, Type = typeof(string), Description = "The specific site to search within")]
+ [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "text/plain", bodyType: typeof(string), Description = "Returns a collection of search results with the name, URL and snippet for each.")]
+ [OpenApiResponseWithoutBody(statusCode: HttpStatusCode.BadRequest, Description = "Invalid query")]
+ [Function("WebSearch")]
+ public async Task WebSearchAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "search")] HttpRequestData req)
+ {
+ var queryParams = QueryHelpers.ParseQuery(req.Url.Query);
+
+ string query = queryParams.TryGetValue("Query", out StringValues q1) ? q1.ToString() : string.Empty;
+ int numResults = queryParams.TryGetValue("NumResults", out StringValues q2) && int.TryParse(q2, out int q2Value) ? q2Value : 0;
+ int offset = queryParams.TryGetValue("Offset", out StringValues q3) && int.TryParse(q3, out var q3Value) ? q3Value : 0;
+ string site = queryParams.TryGetValue("Site", out StringValues q4) ? q4.ToString() : string.Empty;
+
+ if (string.IsNullOrWhiteSpace(query))
+ {
+ return await this.CreateBadRequestResponseAsync(req, "Empty query.");
+ }
+
+ if (numResults <= 0)
+ {
+ return await this.CreateBadRequestResponseAsync(req, "Invalid number of results.");
+ }
+
+ if (string.IsNullOrWhiteSpace(site))
+ {
+ this._logger.LogDebug("Searching the web for '{0}'", query);
+ }
+ else
+ {
+ this._logger.LogDebug("Searching the web for '{0}' within '{1}'", query, site);
+ }
+
+ using (var httpClient = new HttpClient())
+ {
+ var queryString = $"?q={Uri.EscapeDataString(query)}";
+ queryString += string.IsNullOrWhiteSpace(site) ? string.Empty : $"+site:{site}";
+ queryString += $"&count={numResults}";
+ queryString += $"&offset={offset}";
+
+ var uri = new Uri($"{this._config.BingApiBaseUrl}{queryString}");
+ this._logger.LogDebug("Sending request to {0}", uri);
+
+ httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", this._config.BingApiKey);
+
+ var bingResponse = await httpClient.GetStringAsync(uri);
+
+ this._logger.LogDebug("Search completed. Response: {0}", bingResponse);
+
+ BingSearchResponse? data = JsonSerializer.Deserialize(bingResponse);
+ WebPage[]? results = data?.WebPages?.Value;
+
+ var responseText = results == null
+ ? "No results found."
+ : string.Join(",",
+ results.Select(r => $"[NAME]{r.Name}[END NAME] [URL]{r.Url}[END URL] [SNIPPET]{r.Snippet}[END SNIPPET]"));
+
+ return await this.CreateOkResponseAsync(req, responseText);
+ }
+ }
+
+ ///
+ /// Creates an OK response containing texts with the given content.
+ ///
+ /// The http request data.
+ /// The content.
+ ///
+ private async Task CreateOkResponseAsync(HttpRequestData req, string content)
+ {
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
+ await response.WriteStringAsync(content);
+ return response;
+ }
+
+ ///
+ /// Creates a bad request response containing the given error message.
+ ///
+ /// The http request data.
+ /// The error message.
+ ///
+ private async Task CreateBadRequestResponseAsync(HttpRequestData req, string errMsg)
+ {
+ this._logger.LogError(errMsg);
+
+ var response = req.CreateResponse(HttpStatusCode.BadRequest);
+ response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
+ await response.WriteStringAsync(errMsg);
+ return response;
+ }
+}
diff --git a/plugins/web-searcher/Program.cs b/plugins/web-searcher/Program.cs
new file mode 100644
index 000000000..edf0bc39d
--- /dev/null
+++ b/plugins/web-searcher/Program.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.Json;
+using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Abstractions;
+using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Configurations;
+using Microsoft.Azure.WebJobs.Extensions.OpenApi.Core.Enums;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.OpenApi.Models;
+using Plugins.WebSearcher.Models;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults()
+ .ConfigureAppConfiguration(configuration =>
+ {
+ // `ConfigureFunctionsWorkerDefaults` already adds environment variables as a source.
+ configuration
+ .AddUserSecrets(optional: true)
+ .AddJsonFile(path: "local.settings.json", optional: true, reloadOnChange: true);
+ })
+ .ConfigureServices(services =>
+ {
+ services.Configure(options =>
+ {
+ // `ConfigureFunctionsWorkerDefaults` sets the default to ignore casing already.
+ options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ });
+
+ var pluginConfig = services.BuildServiceProvider().GetService()?.GetSection(nameof(PluginConfig)).Get();
+ services.AddSingleton(pluginConfig!);
+
+ services.AddSingleton(_ =>
+ {
+ var options = new OpenApiConfigurationOptions()
+ {
+ Info = new OpenApiInfo()
+ {
+ Version = "1.0.0",
+ Title = "Web Searcher Plugin",
+ Description = "This plugin is capable of searching the internet."
+ },
+ Servers = DefaultOpenApiConfigurationOptions.GetHostNames(),
+ OpenApiVersion = OpenApiVersionType.V3,
+ IncludeRequestingHostName = true,
+ ForceHttps = false,
+ ForceHttp = false,
+ };
+
+ return options;
+ });
+ })
+ .Build();
+
+host.Run();
diff --git a/plugins/web-searcher/README.md b/plugins/web-searcher/README.md
new file mode 100644
index 000000000..e9709a6ca
--- /dev/null
+++ b/plugins/web-searcher/README.md
@@ -0,0 +1,48 @@
+# WebSearcher OpenAI Plugin
+
+> **IMPORTANT:** This sample is for educational purposes only and is not recommended for production deployments.
+
+An OpenAI Plugin that can be used to search the internet using Bing.
+
+## Prerequisites:
+
+1. A [Bing Search](https://learn.microsoft.com/en-us/bing/search-apis/bing-web-search/create-bing-search-service-resource) Resource.
+2. [Azure Function core tool](https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-csharp?tabs=windows%2Cazure-cli#install-the-azure-functions-core-tools).
+3. (Optional) An [Azure Function](https://learn.microsoft.com/en-us/azure/azure-functions/functions-get-started?pivots=programming-language-csharp) resource for deployment.
+
+## Local dev
+
+1. Open `local.settings.json` and enter the `BingApiKey`. The ApiKey can be found in your Bing Search resource in Azure portal.
+2. Run the following command in a new terminal window
+
+```
+> func start
+```
+
+## Deploy to Azure
+
+1. Create an Azure function resource.
+2. Create and set `PluginConfig:BingApiKey` to the Bing Api key in **Configuration** -> **Application settings** in Azure portal.
+3. Publish the package by running
+
+```
+dotnet publish --output ./bin/publish --configuration "Release"
+```
+
+4. Compress the published binary to a zip by running
+
+```
+Compress-Archive -Path .\bin\publish\* -DestinationPath .\bin\publish.zip
+```
+
+5. Deploy to Azure via zip push
+
+```
+az functionapp deployment source config-zip -g [your resource group name] -n [your function app name] --src .\bin\publish.zip
+```
+
+## Usage
+
+You can test the functionality by using the Swagger UI. For example: "{your function url}/swagger/ui"
+
+> Note: The function app does't require any authentication when running locally.
diff --git a/plugins/web-searcher/WebSearcher.csproj b/plugins/web-searcher/WebSearcher.csproj
new file mode 100644
index 000000000..6864f8e40
--- /dev/null
+++ b/plugins/web-searcher/WebSearcher.csproj
@@ -0,0 +1,37 @@
+
+
+ net8.0
+ v4
+ Exe
+ Plugins.WebSearcher
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+ PreserveNewest
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/plugins/web-searcher/host.json b/plugins/web-searcher/host.json
new file mode 100644
index 000000000..3ca80ebc2
--- /dev/null
+++ b/plugins/web-searcher/host.json
@@ -0,0 +1,22 @@
+{
+ "version": "2.0",
+ "functionTimeout": "00:05:00",
+ "logging": {
+ "fileLoggingMode": "DebugOnly",
+ "logLevel": {
+ "default": "Information",
+ "WebSearcher.PluginEndpoint": "Information"
+ },
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ }
+ }
+ },
+ "extensions": {
+ "http": {
+ "routePrefix": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/web-searcher/local.settings.json b/plugins/web-searcher/local.settings.json
new file mode 100644
index 000000000..0e06eb253
--- /dev/null
+++ b/plugins/web-searcher/local.settings.json
@@ -0,0 +1,13 @@
+{
+ "IsEncrypted": false,
+ "Values": {
+ "AzureWebJobsStorage": "UseDevelopmentStorage=none",
+ "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
+ },
+ "Host": {
+ "CORS": "*"
+ },
+ "PluginConfig": {
+ "BingApiKey": ""
+ }
+}
\ No newline at end of file
diff --git a/scripts/.env b/scripts/.env
new file mode 100644
index 000000000..dcac41131
--- /dev/null
+++ b/scripts/.env
@@ -0,0 +1,15 @@
+# Default environment file to be read by scripts
+
+# Default values
+ENV_COMPLETION_MODEL_OPEN_AI="gpt-4o"
+ENV_COMPLETION_MODEL_AZURE_OPEN_AI="gpt-4o"
+ENV_EMBEDDING_MODEL="text-embedding-ada-002"
+ENV_ASPNETCORE="Development"
+ENV_INSTANCE="https://login.microsoftonline.com"
+
+# Constants
+ENV_AZURE_OPEN_AI="AzureOpenAI"
+ENV_OPEN_AI="OpenAI"
+ENV_AZURE_AD="AzureAd"
+ENV_NONE="None"
+ENV_SCOPES="access_as_user"
diff --git a/scripts/Configure.ps1 b/scripts/Configure.ps1
index 622bb1927..94819526c 100644
--- a/scripts/Configure.ps1
+++ b/scripts/Configure.ps1
@@ -1,112 +1,192 @@
<#
.SYNOPSIS
-Configure user secrets, appsettings.Development.json, and .env for Copilot Chat.
+Configure user secrets, appsettings.Development.json, and webapp/.env for Chat Copilot.
-.PARAMETER OpenAI
-Switch to configure for OpenAI.
+.PARAMETER AIService
+The service type used: OpenAI or AzureOpenAI.
-.PARAMETER AzureOpenAI
-Switch to configure for Azure OpenAI.
+.PARAMETER APIKey
+The API key for the AI service.
.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 FrontendClientId
+The client (application) ID associated with your frontend's AAD app registration.
-.PARAMETER ClientID
-The client (application) ID associated with your AAD app registration.
+.PARAMETER BackendClientId
+The client (application) ID associated with your backend's AAD app registration.
-.PARAMETER Tenant
-The tenant (directory) associated with your AAD app registration.
-Defaults to 'common'.
+.PARAMETER TenantId
+The tenant (directory) associated with your AAD app registrations.
See https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-client-application-configuration#authority.
+
+.PARAMETER Instance
+The Azure cloud instance used for authenticating users. Defaults to https://login.microsoftonline.com.
+See https://learn.microsoft.com/en-us/azure/active-directory/develop/authentication-national-cloud#azure-ad-authentication-endpoints.
#>
param(
- [Parameter(ParameterSetName='OpenAI',Mandatory=$false)]
- [switch]$OpenAI,
-
- [Parameter(ParameterSetName='AzureOpenAI',Mandatory=$false)]
- [switch]$AzureOpenAI,
+ [Parameter(Mandatory = $true)]
+ [string]$AIService,
- [Parameter(ParameterSetName='AzureOpenAI',Mandatory=$true)]
+ [Parameter(Mandatory = $true)]
+ [string]$APIKey,
+
+ [Parameter(Mandatory = $false)]
[string]$Endpoint,
- [Parameter(Mandatory=$true)]
- [string]$ApiKey,
+ [Parameter(Mandatory = $false)]
+ [string]$CompletionModel,
- [Parameter(Mandatory=$false)]
- [string]$CompletionModel = "gpt-3.5-turbo",
+ [Parameter(Mandatory = $false)]
+ [string]$EmbeddingModel,
- [Parameter(Mandatory=$false)]
- [string]$EmbeddingModel = "text-embedding-ada-002",
+ [Parameter(Mandatory = $false)]
+ [string] $FrontendClientId,
- [Parameter(Mandatory=$false)]
- [string]$PlannerModel = "gpt-3.5-turbo",
+ [Parameter(Mandatory = $false)]
+ [string] $BackendClientId,
- [Parameter(Mandatory = $true)]
- [string] $ClientId,
+ [Parameter(Mandatory = $false)]
+ [string] $TenantId,
[Parameter(Mandatory = $false)]
- [string] $Tenant = 'common'
+ [string] $Instance
)
+# Get defaults and constants
+$varScriptFilePath = Join-Path "$PSScriptRoot" 'Variables.ps1'
+. $varScriptFilePath
+
+# Set remaining values from Variables.ps1
+if ($AIService -eq $varOpenAI) {
+ if (!$CompletionModel) {
+ Write-Host "No completion model provided - Defaulting to $varCompletionModelOpenAI"
+ $CompletionModel = $varCompletionModelOpenAI
+ }
+
+ # TO DO: Validate model values if set by command line.
+}
+elseif ($AIService -eq $varAzureOpenAI) {
+ if (!$CompletionModel) {
+ Write-Host "No completion model provided - Defaulting to $varCompletionModelAzureOpenAI"
+ $CompletionModel = $varCompletionModelAzureOpenAI
+ }
+
+ # TO DO: Validate model values if set by command line.
+
+ if (!$Endpoint) {
+ Write-Error "Please specify an endpoint for -Endpoint when using AzureOpenAI."
+ exit(1)
+ }
+}
+else {
+ Write-Error "Please specify an AI service (AzureOpenAI or OpenAI) for -AIService."
+ exit(1)
+}
+
+if (!$EmbeddingModel) {
+ $EmbeddingModel = $varEmbeddingModel
+ # TO DO: Validate model values if set by command line.
+}
+
+# Determine authentication type based on arguments
+if ($FrontendClientId -and $BackendClientId -and $TenantId) {
+ $authType = $varAzureAd
+ if (!$Instance) {
+ $Instance = $varInstance
+ }
+}
+elseif (!$FrontendClientId -and !$BackendClientId -and !$TenantId) {
+ $authType = $varNone
+}
+else {
+ Write-Error "To use Azure AD authentication, please set -FrontendClientId, -BackendClientId, and -TenantId."
+ exit(1)
+}
+
Write-Host "#########################"
Write-Host "# Backend configuration #"
Write-Host "#########################"
# Install dev certificate
-if ($IsLinux)
-{
+if ($IsLinux) {
dotnet dev-certs https
if ($LASTEXITCODE -ne 0) { exit(1) }
}
-else # Windows/MacOS
-{
+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")
+$webapiProjectPath = Join-Path "$PSScriptRoot" '../webapi'
+
+Write-Host "Setting 'APIKey' user secret for $AIService..."
+if ($AIService -eq $varOpenAI) {
+ dotnet user-secrets set --project $webapiProjectPath KernelMemory:Services:OpenAI:APIKey $ApiKey
+ if ($LASTEXITCODE -ne 0) { exit(1) }
+ $AIServiceOverrides = @{
+ OpenAI = @{
+ TextModel = $CompletionModel;
+ EmbeddingModel = $EmbeddingModel;
+ }
+ };
}
else {
- Write-Error "Please specify either -OpenAI or -AzureOpenAI"
- exit(1)
+ dotnet user-secrets set --project $webapiProjectPath KernelMemory:Services:AzureOpenAIText:APIKey $ApiKey
+ if ($LASTEXITCODE -ne 0) { exit(1) }
+ dotnet user-secrets set --project $webapiProjectPath KernelMemory:Services:AzureOpenAIEmbedding:APIKey $ApiKey
+ if ($LASTEXITCODE -ne 0) { exit(1) }
+ $AIServiceOverrides = @{
+ AzureOpenAIText = @{
+ Endpoint = $Endpoint;
+ Deployment = $CompletionModel;
+ };
+ AzureOpenAIEmbedding = @{
+ Endpoint = $Endpoint;
+ Deployment = $EmbeddingModel;
+ }
+ };
}
-$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) }
+$appsettingsOverrides = @{
+ Authentication = @{
+ Type = $authType;
+ AzureAd = @{
+ Instance = $Instance;
+ TenantId = $TenantId;
+ ClientId = $BackendClientId;
+ Scopes = $varScopes
+ }
+ };
+ KernelMemory = @{
+ TextGeneratorType = $AIService;
+ DataIngestion = @{
+ EmbeddingGeneratorTypes = @($AIService)
+ };
+ Retrieval = @{
+ EmbeddingGeneratorType = $AIService
+ };
+ Services = $AIServiceOverrides;
+ };
+ Frontend = @{
+ AadClientId = $FrontendClientId
+ };
+}
+$appSettingsJson = -join ("appsettings.", $varASPNetCore, ".json");
+$appsettingsOverridesFilePath = Join-Path $webapiProjectPath $appSettingsJson
-Write-Host "Setting up 'appsettings.Development.json' for $aiServiceType..."
-ConvertTo-Json $appsettingsOverrides | Out-File -Encoding utf8 $appsettingsOverridesFilePath
+Write-Host "Setting up '$appSettingsJson' for $AIService..."
+# Setting depth to 100 to avoid truncating the JSON
+ConvertTo-Json $appsettingsOverrides -Depth 100 | Out-File -Encoding utf8 $appsettingsOverridesFilePath
Write-Host "($appsettingsOverridesFilePath)"
Write-Host "========"
@@ -118,19 +198,15 @@ Write-Host "##########################"
Write-Host "# Frontend configuration #"
Write-Host "##########################"
-$envFilePath = Join-Path "$PSScriptRoot" '../webapp/.env'
+$webappProjectPath = Join-Path "$PSScriptRoot" '../webapp'
+$webappEnvFilePath = Join-Path "$webappProjectPath" '/.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)"
+Set-Content -Path $webappEnvFilePath -Value "REACT_APP_BACKEND_URI=https://localhost:40443/"
+
+Write-Host "($webappEnvFilePath)"
Write-Host "========"
-Get-Content $envFilePath | Write-Host
+Get-Content $webappEnvFilePath | Write-Host
Write-Host "========"
Write-Host "Done!"
diff --git a/scripts/Configure.sh b/scripts/Configure.sh
deleted file mode 100644
index 06fa0e6f9..000000000
--- a/scripts/Configure.sh
+++ /dev/null
@@ -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!"
\ No newline at end of file
diff --git a/scripts/Install-Requirements.ps1 b/scripts/Install.ps1
similarity index 88%
rename from scripts/Install-Requirements.ps1
rename to scripts/Install.ps1
index c7020ea5c..3e7fb286b 100644
--- a/scripts/Install-Requirements.ps1
+++ b/scripts/Install.ps1
@@ -1,6 +1,6 @@
<#
.SYNOPSIS
-Installs the requirements for running Copilot Chat.
+Installs the requirements for running Chat Copilot.
#>
if ($IsLinux)
@@ -26,7 +26,7 @@ if (!$ChocoInstalled)
}
# Ensure required packages are installed
-$Packages = 'dotnet-6.0-sdk', 'nodejs', 'yarn'
+$Packages = 'dotnet-8.0-sdk', 'nodejs', 'yarn'
foreach ($PackageName in $Packages)
{
choco install $PackageName -y
diff --git a/scripts/README.md b/scripts/README.md
index de51f5bf0..5f5e3eb19 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -1,79 +1,3 @@
-# Copilot Chat Setup Scripts (local deployment)
+# Chat Copilot 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;
-```
\ No newline at end of file
+To use these scripts, please follow the quick-start guide found [here](../README.md).
diff --git a/scripts/Start-Backend.ps1 b/scripts/Start-Backend.ps1
index 2a459d608..2e9a5f424 100644
--- a/scripts/Start-Backend.ps1
+++ b/scripts/Start-Backend.ps1
@@ -1,8 +1,19 @@
<#
.SYNOPSIS
-Builds and runs the Copilot Chat backend.
+Builds and runs the Chat Copilot backend.
#>
+# Stop any existing backend API process
+Get-Process -Name "CopilotChatWebApi" -ErrorAction SilentlyContinue | Stop-Process
+
+# Get defaults and constants
+$varScriptFilePath = Join-Path "$PSScriptRoot" 'Variables.ps1'
+. $varScriptFilePath
+
+# Environment variable `ASPNETCORE_ENVIRONMENT` required to override appsettings.json with
+# appsettings.$varASPNetCore.json. See `webapi/ConfigurationExtensions.cs`
+$Env:ASPNETCORE_ENVIRONMENT=$varASPNetCore
+
Join-Path "$PSScriptRoot" '../webapi' | Set-Location
dotnet build
dotnet run
diff --git a/scripts/Start-Backend.sh b/scripts/Start-Backend.sh
deleted file mode 100644
index dce319620..000000000
--- a/scripts/Start-Backend.sh
+++ /dev/null
@@ -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
diff --git a/scripts/Start-Frontend.ps1 b/scripts/Start-Frontend.ps1
index 13e870c91..c8f059f37 100644
--- a/scripts/Start-Frontend.ps1
+++ b/scripts/Start-Frontend.ps1
@@ -1,6 +1,6 @@
<#
.SYNOPSIS
-Builds and runs the Copilot Chat frontend.
+Builds and runs the Chat Copilot frontend.
#>
Join-Path "$PSScriptRoot" '../webapp' | Set-Location
diff --git a/scripts/Start-Frontend.sh b/scripts/Start-Frontend.sh
deleted file mode 100644
index 152b544bf..000000000
--- a/scripts/Start-Frontend.sh
+++ /dev/null
@@ -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
diff --git a/scripts/Start.ps1 b/scripts/Start.ps1
index e5f046f29..d04fee035 100644
--- a/scripts/Start.ps1
+++ b/scripts/Start.ps1
@@ -1,13 +1,53 @@
<#
.SYNOPSIS
-Builds and runs both the backend and frontend for Copilot Chat.
+Initializes and runs both the backend and frontend for Chat Copilot.
#>
+# Verify "Core" version of powershell installed (not "Desktop"): https://aka.ms/powershell
+$ErrorActionPreference = 'Ignore'
+$cmd = get-command 'pwsh'
+$ErrorActionPreference = 'Continue'
+
+if (!$cmd) {
+ Write-Warning "Please update your powershell installation: https://aka.ms/powershell"
+ return;
+}
+
$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-Process pwsh -ArgumentList "-command ""& '$BackendScript'"""
+# check if the backend is running before proceeding
+$backendRunning = $false
+
+# get the port from the REACT_APP_BACKEND_URI env variable
+$envFilePath = Join-Path $PSScriptRoot '..\webapp\.env'
+$envContent = Get-Content -Path $envFilePath
+$port = [regex]::Match($envContent, ':(\d+)/').Groups[1].Value
+
+$maxRetries = 5
+$retryCount = 0
+$retryWait = 5 # set the number of seconds to wait before retrying
+
+# check if the backend is running and check if the retry count is less than the max retries
+while ($backendRunning -eq $false -and $retryCount -lt $maxRetries) {
+ $retryCount++
+ $backendRunning = Test-NetConnection -ComputerName localhost -Port $port -InformationLevel Quiet
+ Start-Sleep -Seconds $retryWait
+}
-# Start frontend (in current PS process)
-& $FrontendScript
+# if the backend is running, start the frontend
+if ($backendRunning -eq $true) {
+ # Start frontend (in current PS process)
+ & $FrontendScript
+}
+else {
+ # otherwise, write to the console that the backend is not running and we have exceeded the number of retries and we are exiting
+ Write-Host "*************************************************"
+ Write-Host "Backend is not running and we have exceeded "
+ Write-Host "the maximum number of retries."
+ Write-Host ""
+ Write-Host "Therefore, we are exiting."
+ Write-Host "*************************************************"
+}
\ No newline at end of file
diff --git a/scripts/Start.sh b/scripts/Start.sh
deleted file mode 100644
index edf88aff4..000000000
--- a/scripts/Start.sh
+++ /dev/null
@@ -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
diff --git a/scripts/Variables.ps1 b/scripts/Variables.ps1
new file mode 100644
index 000000000..b4ff44529
--- /dev/null
+++ b/scripts/Variables.ps1
@@ -0,0 +1,15 @@
+# Default environment file to be read by scripts
+
+# Default values
+$varCompletionModelOpenAI = "gpt-4o"
+$varCompletionModelAzureOpenAI = "gpt-4o"
+$varEmbeddingModel = "text-embedding-ada-002"
+$varASPNetCore = "Development"
+$varInstance = "https://login.microsoftonline.com"
+
+# Constants
+$varAzureOpenAI = "AzureOpenAI"
+$varOpenAI = "OpenAI"
+$varAzureAd = "AzureAd"
+$varNone = "None"
+$varScopes = "access_as_user"
\ No newline at end of file
diff --git a/scripts/configure.sh b/scripts/configure.sh
new file mode 100755
index 000000000..7cf2a591b
--- /dev/null
+++ b/scripts/configure.sh
@@ -0,0 +1,226 @@
+#!/usr/bin/env bash
+# Configure user secrets, appsettings.Development.json, and webapp/.env for Chat Copilot.
+
+set -e
+
+# Get defaults and constants
+SCRIPT_DIRECTORY="$(dirname $0)"
+. $SCRIPT_DIRECTORY/.env
+
+# Argument parsing
+POSITIONAL_ARGS=()
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --aiservice) # Required argument
+ AI_SERVICE="$2"
+ shift
+ shift
+ ;;
+ -a | --apikey) # Required argument
+ API_KEY="$2"
+ shift
+ shift
+ ;;
+ -e | --endpoint) # Required argument for Azure OpenAI
+ ENDPOINT="$2"
+ shift
+ shift
+ ;;
+ --completionmodel)
+ COMPLETION_MODEL="$2"
+ shift
+ shift
+ ;;
+ --embeddingmodel)
+ EMBEDDING_MODEL="$2"
+ shift
+ shift
+ ;;
+ -fc | --frontend-clientid)
+ FRONTEND_CLIENT_ID="$2"
+ shift
+ shift
+ ;;
+ -bc | --backend-clientid)
+ BACKEND_CLIENT_ID="$2"
+ shift
+ shift
+ ;;
+ -t | --tenantid)
+ TENANT_ID="$2"
+ shift
+ shift
+ ;;
+ -i | --instance)
+ INSTANCE="$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
+
+# Validate arguments
+if [ -z "$AI_SERVICE" -o \( "$AI_SERVICE" != "$ENV_OPEN_AI" -a "$AI_SERVICE" != "$ENV_AZURE_OPEN_AI" \) ]; then
+ echo "Please specify an AI service (AzureOpenAI or OpenAI) for --aiservice. "
+ exit 1
+fi
+if [ -z "$API_KEY" ]; then
+ echo "Please specify an API key with -a or --apikey."
+ exit 1
+fi
+if [ "$AI_SERVICE" = "$ENV_AZURE_OPEN_AI" ] && [ -z "$ENDPOINT" ]; then
+ echo "When using $(--aiservice AzureOpenAI), please specify an endpoint with -e or --endpoint."
+ exit 1
+fi
+
+if [ "$FRONTEND_CLIENT_ID" ] && [ "$BACKEND_CLIENT_ID" ] && [ "$TENANT_ID" ]; then
+ # Set auth type to AzureAd
+ AUTH_TYPE="$ENV_AZURE_AD"
+ # If instance empty, use default
+ if [ -z "$INSTANCE" ]; then
+ INSTANCE="$ENV_INSTANCE"
+ fi
+else
+ if [ -z "$FRONTEND_CLIENT_ID" ] && [ -z "$BACKEND_CLIENT_ID" ] && [ -z "$TENANT_ID" ]; then
+ # Set auth type to None
+ AUTH_TYPE="$ENV_NONE"
+ else
+ echo "To use Azure AD authentication, please set --frontend-clientid, --backend-clientid, and --tenantid."
+ exit 1
+ fi
+fi
+
+# Set remaining values from .env if not passed as argument
+if [ "$AI_SERVICE" = "$ENV_OPEN_AI" ]; then
+ if [ -z "$COMPLETION_MODEL" ]; then
+ COMPLETION_MODEL="$ENV_COMPLETION_MODEL_OPEN_AI"
+ fi
+ # TO DO: Validate model values if set by command line.
+else # elif [ "$AI_SERVICE" = "$ENV_AZURE_OPEN_AI" ]; then
+ if [ -z "$COMPLETION_MODEL" ]; then
+ COMPLETION_MODEL="$ENV_COMPLETION_MODEL_AZURE_OPEN_AI"
+ fi
+ # TO DO: Validate model values if set by command line.
+fi
+
+if [ -z "$EMBEDDING_MODEL" ]; then
+ EMBEDDING_MODEL="$ENV_EMBEDDING_MODEL"
+ # TO DO: Validate model values if set by command line.
+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
+
+WEBAPI_PROJECT_PATH="${SCRIPT_DIRECTORY}/../webapi"
+
+echo "Setting 'APIKey' user secret for $AI_SERVICE..."
+if [ "$AI_SERVICE" = "$ENV_OPEN_AI" ]; then
+ dotnet user-secrets set --project $WEBAPI_PROJECT_PATH KernelMemory:Services:OpenAI:APIKey $API_KEY
+ if [ $? -ne 0 ]; then exit 1; fi
+ AISERVICE_OVERRIDES="{
+ \"OpenAI\":
+ {
+ \"TextModel\": \"${COMPLETION_MODEL}\",
+ \"EmbeddingModel\": \"${EMBEDDING_MODEL}\",
+ }
+ }"
+else
+ dotnet user-secrets set --project $WEBAPI_PROJECT_PATH KernelMemory:Services:AzureOpenAIText:APIKey $API_KEY
+ if [ $? -ne 0 ]; then exit 1; fi
+ dotnet user-secrets set --project $WEBAPI_PROJECT_PATH KernelMemory:Services:AzureOpenAIEmbedding:APIKey $API_KEY
+ if [ $? -ne 0 ]; then exit 1; fi
+ AISERVICE_OVERRIDES="{
+ \"AzureOpenAIText\": {
+ \"Endpoint\": \"${ENDPOINT}\",
+ \"Deployment\": \"${COMPLETION_MODEL}\"
+ },
+ \"AzureOpenAIEmbedding\": {
+ \"Endpoint\": \"${ENDPOINT}\",
+ \"Deployment\": \"${EMBEDDING_MODEL}\"
+ }
+ }"
+fi
+
+APPSETTINGS_OVERRIDES="{
+ \"Authentication\": {
+ \"Type\": \"${AUTH_TYPE}\",
+ \"AzureAd\": {
+ \"Instance\": \"${INSTANCE}\",
+ \"TenantId\": \"${TENANT_ID}\",
+ \"ClientId\": \"${BACKEND_CLIENT_ID}\",
+ \"Scopes\": \"${ENV_SCOPES}\"
+ }
+ },
+ \"KernelMemory\": {
+ \"TextGeneratorType\": \"${AI_SERVICE}\",
+ \"DataIngestion\": {
+ \"EmbeddingGeneratorTypes\": [\"${AI_SERVICE}\"]
+ },
+ \"Retrieval\": {
+ \"EmbeddingGeneratorType\": \"${AI_SERVICE}\"
+ },
+ \"Services\": ${AISERVICE_OVERRIDES}
+ },
+ \"Frontend\": {
+ \"AadClientId\": \"${FRONTEND_CLIENT_ID}\"
+ }
+}"
+APPSETTINGS_OVERRIDES_FILEPATH="${WEBAPI_PROJECT_PATH}/appsettings.${ENV_ASPNETCORE}.json"
+
+echo "Setting up 'appsettings.${ENV_ASPNETCORE}.json' for $AI_SERVICE..."
+echo $APPSETTINGS_OVERRIDES >$APPSETTINGS_OVERRIDES_FILEPATH
+
+echo "($APPSETTINGS_OVERRIDES_FILEPATH)"
+echo "========"
+cat $APPSETTINGS_OVERRIDES_FILEPATH
+echo "========"
+
+echo ""
+echo "##########################"
+echo "# Frontend configuration #"
+echo "##########################"
+
+WEBAPP_PROJECT_PATH="${SCRIPT_DIRECTORY}/../webapp"
+WEBAPP_ENV_FILEPATH="${WEBAPP_PROJECT_PATH}/.env"
+
+echo "Setting up '.env' for webapp..."
+echo "REACT_APP_BACKEND_URI=https://localhost:40443/" >$WEBAPP_ENV_FILEPATH
+
+echo "($WEBAPP_ENV_FILEPATH)"
+echo "========"
+cat $WEBAPP_ENV_FILEPATH
+echo "========"
+
+echo "Done!"
diff --git a/scripts/deploy/README.md b/scripts/deploy/README.md
new file mode 100644
index 000000000..5b93a17e6
--- /dev/null
+++ b/scripts/deploy/README.md
@@ -0,0 +1,232 @@
+# Deploying Chat Copilot
+
+This document details how to deploy Chat Copilot'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.
+
+- Chat Copilot deployments use Azure Active Directory for authentication. All endpoints (except `/healthz` and `/authInfo`) require authentication to access.
+
+# Configure your environment
+
+Before you get started, make sure you have the following requirements in place:
+
+- [Azure AD Tenant](https://learn.microsoft.com/azure/active-directory/develop/quickstart-create-new-tenant)
+- 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`"
+- (Linux only) `zip` can be installed by running "`sudo apt install zip`"
+
+## App registrations (identity)
+
+You will need two Azure Active Directory (AAD) application registrations -- one for the frontend web app and one for the backend API.
+
+> For details on creating an application registration, go [here](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app).
+
+> NOTE: Other account types can be used to allow multitenant and personal Microsoft accounts to use your application if you desire. Doing so may result in more users and therefore higher costs.
+
+### Frontend app registration
+
+- Select `Single-page application (SPA)` as platform type, and set the redirect URI to `http://localhost:3000`
+- Select `Accounts in this organizational directory only ({YOUR TENANT} only - Single tenant)` as supported account types.
+- Make a note of the `Application (client) ID` from the Azure Portal for use in the `Deploy Frontend` step below.
+
+### Backend app registration
+
+- Do not set a redirect URI
+- Select `Accounts in this organizational directory only ({YOUR TENANT} only - Single tenant)` as supported account types.
+- Make a note of the `Application (client) ID` from the Azure Portal for use in the `Deploy Azure infrastructure` step below.
+
+### Linking the frontend to the backend
+
+1. Expose an API within the backend app registration
+
+ 1. Select _Expose an API_ from the menu
+
+ 2. Add an _Application ID URI_
+
+ 1. This will generate an `api://` URI
+
+ 2. Click _Save_ to store the generated URI
+
+ 3. Add a scope for `access_as_user`
+
+ 1. Click _Add scope_
+
+ 2. Set _Scope name_ to `access_as_user`
+
+ 3. Set _Who can consent_ to _Admins and users_
+
+ 4. Set _Admin consent display name_ and _User consent display name_ to `Access Chat Copilot as a user`
+
+ 5. Set _Admin consent description_ and _User consent description_ to `Allows the accesses to the Chat Copilot web API as a user`
+
+ 4. Add the web app frontend as an authorized client application
+
+ 1. Click _Add a client application_
+
+ 2. For _Client ID_, enter the frontend's application (client) ID
+
+ 3. Check the checkbox under _Authorized scopes_
+
+ 4. Click _Add application_
+
+2. Add permissions to web app frontend to access web api as user
+
+ 1. Open app registration for web app frontend
+
+ 2. Go to _API Permissions_
+
+ 3. Click _Add a permission_
+
+ 4. Select the tab _APIs my organization uses_
+
+ 5. Choose the app registration representing the web api backend
+
+ 6. Select permissions `access_as_user`
+
+ 7. Click _Add permissions_
+
+# 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} -BackendClientId {YOUR_BACKEND_APPLICATION_ID} -FrontendClientId {YOUR_FRONTEND_APPLICATION_ID} -TenantId {YOUR_TENANT_ID}
+```
+
+- 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} --client-id {YOUR_BACKEND_APPLICATION_ID} --frontend-client-id {YOUR_FRONTEND_APPLICATION_ID} --tenant-id {YOUR_TENANT_ID}
+```
+
+- 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:
+
+[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sk-deploy-existing-azureopenai-portal)
+
+> This will automatically deploy the most recent release of Chat Copilot binaries ([link](https://github.com/microsoft/chat-copilot/releases)).
+
+> To find the deployment name when using `Deploy to Azure`, look for a deployment in your resource group that starts with `Microsoft.Template`.
+
+# Deploy Application
+
+To deploy the application, first package it, then deploy it to the Azure resources created above.
+
+## PowerShell
+
+```powershell
+./package-webapi.ps1
+
+./deploy-webapi.ps1 -Subscription {YOUR_SUBSCRIPTION_ID} -ResourceGroupName {YOUR_RESOURCE_GROUP_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 {YOUR_RESOURCE_GROUP_NAME} --deployment-name {YOUR_DEPLOYMENT_NAME}
+```
+
+# Deploy Hosted Plugins
+
+> **_NOTE:_** This step can be skipped if the required resources for the web searcher plugin are not deployed. The required resources include a Bing resource and an Azure Function. The required resources are NOT deployed by default. To deploy the required resources, use the `-DeployWebSearcherPlugin` or `--deploy-web-searcher-plugin` flag when running the **deploy-azure.ps1/deploy-azure.sh** script.
+
+> **_NOTE:_** This step can be skipped if the previous Azure Resources creation step, including the resources required by the Web Search plugin, succeeded without errors. The `deployPackages = true` setting in main.bicep ensures that the WebSearcher is deployed.
+
+> **_NOTE:_** More hosted plugins will be available.
+
+To deploy the plugins, build the packages first and deploy them to the Azure resources created above.
+
+## PowerShell
+
+```powershell
+./package-plugins.ps1
+
+./deploy-plugins.ps1 -Subscription {YOUR_SUBSCRIPTION_ID} -ResourceGroupName rg-{YOUR_DEPLOYMENT_NAME} -DeploymentName {YOUR_DEPLOYMENT_NAME}
+```
+
+## Bash
+
+```bash
+chmod +x ./package-plugins.sh
+./package-webapi.sh
+
+chmod +x ./deploy-plugins.sh
+./deploy-webapi.sh --subscription {YOUR_SUBSCRIPTION_ID} --resource-group rg-{YOUR_DEPLOYMENT_NAME} --deployment-name {YOUR_DEPLOYMENT_NAME}
+```
+
+# (Optional) Deploy Memory Pipeline
+
+> **_NOTE:_** This step can be skipped if the WebApi is NOT configured to run asynchronously for document processing. By default, the WebApi is configured to run asynchronously for document processing in deployment.
+
+> **_NOTE:_** This step can be skipped if the previous Azure Resources creation step succeeded without errors. The deployPackages = true setting in main.bicep ensures that the latest Chat Copilot memory pipeline is deployed.
+
+To deploy the memorypipeline, build the deployment package first and deploy it to the Azure resources created above.
+
+## PowerShell
+
+```powershell
+.\package-memorypipeline.ps1
+
+.\deploy-memorypipeline.ps1 -Subscription {YOUR_SUBSCRIPTION_ID} -ResourceGroupName {YOUR_RESOURCE_GROUP_NAME} -DeploymentName {YOUR_DEPLOYMENT_NAME}
+```
+
+## Bash
+
+```bash
+chmod +x ./package-memorypipeline.sh
+./package-memorypipeline.sh
+
+chmod +x ./deploy-memorypipeline.sh
+./deploy-memorypipeline.sh --subscription {YOUR_SUBSCRIPTION_ID} --resource-group {YOUR_RESOURCE_GROUP_NAME} --deployment-name {YOUR_DEPLOYMENT_NAME}
+```
+
+Your Chat Copilot 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.
+
+### PowerShell
+
+```powershell
+$webApiName = $(az deployment group show --name {DEPLOYMENT_NAME} --resource-group {YOUR_RESOURCE_GROUP_NAME} --output json | ConvertFrom-Json).properties.outputs.webapiName.value
+
+az webapp cors add --name $webapiName --resource-group $ResourceGroupName --subscription $Subscription --allowed-origins YOUR_FRONTEND_URL
+```
+
+### 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 cors add --name $WEB_API_NAME --resource-group $RESOURCE_GROUP --subscription $SUBSCRIPTION --allowed-origins YOUR_FRONTEND_URL
+```
diff --git a/deploy/deploy-azure.ps1 b/scripts/deploy/deploy-azure.ps1
similarity index 64%
rename from deploy/deploy-azure.ps1
rename to scripts/deploy/deploy-azure.ps1
index 487f3619f..7dd91276d 100644
--- a/deploy/deploy-azure.ps1
+++ b/scripts/deploy/deploy-azure.ps1
@@ -1,6 +1,6 @@
<#
.SYNOPSIS
-Deploy CopilotChat Azure resources
+Deploy Chat Copilot Azure resources
#>
param(
@@ -15,10 +15,24 @@ param(
$Subscription,
[Parameter(Mandatory)]
- [ValidateSet("AzureOpenAI","OpenAI")]
+ [string]
+ # Azure AD client ID for the Web API backend app registration
+ $BackendClientId,
+
+ [Parameter(Mandatory)]
+ [string]
+ # Azure AD client ID for the frontend app registration
+ $FrontendClientId,
+
+ [Parameter(Mandatory)]
+ [string]
+ # Azure AD tenant ID for authenticating users
+ $TenantId,
+
+ [ValidateSet("AzureOpenAI", "OpenAI")]
[string]
# AI service to use
- $AIService,
+ $AIService = "AzureOpenAI",
[string]
# API key for existing Azure OpenAI resource or OpenAI account
@@ -33,16 +47,21 @@ param(
$ResourceGroup,
[string]
- # Region to which to make the deployment (ignored when deploying to an existing resource group)
- $Region = "centralus",
+ # Region to which to make the deployment
+ $Region = "southcentralus",
[string]
# SKU for the Azure App Service plan
$WebAppServiceSku = "B1",
- [switch]
- # Don't deploy Qdrant for memory storage - Use volatile memory instead
- $NoQdrant,
+ [string]
+ # Azure AD cloud instance for authenticating users
+ $AzureAdInstance = "https://login.microsoftonline.com",
+
+ [ValidateSet("AzureAISearch", "Qdrant")]
+ [string]
+ # What method to use to persist embeddings
+ $MemoryStore = "AzureAISearch",
[switch]
# Don't deploy Cosmos DB for chat storage - Use volatile memory instead
@@ -52,27 +71,34 @@ param(
# Don't deploy Speech Services to enable speech as chat input
$NoSpeechServices,
+ [switch]
+ # Deploy the web searcher plugin
+ $DeployWebSearcherPlugin,
+
[switch]
# Switches on verbose template deployment output
- $DebugDeployment
+ $DebugDeployment,
+
+ [switch]
+ # Skip deployment of binary packages
+ $NoDeployPackage
)
# 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."
+ Write-Error "When AIService is AzureOpenAI, both AIEndpoint and AIApiKey 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."
+ 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."
}
}
@@ -84,26 +110,30 @@ if ($AIService -eq "OpenAI" -and !$AIApiKey) {
$jsonConfig = "
{
- `\`"name`\`": { `\`"value`\`": `\`"$DeploymentName`\`" },
`\`"webAppServiceSku`\`": { `\`"value`\`": `\`"$WebAppServiceSku`\`" },
`\`"aiService`\`": { `\`"value`\`": `\`"$AIService`\`" },
`\`"aiApiKey`\`": { `\`"value`\`": `\`"$AIApiKey`\`" },
`\`"aiEndpoint`\`": { `\`"value`\`": `\`"$AIEndpoint`\`" },
+ `\`"deployPackages`\`": { `\`"value`\`": $(If ($NoDeployPackage) {"false"} Else {"true"}) },
+ `\`"azureAdInstance`\`": { `\`"value`\`": `\`"$AzureAdInstance`\`" },
+ `\`"azureAdTenantId`\`": { `\`"value`\`": `\`"$TenantId`\`" },
+ `\`"webApiClientId`\`": { `\`"value`\`": `\`"$BackendClientId`\`"},
+ `\`"frontendClientId`\`": { `\`"value`\`": `\`"$FrontendClientId`\`"},
`\`"deployNewAzureOpenAI`\`": { `\`"value`\`": $(If ($DeployAzureOpenAI) {"true"} Else {"false"}) },
- `\`"deployQdrant`\`": { `\`"value`\`": $(If (!($NoQdrant)) {"true"} Else {"false"}) },
+ `\`"memoryStore`\`": { `\`"value`\`": `\`"$MemoryStore`\`" },
`\`"deployCosmosDB`\`": { `\`"value`\`": $(If (!($NoCosmosDb)) {"true"} Else {"false"}) },
- `\`"deploySpeechServices`\`": { `\`"value`\`": $(If (!($NoSpeechServices)) {"true"} Else {"false"}) }
+ `\`"deploySpeechServices`\`": { `\`"value`\`": $(If (!($NoSpeechServices)) {"true"} Else {"false"}) },
+ `\`"deployWebSearcherPlugin`\`": { `\`"value`\`": $(If ($DeployWebSearcherPlugin) {"true"} Else {"false"}) }
}
"
-$jsonConfig = $jsonConfig -replace '\s',''
+$jsonConfig = $jsonConfig -replace '\s', ''
$ErrorActionPreference = "Stop"
$templateFile = "$($PSScriptRoot)/main.bicep"
-if (!$ResourceGroup)
-{
+if (!$ResourceGroup) {
$ResourceGroup = "rg-" + $DeploymentName
}
@@ -115,13 +145,13 @@ if ($LASTEXITCODE -ne 0) {
az account set -s $Subscription
if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
+ 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
+ exit $LASTEXITCODE
}
Write-Host "Validating template file..."
diff --git a/deploy/deploy-azure.sh b/scripts/deploy/deploy-azure.sh
old mode 100644
new mode 100755
similarity index 62%
rename from deploy/deploy-azure.sh
rename to scripts/deploy/deploy-azure.sh
index 6e6999159..e6df71d05
--- a/deploy/deploy-azure.sh
+++ b/scripts/deploy/deploy-azure.sh
@@ -1,88 +1,124 @@
-#!/bin/bash
+#!/usr/bin/env bash
-# Deploy CopilotChat Azure resources.
+# Deploy Chat Copilot Azure resources.
set -e
usage() {
- echo "Usage: $0 -d DEPLOYMENT_NAME -s SUBSCRIPTION -ai AI_SERVICE_TYPE -aikey AI_SERVICE_KEY [OPTIONS]"
+ echo "Usage: $0 -d DEPLOYMENT_NAME -s SUBSCRIPTION -c BACKEND_CLIENT_ID -fc FRONTEND_CLIENT_ID -t AZURE_AD_TENANT_ID -ai AI_SERVICE_TYPE [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 " -c, --client-id BACKEND_CLIENT_ID Azure AD client ID for the Web API backend app registration (mandatory)"
+ echo " -fc, --frontend-client-id FE_CLIENT_ID Azure AD client ID for the frontend app registration (mandatory)"
+ echo " -t, --tenant-id AZURE_AD_TENANT_ID Azure AD tenant ID for authenticating users (mandatory)"
+ echo " -ai, --ai-service AI_SERVICE_TYPE Type of AI service to use (i.e., OpenAI or AzureOpenAI) (mandatory)"
echo " -aiend, --ai-endpoint AI_ENDPOINT Endpoint for existing Azure OpenAI resource"
+ echo " -aikey, --ai-service-key AI_SERVICE_KEY API key for existing Azure OpenAI resource or OpenAI account"
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 " -i, --instance AZURE_AD_INSTANCE Azure AD cloud instance for authenticating users"
+ echo " (default: \"https://login.microsoftonline.com\")"
+ echo " -ms, --memory-store Method to use to persist embeddings. Valid values are"
+ echo " \"AzureAISearch\" (default) and \"Qdrant\""
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 " -ws, --deploy-web-searcher-plugin Deploy the web searcher plugin"
echo " -dd, --debug-deployment Switches on verbose template deployment output"
+ echo " -ndp, --no-deploy-package Skips deploying binary packages to cloud when set."
}
# Parse arguments
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
- -d|--deployment-name)
+ -d | --deployment-name)
DEPLOYMENT_NAME="$2"
shift
shift
;;
- -s|--subscription)
+ -s | --subscription)
SUBSCRIPTION="$2"
shift
shift
;;
- -ai|--ai-service)
+ -c | --client-id)
+ BACKEND_CLIENT_ID="$2"
+ shift
+ shift
+ ;;
+ -fc | --frontend-client-id)
+ FRONTEND_CLIENT_ID="$2"
+ shift
+ shift
+ ;;
+ -t | --tenant-id)
+ AZURE_AD_TENANT_ID="$2"
+ shift
+ shift
+ ;;
+ -ai | --ai-service)
AI_SERVICE_TYPE="$2"
shift
shift
;;
- -aikey|--ai-service-key)
+ -aikey | --ai-service-key)
AI_SERVICE_KEY="$2"
shift
shift
;;
- -aiend|--ai-endpoint)
+ -aiend | --ai-endpoint)
AI_ENDPOINT="$2"
shift
shift
;;
- -rg|--resource-group)
+ -rg | --resource-group)
RESOURCE_GROUP="$2"
shift
shift
;;
- -r|--region)
+ -r | --region)
REGION="$2"
shift
shift
;;
- -a|--app-service-sku)
+ -a | --app-service-sku)
WEB_APP_SVC_SKU="$2"
shift
shift
;;
- -nq|--no-qdrant)
- NO_QDRANT=true
+ -i | --instance)
+ AZURE_AD_INSTANCE="$2"
+ shift
+ shift
+ ;;
+ -ms | --memory-store)
+ MEMORY_STORE=="$2"
shift
;;
- -nc|--no-cosmos-db)
+ -nc | --no-cosmos-db)
NO_COSMOS_DB=true
shift
;;
- -ns|--no-speech-services)
+ -ns | --no-speech-services)
NO_SPEECH_SERVICES=true
shift
;;
- -dd|--debug-deployment)
+ -ws | --deploy-web-searcher-plugin)
+ DEPLOY_WEB_SEARCHER_PLUGIN=true
+ shift
+ ;;
+ -dd | --debug-deployment)
DEBUG_DEPLOYMENT=true
shift
;;
- *)
+ -ndp | --no-deploy-package)
+ NO_DEPLOY_PACKAGE=true
+ shift
+ ;;
+ *)
echo "Unknown option $1"
usage
exit 1
@@ -91,7 +127,7 @@ while [[ $# -gt 0 ]]; do
done
# Check mandatory arguments
-if [[ -z "$DEPLOYMENT_NAME" ]] || [[ -z "$SUBSCRIPTION" ]] || [[ -z "$AI_SERVICE_TYPE" ]]; then
+if [[ -z "$DEPLOYMENT_NAME" ]] || [[ -z "$SUBSCRIPTION" ]] || [[ -z "$BACKEND_CLIENT_ID" ]] || [[ -z "$FRONTEND_CLIENT_ID" ]] || [[ -z "$AZURE_AD_TENANT_ID" ]] || [[ -z "$AI_SERVICE_TYPE" ]]; then
usage
exit 1
fi
@@ -145,29 +181,37 @@ fi
az account set -s "$SUBSCRIPTION"
# Set defaults
-: "${REGION:="centralus"}"
+: "${REGION:="southcentralus"}"
: "${WEB_APP_SVC_SKU:="B1"}"
-: "${NO_QDRANT:=false}"
+: "${AZURE_AD_INSTANCE:="https://login.microsoftonline.com"}"
+: "${MEMORY_STORE:="AzureAISearch"}"
: "${NO_COSMOS_DB:=false}"
: "${NO_SPEECH_SERVICES:=false}"
+: "${DEPLOY_WEB_SEARCHER_PLUGIN:=false}"
# Create JSON config
-JSON_CONFIG=$(cat << EOF
+JSON_CONFIG=$(
+ cat <
param(
@@ -20,13 +20,13 @@ param(
$DeploymentName,
[string]
- # CopilotChat WebApi package to deploy
- $PackageFilePath = "$PSScriptRoot/out/webapi.zip"
+ # CopilotChat memorypipeline package to deploy
+ $PackageFilePath = "$PSScriptRoot/out/memorypipeline.zip"
)
# Ensure $PackageFilePath exists
if (!(Test-Path $PackageFilePath)) {
- Write-Error "Package file '$PackageFilePath' does not exist. Have you run 'package-webapi.ps1' yet?"
+ Write-Error "Package file '$PackageFilePath' does not exist. Have you run 'package-memorypipeline.ps1' yet?"
exit 1
}
@@ -42,22 +42,22 @@ if ($LASTEXITCODE -ne 0) {
}
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) {
+$memoryPipelineName=$(az deployment group show --name $DeploymentName --resource-group $ResourceGroupName --output json | ConvertFrom-Json).properties.outputs.memoryPipelineName.value
+if ($null -eq $memoryPipelineName) {
Write-Error "Could not get Azure WebApp resource name from deployment output."
exit 1
}
-Write-Host "Azure WebApp name: $webappName"
+Write-Host "Azure WebApp name: $memoryPipelineName"
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
+az webapp config appsettings set --resource-group $ResourceGroupName --name $memoryPipelineName --settings WEBSITE_RUN_FROM_PACKAGE="1" --output none
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
+Write-Host "Deploying '$PackageFilePath' to Azure WebApp '$memoryPipelineName'..."
+az webapp deployment source config-zip --resource-group $ResourceGroupName --name $memoryPipelineName --src $PackageFilePath
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
\ No newline at end of file
diff --git a/deploy/deploy-webapi.sh b/scripts/deploy/deploy-memorypipeline.sh
similarity index 67%
rename from deploy/deploy-webapi.sh
rename to scripts/deploy/deploy-memorypipeline.sh
index 6e0ea511e..a2b79ba89 100644
--- a/deploy/deploy-webapi.sh
+++ b/scripts/deploy/deploy-memorypipeline.sh
@@ -1,6 +1,6 @@
-#!/bin/bash
+#!/bin/bash
-# Deploy CopilotChat's WebAPI to Azure.
+# Deploy CopilotChat's MemoryPipeline to Azure.
set -e
@@ -11,7 +11,7 @@ usage() {
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"
+ echo " -p, --package PACKAGE_FILE_PATH Path to the MemoryPipeline package file from a 'package-memorypipeline.sh' run"
}
# Parse arguments
@@ -53,11 +53,11 @@ if [[ -z "$DEPLOYMENT_NAME" ]] || [[ -z "$SUBSCRIPTION" ]] || [[ -z "$RESOURCE_G
fi
# Set defaults
-: "${PACKAGE_FILE_PATH:="$(dirname "$0")/out/webapi.zip"}"
+: "${PACKAGE_FILE_PATH:="$(dirname "$0")/out/memorypipeline.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?"
+ echo "Package file '$PACKAGE_FILE_PATH' does not exist. Have you run 'package-memorypipeline.sh' yet?"
exit 1
fi
@@ -70,28 +70,25 @@ 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
+eval MEMORY_PIPELINE_NAME=$(az deployment group show --name $DEPLOYMENT_NAME --resource-group $RESOURCE_GROUP --output json | jq -r '.properties.outputs.memoryPipelineName.value')
+# Ensure $MEMORY_PIPELINE_NAME is set
+if [[ -z "$MEMORY_PIPELINE_NAME" ]]; then
echo "Could not get Azure WebApp resource name from deployment output."
exit 1
fi
-echo "Azure WebApp name: $webappName"
+echo "Azure WebApp name: $MEMORY_PIPELINE_NAME"
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"
+az webapp config appsettings set --resource-group $RESOURCE_GROUP --name $MEMORY_PIPELINE_NAME --settings WEBSITE_RUN_FROM_PACKAGE="1" --output none
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
+echo "Deploying '$PACKAGE_FILE_PATH' to Azure WebApp '$MEMORY_PIPELINE_NAME'..."
+az webapp deployment source config-zip --resource-group $RESOURCE_GROUP --name $MEMORY_PIPELINE_NAME --src $PACKAGE_FILE_PATH --debug
if [ $? -ne 0 ]; then
- echo "Could not deploy '$PackageFilePath' to Azure WebApp '$webappName'."
+ echo "Could not deploy '$PACKAGE_FILE_PATH' to Azure WebApp '$MEMORY_PIPELINE_NAME'."
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."
+fi
\ No newline at end of file
diff --git a/scripts/deploy/deploy-plugins.ps1 b/scripts/deploy/deploy-plugins.ps1
new file mode 100644
index 000000000..ffb2946b1
--- /dev/null
+++ b/scripts/deploy/deploy-plugins.ps1
@@ -0,0 +1,113 @@
+<#
+.SYNOPSIS
+Deploy CopilotChat's Plugins 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]
+ # Path that contains the plugin packages to deploy
+ $PackagesPath = "$PSScriptRoot/out/plugins"
+)
+
+# Ensure $PackageFilePath exists
+if (!(Test-Path $PackagesPath)) {
+ Write-Error "Package file '$PackagesPath' does not exist. Have you run 'package-plugins.ps1' yet?"
+ exit 1
+}
+
+# Get all the plugin packages
+$pluginPackages = Get-ChildItem -Path $PackagesPath -Filter *.zip
+if ($null -eq $pluginPackages) {
+ Write-Error "No plugin packages found in '$PackagesPath'. Have you run 'package-plugins.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 Function resource names"
+$pluginDeploymentNames = $(
+ az deployment group show `
+ --name $DeploymentName `
+ --resource-group $ResourceGroupName `
+ --output json | ConvertFrom-Json
+).properties.outputs.pluginNames.value
+Write-Host "-----Found the following Azure Function names-----"
+foreach ($pluginDeploymentName in $pluginDeploymentNames) {
+ Write-Host "$pluginDeploymentName"
+}
+Write-Host ""
+
+# Find the Azure Function resource name for each plugin package
+# before we deploy the plugins. This can minimize the risk of
+# deploying to the wrong Azure Function resource.
+Write-Host "---Matching plugins to Azure Function resources---"
+$pluginDeploymentMatches = @{}
+foreach ($pluginPackage in $pluginPackages) {
+ $pluginName = $pluginPackage.BaseName
+ Write-Host "Looking for the resource for '$pluginName'..."
+
+ # Check if the plugin name matches any of the Azure Function names we got from the deployment output
+ $matchedNumber = 0
+ $matchedDeployment = ""
+ foreach ($pluginDeploymentName in $pluginDeploymentNames) {
+ if ($pluginDeploymentName -match "function-.*$pluginName-plugin") {
+ $matchedNumber++
+ $matchedDeployment = $pluginDeploymentName
+ }
+ }
+
+ if ($matchedNumber -eq 0) {
+ Write-Error "Could not find Azure Function resource name for '$pluginName'."
+ Write-Error "Make sure the deployed Azure Function resource name matches the plugin zip package name."
+ exit 1
+ }
+
+ if ($matchedNumber -gt 1) {
+ Write-Error "Found multiple Azure Function resource names for '$pluginName'."
+ Write-Error "Make sure the deployed Azure Function resource name matches the plugin zip package name."
+ exit 1
+ }
+
+ Write-Host "Matched Azure Function resource name '$matchedDeployment' for '$pluginName'"
+ $pluginDeploymentMatches.Add($pluginPackage, $matchedDeployment)
+}
+Write-Host ""
+
+Write-Host "-------Deploying plugins to Azure Functions-------"
+foreach ($pluginDeploymentMatches in $pluginDeploymentMatches.GetEnumerator()) {
+ $pluginPackage = $pluginDeploymentMatches.Key
+ $pluginDeploymentName = $pluginDeploymentMatches.Value
+
+ Write-Host "Deploying '$pluginPackage' to Azure Function '$pluginDeploymentName'..."
+ az functionapp deployment source config-zip `
+ --resource-group $ResourceGroupName `
+ --name $pluginDeploymentName `
+ --src $pluginPackage
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+}
\ No newline at end of file
diff --git a/scripts/deploy/deploy-plugins.sh b/scripts/deploy/deploy-plugins.sh
new file mode 100755
index 000000000..f681ce340
--- /dev/null
+++ b/scripts/deploy/deploy-plugins.sh
@@ -0,0 +1,137 @@
+#!/bin/bash
+
+# Deploy CopilotChat's Plugins 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, --packages PACKAGES_PATH Path that contains the plugin packages to deploy"
+}
+
+# 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 | --packages)
+ PACKAGES_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
+: "${PACKAGES_PATH:="$(dirname "$0")/out/plugins"}"
+
+# Ensure $PACKAGES_PATH exists
+if [[ ! -d "$PACKAGES_PATH" ]]; then
+ echo "Package file '$PACKAGES_PATH' does not exist. Have you run 'package-plugins.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 Function resource names"
+PLUGIN_DEPLOYMENT_NAMES=$(
+ az deployment group show \
+ --name $DEPLOYMENT_NAME \
+ --resource-group $RESOURCE_GROUP \
+ --output json | jq -r '.properties.outputs.pluginNames.value[]'
+)
+echo "-----Found the following Azure Function names-----"
+for PLUGIN_DEPLOYMENT_NAME in $PLUGIN_DEPLOYMENT_NAMES; do
+ echo $PLUGIN_DEPLOYMENT_NAME
+done
+echo ""
+
+# Find the Azure Function resource name for each plugin package
+# before we deploy the plugins. This can minimize the risk of
+# deploying to the wrong Azure Function resource.
+echo "---Matching plugins to Azure Function resources---"
+PLUGIN_DEPLOYMENT_MATCHES=()
+PLUGIN_PACKAGE_MATCHES=()
+for PLUGIN_PACKAGE in $PACKAGES_PATH/*; do
+ PLUGIN_PACKAGE_NAME=$(basename $PLUGIN_PACKAGE)
+ PLUGIN_NAME=$(echo $PLUGIN_PACKAGE_NAME | sed 's/\.zip//g')
+
+ echo "Looking for the resource for '$PLUGIN_NAME'..."
+
+ MATCHED_NUMBER=0
+ MATCHED_DEPLOYMENT=""
+ for PLUGIN_DEPLOYMENT_NAME in $PLUGIN_DEPLOYMENT_NAMES; do
+ if [[ "$PLUGIN_DEPLOYMENT_NAME" =~ ^function-.*$PLUGIN_NAME-plugin$ ]]; then
+ MATCHED_NUMBER=$((MATCHED_NUMBER + 1))
+ MATCHED_DEPLOYMENT=$PLUGIN_DEPLOYMENT_NAME
+ fi
+ done
+
+ if [[ $MATCHED_NUMBER -eq 0 ]]; then
+ echo "Could not find Azure Function resource name for plugin '$PLUGIN_NAME'."
+ echo "Make sure the deployed Azure Function resource name matches the plugin zip package name."
+ exit 1
+ elif [[ $MATCHED_NUMBER -gt 1 ]]; then
+ echo "Found multiple Azure Function resource names for plugin '$PLUGIN_NAME'."
+ echo "Make sure the deployed Azure Function resource name matches the plugin zip package name."
+ exit 1
+ fi
+
+ echo "Matched Azure Function resource name '$MATCHED_DEPLOYMENT' for '$PLUGIN_NAME'"
+ PLUGIN_DEPLOYMENT_MATCHES+=($MATCHED_DEPLOYMENT)
+ PLUGIN_PACKAGE_MATCHES+=($PLUGIN_PACKAGE)
+done
+echo ""
+
+echo "-------Deploying plugins to Azure Functions-------"
+for i in "${!PLUGIN_DEPLOYMENT_MATCHES[@]}"; do
+ PLUGIN_DEPLOYMENT_NAME=${PLUGIN_DEPLOYMENT_MATCHES[$i]}
+ PLUGIN_PACKAGE=${PLUGIN_PACKAGE_MATCHES[$i]}
+
+ echo "Deploying '$PLUGIN_PACKAGE' to Azure Function '$PLUGIN_DEPLOYMENT_NAME'..."
+ az functionapp deployment source config-zip \
+ --resource-group $RESOURCE_GROUP \
+ --name $PLUGIN_DEPLOYMENT_NAME \
+ --src $PLUGIN_PACKAGE
+
+ if [ $? -ne 0 ]; then
+ echo "Could not deploy '$PLUGIN_PACKAGE' to Azure Function '$PLUGIN_DEPLOYMENT_NAME'."
+ exit 1
+ fi
+done
diff --git a/scripts/deploy/deploy-webapi.ps1 b/scripts/deploy/deploy-webapi.ps1
new file mode 100644
index 000000000..cc5fa5064
--- /dev/null
+++ b/scripts/deploy/deploy-webapi.ps1
@@ -0,0 +1,163 @@
+<#
+.SYNOPSIS
+Deploy Chat Copilot application 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]
+ # Name of the web app deployment slot
+ $DeploymentSlot,
+
+ [string]
+ $PackageFilePath = "$PSScriptRoot/out/webapi.zip",
+
+ [switch]
+ # Don't attempt to add our URIs in frontend app registration's redirect URIs
+ $SkipAppRegistration,
+
+ [switch]
+ # Don't attempt to add our URIs in CORS origins for our plugins
+ $SkipCorsRegistration
+)
+
+# 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..."
+$deployment=$(az deployment group show --name $DeploymentName --resource-group $ResourceGroupName --output json | ConvertFrom-Json)
+$webApiUrl = $deployment.properties.outputs.webapiUrl.value
+$webApiName = $deployment.properties.outputs.webapiName.value
+$pluginNames = $deployment.properties.outputs.pluginNames.value
+
+if ($null -eq $webApiName) {
+ Write-Error "Could not get Azure WebApp resource name from deployment output."
+ exit 1
+}
+
+Write-Host "Azure WebApp name: $webApiName"
+
+Write-Host "Configuring Azure WebApp to run from package..."
+az webapp config appsettings set --resource-group $ResourceGroupName --name $webApiName --settings WEBSITE_RUN_FROM_PACKAGE="1" --output none
+if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+}
+
+# Set up deployment command as a string
+$azWebAppCommand = "az webapp deployment source config-zip --resource-group $ResourceGroupName --name $webApiName --src $PackageFilePath"
+
+# Check if DeploymentSlot parameter was passed
+$origins = @("$webApiUrl")
+if ($DeploymentSlot) {
+ Write-Host "Checking if slot $DeploymentSlot exists for '$webApiName'..."
+ $azWebAppCommand += " --slot $DeploymentSlot"
+ $slotInfo = az webapp deployment slot list --resource-group $ResourceGroupName --name $webApiName | ConvertFrom-JSON
+ $availableSlots = $slotInfo | Select-Object -Property Name
+ $origins = $slotInfo | Select-Object -Property defaultHostName
+ $slotExists = false
+
+ foreach ($slot in $availableSlots) {
+ if ($slot.name -eq $DeploymentSlot) {
+ # Deployment slot was found we dont need to create it
+ $slotExists = true
+ }
+ }
+
+ # Web App deployment slot does not exist, create it
+ if (!$slotExists) {
+ Write-Host "Deployment slot $DeploymentSlot does not exist, creating..."
+ az webapp deployment slot create --slot $DeploymentSlot --resource-group $ResourceGroupName --name $webApiName --output none
+ $origins = az webapp deployment slot list --resource-group $ResourceGroupName --name $webApiName | ConvertFrom-JSON | Select-Object -Property defaultHostName
+ }
+}
+
+Write-Host "Deploying '$PackageFilePath' to Azure WebApp '$webApiName'..."
+
+# Invoke the command string
+Invoke-Expression $azWebAppCommand
+if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+}
+
+if (-Not $SkipAppRegistration) {
+ $webapiSettings = $(az webapp config appsettings list --name $webapiName --resource-group $ResourceGroupName | ConvertFrom-JSON)
+ $frontendClientId = ($webapiSettings | Where-Object -Property name -EQ -Value Frontend:AadClientId).value
+ $objectId = (az ad app show --id $frontendClientId | 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
+ $needToUpdateRegistration = $false
+
+ foreach ($address in $origins) {
+ $origin = "https://$address"
+ Write-Host "Ensuring '$origin' is included in AAD app registration's redirect URIs..."
+
+ if ($redirectUris -notcontains "$origin") {
+ $redirectUris += "$origin"
+ $needToUpdateRegistration = $true
+ }
+ }
+
+ if ($needToUpdateRegistration) {
+ $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) {
+ Write-Host "Failed to update AAD app registration - Use -SkipAppRegistration switch to skip this step"
+ exit $LASTEXITCODE
+ }
+ }
+}
+
+if (-Not $SkipCorsRegistration) {
+ foreach ($pluginName in $pluginNames) {
+ $allowedOrigins = $((az webapp cors show --name $pluginName --resource-group $ResourceGroupName --subscription $Subscription | ConvertFrom-Json).allowedOrigins)
+ foreach ($address in $origins) {
+ $origin = "https://$address"
+ Write-Host "Ensuring '$origin' is included in CORS origins for plugin '$pluginName'..."
+ if (-not $allowedOrigins -contains $origin) {
+ az webapp cors add --name $pluginName --resource-group $ResourceGroupName --subscription $Subscription --allowed-origins $origin
+ if ($LASTEXITCODE -ne 0) {
+ Write-Host "Failed to update plugin CORS URIs - Use -SkipCorsRegistration switch to skip this step"
+ exit $LASTEXITCODE
+ }
+ }
+ }
+ }
+}
+
+Write-Host "To verify your deployment, go to 'https://$webApiUrl/' in your browser."
\ No newline at end of file
diff --git a/scripts/deploy/deploy-webapi.sh b/scripts/deploy/deploy-webapi.sh
new file mode 100755
index 000000000..764fe75c1
--- /dev/null
+++ b/scripts/deploy/deploy-webapi.sh
@@ -0,0 +1,203 @@
+#!/bin/bash
+
+# Deploy Chat Copilot application to Azure
+
+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 package file from a 'package-webapi.sh' run (default: \"./out/webapi.zip\")"
+ echo " -o, --slot DEPLOYMENT_SLOT Name of the target web app deployment slot"
+ echo " -sr, --skip-app-registration Skip adding our URI in app registration's redirect URIs"
+ echo " -sc, --skip-cors-registration Skip registration of service with the plugins as allowed CORS origin"
+}
+
+# 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
+ ;;
+ -r|--skip-app-registration)
+ REGISTER_APP=false
+ shift
+ ;;
+ -o|--slot)
+ DEPLOYMENT_SLOT="$2"
+ shift
+ shift
+ ;;
+ -c|--skip-cors-registration)
+ REGISTER_CORS=false
+ 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..."
+DEPLOYMENT_JSON=$(az deployment group show --name $DEPLOYMENT_NAME --resource-group $RESOURCE_GROUP --output json)
+WEB_API_URL=$(echo $DEPLOYMENT_JSON | jq -r '.properties.outputs.webapiUrl.value')
+echo "WEB_API_URL: $WEB_API_URL"
+WEB_API_NAME=$(echo $DEPLOYMENT_JSON | jq -r '.properties.outputs.webapiName.value')
+echo "WEB_API_NAME: $WEB_API_NAME"
+PLUGIN_NAMES=$(echo $DEPLOYMENT_JSON | jq -r '.properties.outputs.pluginNames.value[]')
+# Remove double quotes
+PLUGIN_NAMES=${PLUGIN_NAMES//\"/}
+echo "PLUGIN_NAMES: $PLUGIN_NAMES"
+# Ensure $WEB_API_NAME is set
+if [[ -z "$WEB_API_NAME" ]]; then
+ echo "Could not get Azure WebApp resource name from deployment output."
+ exit 1
+fi
+
+echo "Configuring Azure WebApp to run from package..."
+az webapp config appsettings set --resource-group $RESOURCE_GROUP --name $WEB_API_NAME --settings WEBSITE_RUN_FROM_PACKAGE="1" --output none
+if [ $? -ne 0 ]; then
+ echo "Could not configure Azure WebApp to run from package."
+ exit 1
+fi
+
+# Set up deployment command as a string
+AZ_WEB_APP_CMD="az webapp deployment source config-zip --resource-group $RESOURCE_GROUP --name $WEB_API_NAME --src $PACKAGE_FILE_PATH"
+
+ORIGINS="$WEB_API_URL"
+if [ -n "$DEPLOYMENT_SLOT" ]; then
+ AZ_WEB_APP_CMD+=" --slot ${DEPLOYMENT_SLOT}"
+ echo "Checking whether slot $DEPLOYMENT_SLOT exists for $WEB_APP_NAME..."
+ SLOT_INFO=$(az webapp deployment slot list --resource-group $RESOURCE_GROUP --name $WEB_API_NAME)
+
+ AVAILABLE_SLOTS=$(echo $SLOT_INFO | jq '.[].name')
+ ORIGINS=$(echo $slotInfo | jq '.[].defaultHostName')
+ SLOT_EXISTS=false
+
+ # Checking if the slot exists
+ for SLOT in $(echo "$AVAILABLE_SLOTS" | tr '\n' ' '); do
+ if [[ "$SLOT" == "$DEPLOYMENT_SLOT" ]]; then
+ SLOT_EXISTS=true
+ break
+ fi
+ done
+
+ if [[ "$SLOT_EXISTS" = false ]]; then
+ echo "Deployment slot ${DEPLOYMENT_SLOT} does not exist, creating..."
+
+ az webapp deployment slot create --slot=$DEPLOYMENT_SLOT --resource-group=$RESOURCE_GROUP --name $WEB_API_NAME
+
+ ORIGINS=$(az webapp deployment slot list --resource-group=$RESOURCE_GROUP --name $WEB_API_NAME | jq '.[].defaultHostName')
+ fi
+fi
+
+echo "Deploying '$PACKAGE_FILE_PATH' to Azure WebApp '$WEB_API_NAME'..."
+eval "$AZ_WEB_APP_CMD"
+if [ $? -ne 0 ]; then
+ echo "Could not deploy '$PACKAGE_FILE_PATH' to Azure WebApp '$WEB_API_NAME'."
+ exit 1
+fi
+
+if [[ -z $REGISTER_APP ]]; then
+ WEBAPI_SETTINGS=$(az webapp config appsettings list --name $WEB_API_NAME --resource-group $RESOURCE_GROUP --output json)
+ FRONTEND_CLIENT_ID=$(echo $WEBAPI_SETTINGS | jq -r '.[] | select(.name == "Frontend:AadClientId") | .value')
+ OBJECT_ID=$(az ad app show --id $FRONTEND_CLIENT_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[]')
+ NEED_TO_UPDATE_REG=false
+
+ for ADDRESS in $(echo "$ORIGINS"); do
+ ORIGIN="https://$ADDRESS"
+ echo "Ensuring '$ORIGIN' is included in AAD app registration's redirect URIs..."
+
+ if [[ ! "$REDIRECT_URIS" =~ "$ORIGIN" ]]; then
+ if [[ -n $REDIRECT_URIS ]]; then
+ REDIRECT_URIS=$(echo "$REDIRECT_URIS,$ORIGIN")
+ else
+ REDIRECT_URIS=$(echo "$ORIGIN")
+ fi
+ NEED_TO_UPDATE_REG=true
+ fi
+ done
+
+ if [ $NEED_TO_UPDATE_REG = true ]; then
+ BODY="{spa:{redirectUris:['$(echo "$REDIRECT_URIS")']}}"
+ BODY="${BODY//\,/\',\'}"
+
+ echo "Updating redirects with $BODY"
+
+ az rest \
+ --method PATCH \
+ --uri "https://graph.microsoft.com/v1.0/applications/$OBJECT_ID" \
+ --headers 'Content-Type=application/json' \
+ --body $BODY
+
+ if [ $? -ne 0 ]; then
+ echo "Failed to update app registration $OBJECT_ID with redirect URIs - Use -sc switch to skip this step"
+ exit 1
+ fi
+ fi
+fi
+
+if [[ -z $REGISTER_CORS ]]; then
+ for PLUGIN_NAME in $PLUGIN_NAMES; do
+ ALLOWED_ORIGINS=$(az webapp cors show --name $PLUGIN_NAME --resource-group $RESOURCE_GROUP --subscription $SUBSCRIPTION | jq -r '.allowedOrigins[]')
+ for ADDRESS in $(echo "$ORIGINS"); do
+ ORIGIN="https://$ADDRESS"
+ echo "Ensuring '$ORIGIN' is included in CORS origins for plugin '$PLUGIN_NAME'..."
+ if [[ ! "$ALLOWED_ORIGINS" =~ "$ORIGIN" ]]; then
+ az webapp cors add --name $PLUGIN_NAME --resource-group $RESOURCE_GROUP --subscription $SUBSCRIPTION --allowed-origins "$ORIGIN"
+ if [ $? -ne 0 ]; then
+ echo "Failed to update CORS origins with $ORIGIN - Use -sc switch to skip this step"
+ exit 1
+ fi
+ fi
+ done
+ done
+fi
+
+echo "To verify your deployment, go to 'https://$WEB_API_URL/' in your browser."
diff --git a/scripts/deploy/main.bicep b/scripts/deploy/main.bicep
new file mode 100644
index 000000000..cc7991e7c
--- /dev/null
+++ b/scripts/deploy/main.bicep
@@ -0,0 +1,1149 @@
+/*
+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 webApiPackageUri string = 'https://aka.ms/copilotchat/webapi/latest'
+
+@description('Location of package to deploy as the memory pipeline')
+#disable-next-line no-hardcoded-env-urls
+param memoryPipelinePackageUri string = 'https://aka.ms/copilotchat/memorypipeline/latest'
+
+@description('Location of the websearcher plugin to deploy')
+#disable-next-line no-hardcoded-env-urls
+param webSearcherPackageUri string = 'https://aka.ms/copilotchat/websearcher/latest'
+
+@description('Underlying AI service')
+@allowed([
+ 'AzureOpenAI'
+ 'OpenAI'
+])
+param aiService string = 'AzureOpenAI'
+
+@description('Model to use for chat completions')
+param completionModel string = 'gpt-4o'
+
+@description('Model to use for text embeddings')
+param embeddingModel string = 'text-embedding-ada-002'
+
+@description('Azure OpenAI endpoint to use (Azure OpenAI only)')
+param aiEndpoint string = ''
+
+@secure()
+@description('Azure OpenAI or OpenAI API key')
+param aiApiKey string
+
+@description('Azure AD client ID for the backend web API')
+param webApiClientId string
+
+@description('Azure AD client ID for the frontend')
+param frontendClientId string
+
+@description('Azure AD tenant ID for authenticating users')
+param azureAdTenantId string
+
+@description('Azure AD cloud instance for authenticating users')
+param azureAdInstance string = environment().authentication.loginEndpoint
+
+@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('What method to use to persist embeddings')
+@allowed([
+ 'AzureAISearch'
+ 'Qdrant'
+])
+param memoryStore string = 'AzureAISearch'
+
+@description('Whether to deploy Azure Speech Services to enable input by voice')
+param deploySpeechServices bool = true
+
+@description('Whether to deploy the web searcher plugin, which requires a Bing resource')
+param deployWebSearcherPlugin bool = false
+
+@description('Whether to deploy pre-built binary packages to the cloud')
+param deployPackages bool = true
+
+@description('Region for the resources')
+param location string = resourceGroup().location
+
+@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@2023-05-01' = if (deployNewAzureOpenAI) {
+ name: 'ai-${uniqueName}'
+ location: location
+ kind: 'OpenAI'
+ sku: {
+ name: 'S0'
+ }
+ properties: {
+ customSubDomainName: toLower(uniqueName)
+ }
+}
+
+resource openAI_completionModel 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = if (deployNewAzureOpenAI) {
+ parent: openAI
+ name: completionModel
+ sku: {
+ name: 'Standard'
+ capacity: 30
+ }
+ properties: {
+ model: {
+ format: 'OpenAI'
+ name: completionModel
+ }
+ }
+}
+
+resource openAI_embeddingModel 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = if (deployNewAzureOpenAI) {
+ parent: openAI
+ name: embeddingModel
+ sku: {
+ name: 'Standard'
+ capacity: 30
+ }
+ properties: {
+ model: {
+ format: 'OpenAI'
+ name: embeddingModel
+ }
+ }
+ 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: memoryStore == 'Qdrant' ? virtualNetwork.properties.subnets[0].id : null
+ siteConfig: {
+ healthCheckPath: '/healthz'
+ }
+ }
+}
+
+resource appServiceWebConfig 'Microsoft.Web/sites/config@2022-09-01' = {
+ parent: appServiceWeb
+ name: 'web'
+ dependsOn: [
+ webSubnetConnection
+ ]
+ 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: concat([
+ {
+ name: 'Authentication:Type'
+ value: 'AzureAd'
+ }
+ {
+ name: 'Authentication:AzureAd:Instance'
+ value: azureAdInstance
+ }
+ {
+ name: 'Authentication:AzureAd:TenantId'
+ value: azureAdTenantId
+ }
+ {
+ name: 'Authentication:AzureAd:ClientId'
+ value: webApiClientId
+ }
+ {
+ name: 'Authentication:AzureAd:Scopes'
+ value: 'access_as_user'
+ }
+ {
+ 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: '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: 'Frontend:AadClientId'
+ value: frontendClientId
+ }
+ {
+ name: 'Logging:LogLevel:Default'
+ value: 'Warning'
+ }
+ {
+ name: 'Logging:LogLevel:CopilotChat.WebApi'
+ 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: 'Logging:ApplicationInsights:LogLevel:Default'
+ value: 'Warning'
+ }
+ {
+ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
+ value: appInsights.properties.ConnectionString
+ }
+ {
+ name: 'ApplicationInsightsAgent_EXTENSION_VERSION'
+ value: '~2'
+ }
+ {
+ name: 'KernelMemory:DocumentStorageType'
+ value: 'AzureBlobs'
+ }
+ {
+ name: 'KernelMemory:TextGeneratorType'
+ value: aiService
+ }
+ {
+ name: 'KernelMemory:DataIngestion:OrchestrationType'
+ value: 'Distributed'
+ }
+ {
+ name: 'KernelMemory:DataIngestion:DistributedOrchestration:QueueType'
+ value: 'AzureQueue'
+ }
+ {
+ name: 'KernelMemory:DataIngestion:EmbeddingGeneratorTypes:0'
+ value: aiService
+ }
+ {
+ name: 'KernelMemory:DataIngestion:MemoryDbTypes:0'
+ value: memoryStore
+ }
+ {
+ name: 'KernelMemory:Retrieval:MemoryDbType'
+ value: memoryStore
+ }
+ {
+ name: 'KernelMemory:Retrieval:EmbeddingGeneratorType'
+ value: aiService
+ }
+ {
+ name: 'KernelMemory:Services:AzureBlobs:Auth'
+ value: 'ConnectionString'
+ }
+ {
+ name: 'KernelMemory:Services:AzureBlobs:ConnectionString'
+ value: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[1].value}'
+ }
+ {
+ name: 'KernelMemory:Services:AzureBlobs:Container'
+ value: 'chatmemory'
+ }
+ {
+ name: 'KernelMemory:Services:AzureQueue:Auth'
+ value: 'ConnectionString'
+ }
+ {
+ name: 'KernelMemory:Services:AzureQueue:ConnectionString'
+ value: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[1].value}'
+ }
+ {
+ name: 'KernelMemory:Services:AzureAISearch:Auth'
+ value: 'ApiKey'
+ }
+ {
+ name: 'KernelMemory:Services:AzureAISearch:Endpoint'
+ value: memoryStore == 'AzureAISearch' ? 'https://${azureAISearch.name}.search.windows.net' : ''
+ }
+ {
+ name: 'KernelMemory:Services:AzureAISearch:APIKey'
+ value: memoryStore == 'AzureAISearch' ? azureAISearch.listAdminKeys().primaryKey : ''
+ }
+ {
+ name: 'KernelMemory:Services:Qdrant:Endpoint'
+ value: memoryStore == 'Qdrant' ? 'https://${appServiceQdrant.properties.defaultHostName}' : ''
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIText:Auth'
+ value: 'ApiKey'
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIText:Endpoint'
+ value: deployNewAzureOpenAI ? openAI.properties.endpoint : aiEndpoint
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIText:APIKey'
+ value: deployNewAzureOpenAI ? openAI.listKeys().key1 : aiApiKey
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIText:Deployment'
+ value: completionModel
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIEmbedding:Auth'
+ value: 'ApiKey'
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIEmbedding:Endpoint'
+ value: deployNewAzureOpenAI ? openAI.properties.endpoint : aiEndpoint
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIEmbedding:APIKey'
+ value: deployNewAzureOpenAI ? openAI.listKeys().key1 : aiApiKey
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIEmbedding:Deployment'
+ value: embeddingModel
+ }
+ {
+ name: 'KernelMemory:Services:OpenAI:TextModel'
+ value: completionModel
+ }
+ {
+ name: 'KernelMemory:Services:OpenAI:EmbeddingModel'
+ value: embeddingModel
+ }
+ {
+ name: 'KernelMemory:Services:OpenAI:APIKey'
+ value: aiApiKey
+ }
+ {
+ name: 'Plugins:0:Name'
+ value: 'Klarna Shopping'
+ }
+ {
+ name: 'Plugins:0:ManifestDomain'
+ value: 'https://www.klarna.com'
+ }
+ ],
+ (deployWebSearcherPlugin) ? [
+ {
+ name: 'Plugins:1:Name'
+ value: 'WebSearcher'
+ }
+ {
+ name: 'Plugins:1:ManifestDomain'
+ value: 'https://${functionAppWebSearcherPlugin.properties.defaultHostName}'
+ }
+ {
+ name: 'Plugins:1:Key'
+ value: listkeys('${functionAppWebSearcherPlugin.id}/host/default/', '2022-09-01').functionKeys.default
+ }
+ ] : []
+ )
+ }
+}
+
+resource appServiceWebDeploy 'Microsoft.Web/sites/extensions@2022-09-01' = if (deployPackages) {
+ name: 'MSDeploy'
+ kind: 'string'
+ parent: appServiceWeb
+ properties: {
+ packageUri: webApiPackageUri
+ }
+ dependsOn: [
+ appServiceWebConfig
+ ]
+}
+
+resource appServiceMemoryPipeline 'Microsoft.Web/sites@2022-09-01' = {
+ name: 'app-${uniqueName}-memorypipeline'
+ location: location
+ kind: 'app'
+ tags: {
+ skweb: '1'
+ }
+ properties: {
+ httpsOnly: true
+ serverFarmId: appServicePlan.id
+ virtualNetworkSubnetId: memoryStore == 'Qdrant' ? virtualNetwork.properties.subnets[0].id : null
+ siteConfig: {
+ alwaysOn: true
+ }
+ }
+}
+
+resource appServiceMemoryPipelineConfig 'Microsoft.Web/sites/config@2022-09-01' = {
+ parent: appServiceMemoryPipeline
+ name: 'web'
+ dependsOn: [
+ memSubnetConnection
+ ]
+ properties: {
+ alwaysOn: true
+ detailedErrorLoggingEnabled: true
+ minTlsVersion: '1.2'
+ netFrameworkVersion: 'v6.0'
+ use32BitWorkerProcess: false
+ vnetRouteAllEnabled: true
+ appSettings: [
+ {
+ name: 'KernelMemory:DocumentStorageType'
+ value: 'AzureBlobs'
+ }
+ {
+ name: 'KernelMemory:TextGeneratorType'
+ value: aiService
+ }
+ {
+ name: 'KernelMemory:DataIngestion:ImageOcrType'
+ value: 'AzureFormRecognizer'
+ }
+ {
+ name: 'KernelMemory:DataIngestion:OrchestrationType'
+ value: 'Distributed'
+ }
+ {
+ name: 'KernelMemory:DataIngestion:DistributedOrchestration:QueueType'
+ value: 'AzureQueue'
+ }
+ {
+ name: 'KernelMemory:DataIngestion:EmbeddingGeneratorTypes:0'
+ value: aiService
+ }
+ {
+ name: 'KernelMemory:DataIngestion:MemoryDbTypes:0'
+ value: memoryStore
+ }
+ {
+ name: 'KernelMemory:Retrieval:MemoryDbType'
+ value: memoryStore
+ }
+ {
+ name: 'KernelMemory:Retrieval:EmbeddingGeneratorType'
+ value: aiService
+ }
+ {
+ name: 'KernelMemory:Services:AzureBlobs:Auth'
+ value: 'ConnectionString'
+ }
+ {
+ name: 'KernelMemory:Services:AzureBlobs:ConnectionString'
+ value: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[1].value}'
+ }
+ {
+ name: 'KernelMemory:Services:AzureBlobs:Container'
+ value: 'chatmemory'
+ }
+ {
+ name: 'KernelMemory:Services:AzureQueue:Auth'
+ value: 'ConnectionString'
+ }
+ {
+ name: 'KernelMemory:Services:AzureQueue:ConnectionString'
+ value: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[1].value}'
+ }
+ {
+ name: 'KernelMemory:Services:AzureAISearch:Auth'
+ value: 'ApiKey'
+ }
+ {
+ name: 'KernelMemory:Services:AzureAISearch:Endpoint'
+ value: memoryStore == 'AzureAISearch' ? 'https://${azureAISearch.name}.search.windows.net' : ''
+ }
+ {
+ name: 'KernelMemory:Services:AzureAISearch:APIKey'
+ value: memoryStore == 'AzureAISearch' ? azureAISearch.listAdminKeys().primaryKey : ''
+ }
+ {
+ name: 'KernelMemory:Services:Qdrant:Endpoint'
+ value: memoryStore == 'Qdrant' ? 'https://${appServiceQdrant.properties.defaultHostName}' : ''
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIText:Auth'
+ value: 'ApiKey'
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIText:Endpoint'
+ value: deployNewAzureOpenAI ? openAI.properties.endpoint : aiEndpoint
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIText:APIKey'
+ value: deployNewAzureOpenAI ? openAI.listKeys().key1 : aiApiKey
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIText:Deployment'
+ value: completionModel
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIEmbedding:Auth'
+ value: 'ApiKey'
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIEmbedding:Endpoint'
+ value: deployNewAzureOpenAI ? openAI.properties.endpoint : aiEndpoint
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIEmbedding:APIKey'
+ value: deployNewAzureOpenAI ? openAI.listKeys().key1 : aiApiKey
+ }
+ {
+ name: 'KernelMemory:Services:AzureOpenAIEmbedding:Deployment'
+ value: embeddingModel
+ }
+ {
+ name: 'KernelMemory:Services:AzureFormRecognizer:Auth'
+ value: 'ApiKey'
+ }
+ {
+ name: 'KernelMemory:Services:AzureFormRecognizer:Endpoint'
+ value: ocrAccount.properties.endpoint
+ }
+ {
+ name: 'KernelMemory:Services:AzureFormRecognizer:APIKey'
+ value: ocrAccount.listKeys().key1
+ }
+ {
+ name: 'KernelMemory:Services:OpenAI:TextModel'
+ value: completionModel
+ }
+ {
+ name: 'KernelMemory:Services:OpenAI:EmbeddingModel'
+ value: embeddingModel
+ }
+ {
+ name: 'KernelMemory:Services:OpenAI:APIKey'
+ value: aiApiKey
+ }
+ {
+ name: 'Logging:LogLevel:Default'
+ value: 'Information'
+ }
+ {
+ name: 'Logging:LogLevel:AspNetCore'
+ value: 'Warning'
+ }
+ {
+ name: 'Logging:ApplicationInsights:LogLevel:Default'
+ value: 'Warning'
+ }
+ {
+ name: 'ApplicationInsights:ConnectionString'
+ value: appInsights.properties.ConnectionString
+ }
+ ]
+ }
+}
+
+resource appServiceMemoryPipelineDeploy 'Microsoft.Web/sites/extensions@2022-09-01' = if (deployPackages) {
+ name: 'MSDeploy'
+ kind: 'string'
+ parent: appServiceMemoryPipeline
+ properties: {
+ packageUri: memoryPipelinePackageUri
+ }
+ dependsOn: [
+ appServiceMemoryPipelineConfig
+ ]
+}
+
+resource functionAppWebSearcherPlugin 'Microsoft.Web/sites@2022-09-01' = if (deployWebSearcherPlugin) {
+ name: 'function-${uniqueName}-websearcher-plugin'
+ location: location
+ kind: 'functionapp'
+ tags: {
+ skweb: '1'
+ }
+ properties: {
+ serverFarmId: appServicePlan.id
+ httpsOnly: true
+ siteConfig: {
+ alwaysOn: true
+ }
+ }
+}
+
+resource functionAppWebSearcherPluginConfig 'Microsoft.Web/sites/config@2022-09-01' = if (deployWebSearcherPlugin) {
+ parent: functionAppWebSearcherPlugin
+ name: 'web'
+ properties: {
+ minTlsVersion: '1.2'
+ appSettings: [
+ {
+ name: 'FUNCTIONS_EXTENSION_VERSION'
+ value: '~4'
+ }
+ {
+ name: 'FUNCTIONS_WORKER_RUNTIME'
+ value: 'dotnet-isolated'
+ }
+ {
+ name: 'AzureWebJobsStorage'
+ value: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[1].value}'
+ }
+ {
+ name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
+ value: appInsights.properties.InstrumentationKey
+ }
+ {
+ name: 'PluginConfig:BingApiKey'
+ value: (deployWebSearcherPlugin) ? bingSearchService.listKeys().key1 : ''
+ }
+ ]
+ }
+}
+
+resource functionAppWebSearcherDeploy 'Microsoft.Web/sites/extensions@2022-09-01' = if (deployPackages && deployWebSearcherPlugin) {
+ name: 'MSDeploy'
+ kind: 'string'
+ parent: functionAppWebSearcherPlugin
+ properties: {
+ packageUri: webSearcherPackageUri
+ }
+ dependsOn: [
+ functionAppWebSearcherPluginConfig
+ ]
+}
+
+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 appInsightExtensionWeb 'Microsoft.Web/sites/siteextensions@2022-09-01' = {
+ parent: appServiceWeb
+ name: 'Microsoft.ApplicationInsights.AzureWebSites'
+ dependsOn: [ appServiceWebDeploy ]
+}
+
+resource appInsightExtensionMemory 'Microsoft.Web/sites/siteextensions@2022-09-01' = {
+ parent: appServiceMemoryPipeline
+ name: 'Microsoft.ApplicationInsights.AzureWebSites'
+ dependsOn: [ appServiceMemoryPipelineDeploy ]
+}
+
+resource appInsightExtensionWebSearchPlugin 'Microsoft.Web/sites/siteextensions@2022-09-01' = if (deployWebSearcherPlugin) {
+ parent: functionAppWebSearcherPlugin
+ name: 'Microsoft.ApplicationInsights.AzureWebSites'
+ dependsOn: [ functionAppWebSearcherDeploy ]
+}
+
+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' = {
+ 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' = if (memoryStore == 'Qdrant') {
+ name: 'default'
+ resource share 'shares' = {
+ name: storageFileShareName
+ }
+ }
+}
+
+resource appServicePlanQdrant 'Microsoft.Web/serverfarms@2022-03-01' = if (memoryStore == 'Qdrant') {
+ name: 'asp-${uniqueName}-qdrant'
+ location: location
+ kind: 'linux'
+ sku: {
+ name: 'P1v3'
+ }
+ properties: {
+ reserved: true
+ }
+}
+
+resource appServiceQdrant 'Microsoft.Web/sites@2022-09-01' = if (memoryStore == 'Qdrant') {
+ name: 'app-${uniqueName}-qdrant'
+ location: location
+ kind: 'app,linux,container'
+ properties: {
+ serverFarmId: appServicePlanQdrant.id
+ httpsOnly: true
+ reserved: true
+ clientCertMode: 'Required'
+ virtualNetworkSubnetId: memoryStore == 'Qdrant' ? virtualNetwork.properties.subnets[1].id : null
+ siteConfig: {
+ numberOfWorkers: 1
+ linuxFxVersion: 'DOCKER|qdrant/qdrant:latest'
+ alwaysOn: true
+ vnetRouteAllEnabled: true
+ ipSecurityRestrictions: [
+ {
+ vnetSubnetResourceId: memoryStore == 'Qdrant' ? virtualNetwork.properties.subnets[0].id : null
+ action: 'Allow'
+ priority: 300
+ name: 'Allow front vnet'
+ }
+ {
+ ipAddress: 'Any'
+ action: 'Deny'
+ priority: 2147483647
+ name: 'Deny all'
+ }
+ ]
+ azureStorageAccounts: {
+ aciqdrantshare: {
+ type: 'AzureFiles'
+ accountName: memoryStore == 'Qdrant' ? storage.name : 'notdeployed'
+ shareName: storageFileShareName
+ mountPath: '/qdrant/storage'
+ accessKey: memoryStore == 'Qdrant' ? storage.listKeys().keys[0].value : ''
+ }
+ }
+ }
+ }
+}
+
+resource azureAISearch 'Microsoft.Search/searchServices@2022-09-01' = if (memoryStore == 'AzureAISearch') {
+ name: 'acs-${uniqueName}'
+ location: location
+ sku: {
+ name: 'basic'
+ }
+ properties: {
+ replicaCount: 1
+ partitionCount: 1
+ }
+}
+
+resource virtualNetwork 'Microsoft.Network/virtualNetworks@2021-05-01' = if (memoryStore == 'Qdrant') {
+ 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' = if (memoryStore == 'Qdrant') {
+ 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' = if (memoryStore == 'Qdrant') {
+ name: 'nsg-${uniqueName}-qdrant'
+ location: location
+ properties: {
+ securityRules: []
+ }
+}
+
+resource webSubnetConnection 'Microsoft.Web/sites/virtualNetworkConnections@2022-09-01' = if (memoryStore == 'Qdrant') {
+ parent: appServiceWeb
+ name: 'webSubnetConnection'
+ properties: {
+ vnetResourceId: memoryStore == 'Qdrant' ? virtualNetwork.properties.subnets[0].id : null
+ isSwift: true
+ }
+}
+
+resource memSubnetConnection 'Microsoft.Web/sites/virtualNetworkConnections@2022-09-01' = if (memoryStore == 'Qdrant') {
+ parent: appServiceMemoryPipeline
+ name: 'memSubnetConnection'
+ properties: {
+ vnetResourceId: memoryStore == 'Qdrant' ? virtualNetwork.properties.subnets[0].id : null
+ isSwift: true
+ }
+}
+
+resource qdrantSubnetConnection 'Microsoft.Web/sites/virtualNetworkConnections@2022-09-01' = if (memoryStore == 'Qdrant') {
+ parent: appServiceQdrant
+ name: 'qdrantSubnetConnection'
+ properties: {
+ vnetResourceId: memoryStore == 'Qdrant' ? virtualNetwork.properties.subnets[1].id : null
+ 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: [
+ '/chatId'
+ ]
+ 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: [
+ '/userId'
+ ]
+ 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: [
+ '/chatId'
+ ]
+ kind: 'Hash'
+ version: 2
+ }
+ }
+ }
+}
+
+resource speechAccount 'Microsoft.CognitiveServices/accounts@2022-12-01' = if (deploySpeechServices) {
+ name: 'cog-speech-${uniqueName}'
+ location: location
+ sku: {
+ name: 'S0'
+ }
+ kind: 'SpeechServices'
+ identity: {
+ type: 'None'
+ }
+ properties: {
+ customSubDomainName: 'cog-speech-${uniqueName}'
+ networkAcls: {
+ defaultAction: 'Allow'
+ }
+ publicNetworkAccess: 'Enabled'
+ }
+}
+
+resource ocrAccount 'Microsoft.CognitiveServices/accounts@2022-12-01' = {
+ name: 'cog-ocr-${uniqueName}'
+ location: location
+ sku: {
+ name: 'S0'
+ }
+ kind: 'FormRecognizer'
+ identity: {
+ type: 'None'
+ }
+ properties: {
+ customSubDomainName: 'cog-ocr-${uniqueName}'
+ networkAcls: {
+ defaultAction: 'Allow'
+ }
+ publicNetworkAccess: 'Enabled'
+ }
+}
+
+resource bingSearchService 'Microsoft.Bing/accounts@2020-06-10' = if (deployWebSearcherPlugin) {
+ name: 'bing-search-${uniqueName}'
+ location: 'global'
+ sku: {
+ name: 'S1'
+ }
+ kind: 'Bing.Search.v7'
+}
+
+output webapiUrl string = appServiceWeb.properties.defaultHostName
+output webapiName string = appServiceWeb.name
+output memoryPipelineName string = appServiceMemoryPipeline.name
+output pluginNames array = concat(
+ [],
+ (deployWebSearcherPlugin) ? [ functionAppWebSearcherPlugin.name ] : []
+)
diff --git a/scripts/deploy/main.json b/scripts/deploy/main.json
new file mode 100644
index 000000000..92141d4fb
--- /dev/null
+++ b/scripts/deploy/main.json
@@ -0,0 +1,1160 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.28.1.47646",
+ "templateHash": "18195377528136882013"
+ }
+ },
+ "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"
+ }
+ },
+ "webApiPackageUri": {
+ "type": "string",
+ "defaultValue": "https://aka.ms/copilotchat/webapi/latest",
+ "metadata": {
+ "description": "Location of package to deploy as the web service"
+ }
+ },
+ "memoryPipelinePackageUri": {
+ "type": "string",
+ "defaultValue": "https://aka.ms/copilotchat/memorypipeline/latest",
+ "metadata": {
+ "description": "Location of package to deploy as the memory pipeline"
+ }
+ },
+ "webSearcherPackageUri": {
+ "type": "string",
+ "defaultValue": "https://aka.ms/copilotchat/websearcher/latest",
+ "metadata": {
+ "description": "Location of the websearcher plugin to deploy"
+ }
+ },
+ "aiService": {
+ "type": "string",
+ "defaultValue": "AzureOpenAI",
+ "allowedValues": [
+ "AzureOpenAI",
+ "OpenAI"
+ ],
+ "metadata": {
+ "description": "Underlying AI service"
+ }
+ },
+ "completionModel": {
+ "type": "string",
+ "defaultValue": "gpt-4o",
+ "metadata": {
+ "description": "Model to use for chat completions"
+ }
+ },
+ "embeddingModel": {
+ "type": "string",
+ "defaultValue": "text-embedding-ada-002",
+ "metadata": {
+ "description": "Model to use for text embeddings"
+ }
+ },
+ "aiEndpoint": {
+ "type": "string",
+ "defaultValue": "",
+ "metadata": {
+ "description": "Azure OpenAI endpoint to use (Azure OpenAI only)"
+ }
+ },
+ "aiApiKey": {
+ "type": "securestring",
+ "metadata": {
+ "description": "Azure OpenAI or OpenAI API key"
+ }
+ },
+ "webApiClientId": {
+ "type": "string",
+ "metadata": {
+ "description": "Azure AD client ID for the backend web API"
+ }
+ },
+ "frontendClientId": {
+ "type": "string",
+ "metadata": {
+ "description": "Azure AD client ID for the frontend"
+ }
+ },
+ "azureAdTenantId": {
+ "type": "string",
+ "metadata": {
+ "description": "Azure AD tenant ID for authenticating users"
+ }
+ },
+ "azureAdInstance": {
+ "type": "string",
+ "defaultValue": "[environment().authentication.loginEndpoint]",
+ "metadata": {
+ "description": "Azure AD cloud instance for authenticating users"
+ }
+ },
+ "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"
+ }
+ },
+ "memoryStore": {
+ "type": "string",
+ "defaultValue": "AzureAISearch",
+ "allowedValues": [
+ "AzureAISearch",
+ "Qdrant"
+ ],
+ "metadata": {
+ "description": "What method to use to persist embeddings"
+ }
+ },
+ "deploySpeechServices": {
+ "type": "bool",
+ "defaultValue": true,
+ "metadata": {
+ "description": "Whether to deploy Azure Speech Services to enable input by voice"
+ }
+ },
+ "deployWebSearcherPlugin": {
+ "type": "bool",
+ "defaultValue": false,
+ "metadata": {
+ "description": "Whether to deploy the web searcher plugin, which requires a Bing resource"
+ }
+ },
+ "deployPackages": {
+ "type": "bool",
+ "defaultValue": true,
+ "metadata": {
+ "description": "Whether to deploy pre-built binary packages to the cloud"
+ }
+ },
+ "location": {
+ "type": "string",
+ "defaultValue": "[resourceGroup().location]",
+ "metadata": {
+ "description": "Region for the resources"
+ }
+ }
+ },
+ "variables": {
+ "rgIdHash": "[uniqueString(resourceGroup().id)]",
+ "uniqueName": "[format('{0}-{1}', parameters('name'), variables('rgIdHash'))]",
+ "storageFileShareName": "aciqdrantshare"
+ },
+ "resources": [
+ {
+ "condition": "[equals(parameters('memoryStore'), 'Qdrant')]",
+ "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": "[equals(parameters('memoryStore'), 'Qdrant')]",
+ "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": "2023-05-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": "2023-05-01",
+ "name": "[format('{0}/{1}', format('ai-{0}', variables('uniqueName')), parameters('completionModel'))]",
+ "sku": {
+ "name": "Standard",
+ "capacity": 30
+ },
+ "properties": {
+ "model": {
+ "format": "OpenAI",
+ "name": "[parameters('completionModel')]"
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName')))]"
+ ]
+ },
+ {
+ "condition": "[parameters('deployNewAzureOpenAI')]",
+ "type": "Microsoft.CognitiveServices/accounts/deployments",
+ "apiVersion": "2023-05-01",
+ "name": "[format('{0}/{1}', format('ai-{0}', variables('uniqueName')), parameters('embeddingModel'))]",
+ "sku": {
+ "name": "Standard",
+ "capacity": 30
+ },
+ "properties": {
+ "model": {
+ "format": "OpenAI",
+ "name": "[parameters('embeddingModel')]"
+ }
+ },
+ "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": "[if(equals(parameters('memoryStore'), 'Qdrant'), reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[0].id, null())]",
+ "siteConfig": {
+ "healthCheckPath": "/healthz"
+ }
+ },
+ "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": 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": "[concat(createArray(createObject('name', 'Authentication:Type', 'value', 'AzureAd'), createObject('name', 'Authentication:AzureAd:Instance', 'value', parameters('azureAdInstance')), createObject('name', 'Authentication:AzureAd:TenantId', 'value', parameters('azureAdTenantId')), createObject('name', 'Authentication:AzureAd:ClientId', 'value', parameters('webApiClientId')), createObject('name', 'Authentication:AzureAd:Scopes', 'value', 'access_as_user'), createObject('name', 'ChatStore:Type', 'value', if(parameters('deployCosmosDB'), 'cosmos', 'volatile')), createObject('name', 'ChatStore:Cosmos:Database', 'value', 'CopilotChat'), createObject('name', 'ChatStore:Cosmos:ChatSessionsContainer', 'value', 'chatsessions'), createObject('name', 'ChatStore:Cosmos:ChatMessagesContainer', 'value', 'chatmessages'), createObject('name', 'ChatStore:Cosmos:ChatMemorySourcesContainer', 'value', 'chatmemorysources'), createObject('name', 'ChatStore:Cosmos:ChatParticipantsContainer', 'value', 'chatparticipants'), createObject('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, '')), createObject('name', 'AzureSpeech:Region', 'value', parameters('location')), createObject('name', 'AzureSpeech:Key', 'value', if(parameters('deploySpeechServices'), listKeys(resourceId('Microsoft.CognitiveServices/accounts', format('cog-speech-{0}', variables('uniqueName'))), '2022-12-01').key1, '')), createObject('name', 'AllowedOrigins', 'value', '[*]'), createObject('name', 'Kestrel:Endpoints:Https:Url', 'value', 'https://localhost:443'), createObject('name', 'Frontend:AadClientId', 'value', parameters('frontendClientId')), createObject('name', 'Logging:LogLevel:Default', 'value', 'Warning'), createObject('name', 'Logging:LogLevel:CopilotChat.WebApi', 'value', 'Warning'), createObject('name', 'Logging:LogLevel:Microsoft.SemanticKernel', 'value', 'Warning'), createObject('name', 'Logging:LogLevel:Microsoft.AspNetCore.Hosting', 'value', 'Warning'), createObject('name', 'Logging:LogLevel:Microsoft.Hosting.Lifetimel', 'value', 'Warning'), createObject('name', 'Logging:ApplicationInsights:LogLevel:Default', 'value', 'Warning'), createObject('name', 'APPLICATIONINSIGHTS_CONNECTION_STRING', 'value', reference(resourceId('Microsoft.Insights/components', format('appins-{0}', variables('uniqueName'))), '2020-02-02').ConnectionString), createObject('name', 'ApplicationInsightsAgent_EXTENSION_VERSION', 'value', '~2'), createObject('name', 'KernelMemory:DocumentStorageType', 'value', 'AzureBlobs'), createObject('name', 'KernelMemory:TextGeneratorType', 'value', parameters('aiService')), createObject('name', 'KernelMemory:DataIngestion:OrchestrationType', 'value', 'Distributed'), createObject('name', 'KernelMemory:DataIngestion:DistributedOrchestration:QueueType', 'value', 'AzureQueue'), createObject('name', 'KernelMemory:DataIngestion:EmbeddingGeneratorTypes:0', 'value', parameters('aiService')), createObject('name', 'KernelMemory:DataIngestion:MemoryDbTypes:0', 'value', parameters('memoryStore')), createObject('name', 'KernelMemory:Retrieval:MemoryDbType', 'value', parameters('memoryStore')), createObject('name', 'KernelMemory:Retrieval:EmbeddingGeneratorType', 'value', parameters('aiService')), createObject('name', 'KernelMemory:Services:AzureBlobs:Auth', 'value', 'ConnectionString'), createObject('name', 'KernelMemory:Services:AzureBlobs:ConnectionString', 'value', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', format('st{0}', variables('rgIdHash')), listKeys(resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash'))), '2022-09-01').keys[1].value)), createObject('name', 'KernelMemory:Services:AzureBlobs:Container', 'value', 'chatmemory'), createObject('name', 'KernelMemory:Services:AzureQueue:Auth', 'value', 'ConnectionString'), createObject('name', 'KernelMemory:Services:AzureQueue:ConnectionString', 'value', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', format('st{0}', variables('rgIdHash')), listKeys(resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash'))), '2022-09-01').keys[1].value)), createObject('name', 'KernelMemory:Services:AzureAISearch:Auth', 'value', 'ApiKey'), createObject('name', 'KernelMemory:Services:AzureAISearch:Endpoint', 'value', if(equals(parameters('memoryStore'), 'AzureAISearch'), format('https://{0}.search.windows.net', format('acs-{0}', variables('uniqueName'))), '')), createObject('name', 'KernelMemory:Services:AzureAISearch:APIKey', 'value', if(equals(parameters('memoryStore'), 'AzureAISearch'), listAdminKeys(resourceId('Microsoft.Search/searchServices', format('acs-{0}', variables('uniqueName'))), '2022-09-01').primaryKey, '')), createObject('name', 'KernelMemory:Services:Qdrant:Endpoint', 'value', if(equals(parameters('memoryStore'), 'Qdrant'), format('https://{0}', reference(resourceId('Microsoft.Web/sites', format('app-{0}-qdrant', variables('uniqueName'))), '2022-09-01').defaultHostName), '')), createObject('name', 'KernelMemory:Services:AzureOpenAIText:Auth', 'value', 'ApiKey'), createObject('name', 'KernelMemory:Services:AzureOpenAIText:Endpoint', 'value', if(parameters('deployNewAzureOpenAI'), reference(resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName'))), '2023-05-01').endpoint, parameters('aiEndpoint'))), createObject('name', 'KernelMemory:Services:AzureOpenAIText:APIKey', 'value', if(parameters('deployNewAzureOpenAI'), listKeys(resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName'))), '2023-05-01').key1, parameters('aiApiKey'))), createObject('name', 'KernelMemory:Services:AzureOpenAIText:Deployment', 'value', parameters('completionModel')), createObject('name', 'KernelMemory:Services:AzureOpenAIEmbedding:Auth', 'value', 'ApiKey'), createObject('name', 'KernelMemory:Services:AzureOpenAIEmbedding:Endpoint', 'value', if(parameters('deployNewAzureOpenAI'), reference(resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName'))), '2023-05-01').endpoint, parameters('aiEndpoint'))), createObject('name', 'KernelMemory:Services:AzureOpenAIEmbedding:APIKey', 'value', if(parameters('deployNewAzureOpenAI'), listKeys(resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName'))), '2023-05-01').key1, parameters('aiApiKey'))), createObject('name', 'KernelMemory:Services:AzureOpenAIEmbedding:Deployment', 'value', parameters('embeddingModel')), createObject('name', 'KernelMemory:Services:OpenAI:TextModel', 'value', parameters('completionModel')), createObject('name', 'KernelMemory:Services:OpenAI:EmbeddingModel', 'value', parameters('embeddingModel')), createObject('name', 'KernelMemory:Services:OpenAI:APIKey', 'value', parameters('aiApiKey')), createObject('name', 'Plugins:0:Name', 'value', 'Klarna Shopping'), createObject('name', 'Plugins:0:ManifestDomain', 'value', 'https://www.klarna.com')), if(parameters('deployWebSearcherPlugin'), createArray(createObject('name', 'Plugins:1:Name', 'value', 'WebSearcher'), createObject('name', 'Plugins:1:ManifestDomain', 'value', format('https://{0}', reference(resourceId('Microsoft.Web/sites', format('function-{0}-websearcher-plugin', variables('uniqueName'))), '2022-09-01').defaultHostName)), createObject('name', 'Plugins:1:Key', 'value', listkeys(format('{0}/host/default/', resourceId('Microsoft.Web/sites', format('function-{0}-websearcher-plugin', variables('uniqueName')))), '2022-09-01').functionKeys.default)), createArray()))]"
+ },
+ "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.Search/searchServices', format('acs-{0}', variables('uniqueName')))]",
+ "[resourceId('Microsoft.DocumentDB/databaseAccounts', toLower(format('cosmos-{0}', variables('uniqueName'))))]",
+ "[resourceId('Microsoft.Web/sites', format('function-{0}-websearcher-plugin', variables('uniqueName')))]",
+ "[resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName')))]",
+ "[resourceId('Microsoft.CognitiveServices/accounts', format('cog-speech-{0}', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash')))]",
+ "[resourceId('Microsoft.Web/sites/virtualNetworkConnections', format('app-{0}-webapi', variables('uniqueName')), 'webSubnetConnection')]"
+ ]
+ },
+ {
+ "condition": "[parameters('deployPackages')]",
+ "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('webApiPackageUri')]"
+ },
+ "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.Web/sites",
+ "apiVersion": "2022-09-01",
+ "name": "[format('app-{0}-memorypipeline', variables('uniqueName'))]",
+ "location": "[parameters('location')]",
+ "kind": "app",
+ "tags": {
+ "skweb": "1"
+ },
+ "properties": {
+ "httpsOnly": true,
+ "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format('asp-{0}-webapi', variables('uniqueName')))]",
+ "virtualNetworkSubnetId": "[if(equals(parameters('memoryStore'), 'Qdrant'), reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[0].id, null())]",
+ "siteConfig": {
+ "alwaysOn": true
+ }
+ },
+ "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}-memorypipeline', variables('uniqueName')), 'web')]",
+ "properties": {
+ "alwaysOn": true,
+ "detailedErrorLoggingEnabled": true,
+ "minTlsVersion": "1.2",
+ "netFrameworkVersion": "v6.0",
+ "use32BitWorkerProcess": false,
+ "vnetRouteAllEnabled": true,
+ "appSettings": [
+ {
+ "name": "KernelMemory:DocumentStorageType",
+ "value": "AzureBlobs"
+ },
+ {
+ "name": "KernelMemory:TextGeneratorType",
+ "value": "[parameters('aiService')]"
+ },
+ {
+ "name": "KernelMemory:DataIngestion:ImageOcrType",
+ "value": "AzureFormRecognizer"
+ },
+ {
+ "name": "KernelMemory:DataIngestion:OrchestrationType",
+ "value": "Distributed"
+ },
+ {
+ "name": "KernelMemory:DataIngestion:DistributedOrchestration:QueueType",
+ "value": "AzureQueue"
+ },
+ {
+ "name": "KernelMemory:DataIngestion:EmbeddingGeneratorTypes:0",
+ "value": "[parameters('aiService')]"
+ },
+ {
+ "name": "KernelMemory:DataIngestion:MemoryDbTypes:0",
+ "value": "[parameters('memoryStore')]"
+ },
+ {
+ "name": "KernelMemory:Retrieval:MemoryDbType",
+ "value": "[parameters('memoryStore')]"
+ },
+ {
+ "name": "KernelMemory:Retrieval:EmbeddingGeneratorType",
+ "value": "[parameters('aiService')]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureBlobs:Auth",
+ "value": "ConnectionString"
+ },
+ {
+ "name": "KernelMemory:Services:AzureBlobs:ConnectionString",
+ "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', format('st{0}', variables('rgIdHash')), listKeys(resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash'))), '2022-09-01').keys[1].value)]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureBlobs:Container",
+ "value": "chatmemory"
+ },
+ {
+ "name": "KernelMemory:Services:AzureQueue:Auth",
+ "value": "ConnectionString"
+ },
+ {
+ "name": "KernelMemory:Services:AzureQueue:ConnectionString",
+ "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', format('st{0}', variables('rgIdHash')), listKeys(resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash'))), '2022-09-01').keys[1].value)]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureAISearch:Auth",
+ "value": "ApiKey"
+ },
+ {
+ "name": "KernelMemory:Services:AzureAISearch:Endpoint",
+ "value": "[if(equals(parameters('memoryStore'), 'AzureAISearch'), format('https://{0}.search.windows.net', format('acs-{0}', variables('uniqueName'))), '')]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureAISearch:APIKey",
+ "value": "[if(equals(parameters('memoryStore'), 'AzureAISearch'), listAdminKeys(resourceId('Microsoft.Search/searchServices', format('acs-{0}', variables('uniqueName'))), '2022-09-01').primaryKey, '')]"
+ },
+ {
+ "name": "KernelMemory:Services:Qdrant:Endpoint",
+ "value": "[if(equals(parameters('memoryStore'), 'Qdrant'), format('https://{0}', reference(resourceId('Microsoft.Web/sites', format('app-{0}-qdrant', variables('uniqueName'))), '2022-09-01').defaultHostName), '')]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureOpenAIText:Auth",
+ "value": "ApiKey"
+ },
+ {
+ "name": "KernelMemory:Services:AzureOpenAIText:Endpoint",
+ "value": "[if(parameters('deployNewAzureOpenAI'), reference(resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName'))), '2023-05-01').endpoint, parameters('aiEndpoint'))]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureOpenAIText:APIKey",
+ "value": "[if(parameters('deployNewAzureOpenAI'), listKeys(resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName'))), '2023-05-01').key1, parameters('aiApiKey'))]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureOpenAIText:Deployment",
+ "value": "[parameters('completionModel')]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureOpenAIEmbedding:Auth",
+ "value": "ApiKey"
+ },
+ {
+ "name": "KernelMemory:Services:AzureOpenAIEmbedding:Endpoint",
+ "value": "[if(parameters('deployNewAzureOpenAI'), reference(resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName'))), '2023-05-01').endpoint, parameters('aiEndpoint'))]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureOpenAIEmbedding:APIKey",
+ "value": "[if(parameters('deployNewAzureOpenAI'), listKeys(resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName'))), '2023-05-01').key1, parameters('aiApiKey'))]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureOpenAIEmbedding:Deployment",
+ "value": "[parameters('embeddingModel')]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureFormRecognizer:Auth",
+ "value": "ApiKey"
+ },
+ {
+ "name": "KernelMemory:Services:AzureFormRecognizer:Endpoint",
+ "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', format('cog-ocr-{0}', variables('uniqueName'))), '2022-12-01').endpoint]"
+ },
+ {
+ "name": "KernelMemory:Services:AzureFormRecognizer:APIKey",
+ "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', format('cog-ocr-{0}', variables('uniqueName'))), '2022-12-01').key1]"
+ },
+ {
+ "name": "KernelMemory:Services:OpenAI:TextModel",
+ "value": "[parameters('completionModel')]"
+ },
+ {
+ "name": "KernelMemory:Services:OpenAI:EmbeddingModel",
+ "value": "[parameters('embeddingModel')]"
+ },
+ {
+ "name": "KernelMemory:Services:OpenAI:APIKey",
+ "value": "[parameters('aiApiKey')]"
+ },
+ {
+ "name": "Logging:LogLevel:Default",
+ "value": "Information"
+ },
+ {
+ "name": "Logging:LogLevel:AspNetCore",
+ "value": "Warning"
+ },
+ {
+ "name": "Logging:ApplicationInsights:LogLevel:Default",
+ "value": "Warning"
+ },
+ {
+ "name": "ApplicationInsights:ConnectionString",
+ "value": "[reference(resourceId('Microsoft.Insights/components', format('appins-{0}', variables('uniqueName'))), '2020-02-02').ConnectionString]"
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Insights/components', format('appins-{0}', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Web/sites', format('app-{0}-memorypipeline', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Web/sites', format('app-{0}-qdrant', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Search/searchServices', format('acs-{0}', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Web/sites/virtualNetworkConnections', format('app-{0}-memorypipeline', variables('uniqueName')), 'memSubnetConnection')]",
+ "[resourceId('Microsoft.CognitiveServices/accounts', format('cog-ocr-{0}', variables('uniqueName')))]",
+ "[resourceId('Microsoft.CognitiveServices/accounts', format('ai-{0}', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash')))]"
+ ]
+ },
+ {
+ "condition": "[parameters('deployPackages')]",
+ "type": "Microsoft.Web/sites/extensions",
+ "apiVersion": "2022-09-01",
+ "name": "[format('{0}/{1}', format('app-{0}-memorypipeline', variables('uniqueName')), 'MSDeploy')]",
+ "kind": "string",
+ "properties": {
+ "packageUri": "[parameters('memoryPipelinePackageUri')]"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/sites', format('app-{0}-memorypipeline', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Web/sites/config', format('app-{0}-memorypipeline', variables('uniqueName')), 'web')]"
+ ]
+ },
+ {
+ "condition": "[parameters('deployWebSearcherPlugin')]",
+ "type": "Microsoft.Web/sites",
+ "apiVersion": "2022-09-01",
+ "name": "[format('function-{0}-websearcher-plugin', variables('uniqueName'))]",
+ "location": "[parameters('location')]",
+ "kind": "functionapp",
+ "tags": {
+ "skweb": "1"
+ },
+ "properties": {
+ "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format('asp-{0}-webapi', variables('uniqueName')))]",
+ "httpsOnly": true,
+ "siteConfig": {
+ "alwaysOn": true
+ }
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/serverfarms', format('asp-{0}-webapi', variables('uniqueName')))]"
+ ]
+ },
+ {
+ "condition": "[parameters('deployWebSearcherPlugin')]",
+ "type": "Microsoft.Web/sites/config",
+ "apiVersion": "2022-09-01",
+ "name": "[format('{0}/{1}', format('function-{0}-websearcher-plugin', variables('uniqueName')), 'web')]",
+ "properties": {
+ "minTlsVersion": "1.2",
+ "appSettings": [
+ {
+ "name": "FUNCTIONS_EXTENSION_VERSION",
+ "value": "~4"
+ },
+ {
+ "name": "FUNCTIONS_WORKER_RUNTIME",
+ "value": "dotnet-isolated"
+ },
+ {
+ "name": "AzureWebJobsStorage",
+ "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1}', format('st{0}', variables('rgIdHash')), listKeys(resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash'))), '2022-09-01').keys[1].value)]"
+ },
+ {
+ "name": "APPINSIGHTS_INSTRUMENTATIONKEY",
+ "value": "[reference(resourceId('Microsoft.Insights/components', format('appins-{0}', variables('uniqueName'))), '2020-02-02').InstrumentationKey]"
+ },
+ {
+ "name": "PluginConfig:BingApiKey",
+ "value": "[if(parameters('deployWebSearcherPlugin'), listKeys(resourceId('Microsoft.Bing/accounts', format('bing-search-{0}', variables('uniqueName'))), '2020-06-10').key1, '')]"
+ }
+ ]
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Insights/components', format('appins-{0}', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Bing/accounts', format('bing-search-{0}', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Web/sites', format('function-{0}-websearcher-plugin', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Storage/storageAccounts', format('st{0}', variables('rgIdHash')))]"
+ ]
+ },
+ {
+ "condition": "[and(parameters('deployPackages'), parameters('deployWebSearcherPlugin'))]",
+ "type": "Microsoft.Web/sites/extensions",
+ "apiVersion": "2022-09-01",
+ "name": "[format('{0}/{1}', format('function-{0}-websearcher-plugin', variables('uniqueName')), 'MSDeploy')]",
+ "kind": "string",
+ "properties": {
+ "packageUri": "[parameters('webSearcherPackageUri')]"
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/sites', format('function-{0}-websearcher-plugin', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Web/sites/config', format('function-{0}-websearcher-plugin', 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.Web/sites/siteextensions",
+ "apiVersion": "2022-09-01",
+ "name": "[format('{0}/{1}', format('app-{0}-memorypipeline', variables('uniqueName')), 'Microsoft.ApplicationInsights.AzureWebSites')]",
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/sites', format('app-{0}-memorypipeline', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Web/sites/extensions', format('app-{0}-memorypipeline', variables('uniqueName')), 'MSDeploy')]"
+ ]
+ },
+ {
+ "condition": "[parameters('deployWebSearcherPlugin')]",
+ "type": "Microsoft.Web/sites/siteextensions",
+ "apiVersion": "2022-09-01",
+ "name": "[format('{0}/{1}', format('function-{0}-websearcher-plugin', variables('uniqueName')), 'Microsoft.ApplicationInsights.AzureWebSites')]",
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/sites/extensions', format('function-{0}-websearcher-plugin', variables('uniqueName')), 'MSDeploy')]",
+ "[resourceId('Microsoft.Web/sites', format('function-{0}-websearcher-plugin', variables('uniqueName')))]"
+ ]
+ },
+ {
+ "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
+ }
+ }
+ },
+ {
+ "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": "[equals(parameters('memoryStore'), 'Qdrant')]",
+ "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": "[equals(parameters('memoryStore'), 'Qdrant')]",
+ "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": "[if(equals(parameters('memoryStore'), 'Qdrant'), reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[1].id, null())]",
+ "siteConfig": {
+ "numberOfWorkers": 1,
+ "linuxFxVersion": "DOCKER|qdrant/qdrant:latest",
+ "alwaysOn": true,
+ "vnetRouteAllEnabled": true,
+ "ipSecurityRestrictions": [
+ {
+ "vnetSubnetResourceId": "[if(equals(parameters('memoryStore'), 'Qdrant'), reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[0].id, null())]",
+ "action": "Allow",
+ "priority": 300,
+ "name": "Allow front vnet"
+ },
+ {
+ "ipAddress": "Any",
+ "action": "Deny",
+ "priority": 2147483647,
+ "name": "Deny all"
+ }
+ ],
+ "azureStorageAccounts": {
+ "aciqdrantshare": {
+ "type": "AzureFiles",
+ "accountName": "[if(equals(parameters('memoryStore'), 'Qdrant'), format('st{0}', variables('rgIdHash')), 'notdeployed')]",
+ "shareName": "[variables('storageFileShareName')]",
+ "mountPath": "/qdrant/storage",
+ "accessKey": "[if(equals(parameters('memoryStore'), 'Qdrant'), 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')))]"
+ ]
+ },
+ {
+ "condition": "[equals(parameters('memoryStore'), 'AzureAISearch')]",
+ "type": "Microsoft.Search/searchServices",
+ "apiVersion": "2022-09-01",
+ "name": "[format('acs-{0}', variables('uniqueName'))]",
+ "location": "[parameters('location')]",
+ "sku": {
+ "name": "basic"
+ },
+ "properties": {
+ "replicaCount": 1,
+ "partitionCount": 1
+ }
+ },
+ {
+ "condition": "[equals(parameters('memoryStore'), 'Qdrant')]",
+ "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')))]"
+ ]
+ },
+ {
+ "condition": "[equals(parameters('memoryStore'), 'Qdrant')]",
+ "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"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "condition": "[equals(parameters('memoryStore'), 'Qdrant')]",
+ "type": "Microsoft.Network/networkSecurityGroups",
+ "apiVersion": "2022-11-01",
+ "name": "[format('nsg-{0}-qdrant', variables('uniqueName'))]",
+ "location": "[parameters('location')]",
+ "properties": {
+ "securityRules": []
+ }
+ },
+ {
+ "condition": "[equals(parameters('memoryStore'), 'Qdrant')]",
+ "type": "Microsoft.Web/sites/virtualNetworkConnections",
+ "apiVersion": "2022-09-01",
+ "name": "[format('{0}/{1}', format('app-{0}-webapi', variables('uniqueName')), 'webSubnetConnection')]",
+ "properties": {
+ "vnetResourceId": "[if(equals(parameters('memoryStore'), 'Qdrant'), reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[0].id, null())]",
+ "isSwift": true
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/sites', format('app-{0}-webapi', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName')))]"
+ ]
+ },
+ {
+ "condition": "[equals(parameters('memoryStore'), 'Qdrant')]",
+ "type": "Microsoft.Web/sites/virtualNetworkConnections",
+ "apiVersion": "2022-09-01",
+ "name": "[format('{0}/{1}', format('app-{0}-memorypipeline', variables('uniqueName')), 'memSubnetConnection')]",
+ "properties": {
+ "vnetResourceId": "[if(equals(parameters('memoryStore'), 'Qdrant'), reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[0].id, null())]",
+ "isSwift": true
+ },
+ "dependsOn": [
+ "[resourceId('Microsoft.Web/sites', format('app-{0}-memorypipeline', variables('uniqueName')))]",
+ "[resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName')))]"
+ ]
+ },
+ {
+ "condition": "[equals(parameters('memoryStore'), 'Qdrant')]",
+ "type": "Microsoft.Web/sites/virtualNetworkConnections",
+ "apiVersion": "2022-09-01",
+ "name": "[format('{0}/{1}', format('app-{0}-qdrant', variables('uniqueName')), 'qdrantSubnetConnection')]",
+ "properties": {
+ "vnetResourceId": "[if(equals(parameters('memoryStore'), 'Qdrant'), reference(resourceId('Microsoft.Network/virtualNetworks', format('vnet-{0}', variables('uniqueName'))), '2021-05-01').subnets[1].id, null())]",
+ "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": [
+ "/chatId"
+ ],
+ "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": [
+ "/userId"
+ ],
+ "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": [
+ "/chatId"
+ ],
+ "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-speech-{0}', variables('uniqueName'))]",
+ "location": "[parameters('location')]",
+ "sku": {
+ "name": "S0"
+ },
+ "kind": "SpeechServices",
+ "identity": {
+ "type": "None"
+ },
+ "properties": {
+ "customSubDomainName": "[format('cog-speech-{0}', variables('uniqueName'))]",
+ "networkAcls": {
+ "defaultAction": "Allow"
+ },
+ "publicNetworkAccess": "Enabled"
+ }
+ },
+ {
+ "type": "Microsoft.CognitiveServices/accounts",
+ "apiVersion": "2022-12-01",
+ "name": "[format('cog-ocr-{0}', variables('uniqueName'))]",
+ "location": "[parameters('location')]",
+ "sku": {
+ "name": "S0"
+ },
+ "kind": "FormRecognizer",
+ "identity": {
+ "type": "None"
+ },
+ "properties": {
+ "customSubDomainName": "[format('cog-ocr-{0}', variables('uniqueName'))]",
+ "networkAcls": {
+ "defaultAction": "Allow"
+ },
+ "publicNetworkAccess": "Enabled"
+ }
+ },
+ {
+ "condition": "[parameters('deployWebSearcherPlugin')]",
+ "type": "Microsoft.Bing/accounts",
+ "apiVersion": "2020-06-10",
+ "name": "[format('bing-search-{0}', variables('uniqueName'))]",
+ "location": "global",
+ "sku": {
+ "name": "S1"
+ },
+ "kind": "Bing.Search.v7"
+ }
+ ],
+ "outputs": {
+ "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'))]"
+ },
+ "memoryPipelineName": {
+ "type": "string",
+ "value": "[format('app-{0}-memorypipeline', variables('uniqueName'))]"
+ },
+ "pluginNames": {
+ "type": "array",
+ "value": "[concat(createArray(), if(parameters('deployWebSearcherPlugin'), createArray(format('function-{0}-websearcher-plugin', variables('uniqueName'))), createArray()))]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/deploy/package-webapi.ps1 b/scripts/deploy/package-memorypipeline.ps1
similarity index 58%
rename from deploy/package-webapi.ps1
rename to scripts/deploy/package-memorypipeline.ps1
index 3966b35c6..93dbf8f08 100644
--- a/deploy/package-webapi.ps1
+++ b/scripts/deploy/package-memorypipeline.ps1
@@ -1,6 +1,6 @@
-<#
+<#
.SYNOPSIS
-Package CopilotChat's WebAPI for deployment to Azure
+Package CopilotChat's MemoryPipeline for deployment to Azure
#>
param(
@@ -10,7 +10,7 @@ param(
[string]
# .NET framework to publish.
- $DotNetFramework = "net6.0",
+ $DotNetFramework = "net8.0",
[string]
# Target runtime to publish.
@@ -18,7 +18,15 @@ param(
[string]
# Output directory for published assets.
- $OutputDirectory = "$PSScriptRoot"
+ $OutputDirectory = "$PSScriptRoot",
+
+ [string]
+ # Version to give to assemblies and files.
+ $Version = "1.0.0",
+
+ [string]
+ # Additional information given in version info.
+ $InformationalVersion = ""
)
Write-Host "BuildConfiguration: $BuildConfiguration"
@@ -28,7 +36,7 @@ Write-Host "OutputDirectory: $OutputDirectory"
$publishOutputDirectory = "$OutputDirectory/publish"
$publishedZipDirectory = "$OutputDirectory/out"
-$publishedZipFilePath = "$publishedZipDirectory/webapi.zip"
+$publishedZipFilePath = "$publishedZipDirectory/memorypipeline.zip"
if (!(Test-Path $publishedZipDirectory)) {
New-Item -ItemType Directory -Force -Path $publishedZipDirectory | Out-Null
}
@@ -37,7 +45,7 @@ if (!(Test-Path $publishOutputDirectory)) {
}
Write-Host "Build configuration: $BuildConfiguration"
-dotnet publish "$PSScriptRoot/../webapi/CopilotChatWebApi.csproj" --configuration $BuildConfiguration --framework $DotNetFramework --runtime $TargetRuntime --self-contained --output "$publishOutputDirectory"
+dotnet publish "$PSScriptRoot/../../memorypipeline/CopilotChatMemoryPipeline.csproj" --configuration $BuildConfiguration --framework $DotNetFramework --runtime $TargetRuntime --self-contained --output "$publishOutputDirectory" /p:AssemblyVersion=$Version /p:FileVersion=$Version /p:InformationalVersion=$InformationalVersion
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
@@ -45,4 +53,4 @@ if ($LASTEXITCODE -ne 0) {
Write-Host "Compressing to $publishedZipFilePath"
Compress-Archive -Path $publishOutputDirectory\* -DestinationPath $publishedZipFilePath -Force
-Write-Host "Published webapi package to '$publishedZipFilePath'"
\ No newline at end of file
+Write-Host "Published memorypipeline package to '$publishedZipFilePath'"
\ No newline at end of file
diff --git a/deploy/package-webapi.sh b/scripts/deploy/package-memorypipeline.sh
similarity index 58%
rename from deploy/package-webapi.sh
rename to scripts/deploy/package-memorypipeline.sh
index 8f5da09eb..4dbcbe002 100644
--- a/deploy/package-webapi.sh
+++ b/scripts/deploy/package-memorypipeline.sh
@@ -1,6 +1,6 @@
-#!/bin/bash
+#!/bin/bash
-# Package CiopilotChat's WebAPI for deployment to Azure
+# Package CiopilotChat's MemoryPipeline for deployment to Azure
set -e
@@ -8,13 +8,15 @@ 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 "Usage: $0 [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 " -d, --dotnet DOTNET_FRAMEWORK_VERSION Target dotnet framework (default: net8.0)"
+ echo " -r, --runtime TARGET_RUNTIME Runtime identifier (default: win-x64)"
+ echo " -o, --output OUTPUT_DIRECTORY Output directory (default: $SCRIPT_ROOT)"
+ echo " -v --version VERSION Version to set files to (default: 1.0.0)"
+ echo " -i --info INFO Additional info to put in version details"
echo " -nz, --no-zip Do not zip package (default: false)"
}
@@ -22,31 +24,41 @@ usage() {
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
- -c|--configuration)
+ -c | --configuration)
CONFIGURATION="$2"
shift
shift
;;
- -d|--dotnet)
+ -d | --dotnet)
DOTNET="$2"
shift
shift
;;
- -r|--runtime)
+ -r | --runtime)
RUNTIME="$2"
shift
shift
;;
- -o|--output)
+ -o | --output)
OUTPUT_DIRECTORY="$2"
shift
shift
;;
- -nz|--no-zip)
+ -v | --version)
+ VERSION="$2"
+ shift
+ shift
+ ;;
+ -i | --info)
+ INFO="$2"
+ shift
+ shift
+ ;;
+ -nz | --no-zip)
NO_ZIP=true
shift
;;
- *)
+ *)
echo "Unknown option $1"
usage
exit 1
@@ -56,13 +68,15 @@ done
# Set defaults
: "${CONFIGURATION:="Release"}"
-: "${DOTNET:="net6.0"}"
+: "${DOTNET:="net8.0"}"
: "${RUNTIME:="win-x64"}"
+: "${VERSION:="1.0.0"}"
+: "${INFO:=""}"
: "${OUTPUT_DIRECTORY:="$SCRIPT_ROOT"}"
PUBLISH_OUTPUT_DIRECTORY="$OUTPUT_DIRECTORY/publish"
PUBLISH_ZIP_DIRECTORY="$OUTPUT_DIRECTORY/out"
-PACKAGE_FILE_PATH="$PUBLISH_ZIP_DIRECTORY/webapi.zip"
+PACKAGE_FILE_PATH="$PUBLISH_ZIP_DIRECTORY/memorypipeline.zip"
if [[ ! -d "$PUBLISH_OUTPUT_DIRECTORY" ]]; then
mkdir -p "$PUBLISH_OUTPUT_DIRECTORY"
@@ -72,7 +86,16 @@ if [[ ! -d "$PUBLISH_ZIP_DIRECTORY" ]]; then
fi
echo "Build configuration: $CONFIGURATION"
-dotnet publish "$SCRIPT_ROOT/../webapi/CopilotChatWebApi.csproj" --configuration $CONFIGURATION --framework $DOTNET --runtime $RUNTIME --self-contained --output "$PUBLISH_OUTPUT_DIRECTORY"
+dotnet publish "$SCRIPT_ROOT/../../memorypipeline/CopilotChatMemoryPipeline.csproj" \
+ --configuration $CONFIGURATION \
+ --framework $DOTNET \
+ --runtime $RUNTIME \
+ --self-contained \
+ --output "$PUBLISH_OUTPUT_DIRECTORY" \
+ -p:AssemblyVersion=$VERSION \
+ -p:FileVersion=$VERSION \
+ -p:InformationalVersion=$INFO
+
if [ $? -ne 0 ]; then
exit 1
fi
@@ -84,5 +107,3 @@ if [[ -z "$NO_ZIP" ]]; then
zip -r $PACKAGE_FILE_PATH .
popd
fi
-
-
diff --git a/scripts/deploy/package-plugins.ps1 b/scripts/deploy/package-plugins.ps1
new file mode 100644
index 000000000..12a65bfb4
--- /dev/null
+++ b/scripts/deploy/package-plugins.ps1
@@ -0,0 +1,65 @@
+<#
+.SYNOPSIS
+Package CopilotChat's plugins for deployment to Azure
+#>
+
+param(
+ [string]
+ # Build configuration to publish.
+ $BuildConfiguration = "Release",
+
+ [string]
+ # .NET framework to publish.
+ $DotNetFramework = "net8.0",
+
+ [string]
+ # Output directory for published assets.
+ $OutputDirectory = "$PSScriptRoot",
+
+ [string]
+ # Version to give to assemblies and files.
+ $Version = "1.0.0",
+
+ [string]
+ # Additional information given in version info.
+ $InformationalVersion = ""
+)
+
+Write-Host "BuildConfiguration: $BuildConfiguration"
+Write-Host "DotNetFramework: $DotNetFramework"
+Write-Host "TargetRuntime: $TargetRuntime"
+Write-Host "OutputDirectory: $OutputDirectory"
+
+$publishOutputDirectory = "$OutputDirectory/publish"
+$publishedZipDirectory = "$OutputDirectory/out/plugins"
+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
+}
+
+$pluginProjectFiles = Get-ChildItem -Path "$PSScriptRoot/../../plugins" -Recurse -Filter "*.csproj" -Exclude "PluginShared.csproj"
+foreach ($pluginProjectFile in $pluginProjectFiles) {
+ $pluginName = $pluginProjectFile.Name.Replace(".csproj", "").ToLowerInvariant()
+ $publishedZipFilePath = "$publishedZipDirectory/$pluginName.zip"
+
+ Write-Host "Packaging $pluginName from $pluginProjectFile"
+
+ dotnet publish $pluginProjectFile `
+ --configuration $BuildConfiguration `
+ --framework $DotNetFramework `
+ --output "$publishOutputDirectory" `
+ /p:AssemblyVersion=$Version `
+ /p:FileVersion=$Version `
+ /p:InformationalVersion=$InformationalVersion
+
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+
+ Write-Host "Compressing to $publishedZipFilePath"
+ Compress-Archive -Path $publishOutputDirectory\* -DestinationPath $publishedZipFilePath -Force
+
+ Write-Host "Published $pluginName package to '$publishedZipFilePath'"
+}
\ No newline at end of file
diff --git a/scripts/deploy/package-plugins.sh b/scripts/deploy/package-plugins.sh
new file mode 100644
index 000000000..8ede9a283
--- /dev/null
+++ b/scripts/deploy/package-plugins.sh
@@ -0,0 +1,114 @@
+#!/usr/bin/env bash
+
+# Package CopilotChat's plugins for deployment to Azure
+
+set -e
+
+SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+OUTPUT_DIRECTORY="$SCRIPT_ROOT"
+
+usage() {
+ echo "Usage: $0 [OPTIONS]"
+ echo ""
+ echo "Arguments:"
+ echo " -c, --configuration CONFIGURATION Build configuration (default: Release)"
+ echo " -d, --dotnet DOTNET_FRAMEWORK_VERSION Target dotnet framework (default: net8.0)"
+ echo " -o, --output OUTPUT_DIRECTORY Output directory (default: $SCRIPT_ROOT)"
+ echo " -v --version VERSION Version to set files to (default: 1.0.0)"
+ echo " -i --info INFO Additional info to put in version details"
+ 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
+ ;;
+ -o | --output)
+ OUTPUT_DIRECTORY="$2"
+ shift
+ shift
+ ;;
+ -v | --version)
+ VERSION="$2"
+ shift
+ shift
+ ;;
+ -i | --info)
+ INFO="$2"
+ shift
+ shift
+ ;;
+ -nz | --no-zip)
+ NO_ZIP=true
+ shift
+ ;;
+ *)
+ echo "Unknown option $1"
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+# Set defaults
+: "${CONFIGURATION:="Release"}"
+: "${DOTNET:="net8.0"}"
+: "${VERSION:="1.0.0"}"
+: "${INFO:=""}"
+: "${OUTPUT_DIRECTORY:="$SCRIPT_ROOT"}"
+
+PUBLISH_OUTPUT_DIRECTORY="$OUTPUT_DIRECTORY/publish"
+PUBLISH_ZIP_DIRECTORY="$OUTPUT_DIRECTORY/out/plugins"
+
+if [[ ! -d "$PUBLISH_OUTPUT_DIRECTORY" ]]; then
+ mkdir -p "$PUBLISH_OUTPUT_DIRECTORY"
+fi
+if [[ ! -d "$PUBLISH_ZIP_DIRECTORY" ]]; then
+ mkdir -p "$PUBLISH_ZIP_DIRECTORY"
+fi
+
+PLUGIN_PROJECT_FILES=()
+mapfile -d $'\0' PLUGIN_PROJECT_FILES < <(find "${SCRIPT_ROOT}/../../plugins" -name "*.csproj")
+
+for PLUGIN_PROJECT_FILE in $PLUGIN_PROJECT_FILES; do
+ PLUGIN_PROJECT_FILE=$(realpath "$PLUGIN_PROJECT_FILE")
+ PLUGIN_NAME=$(basename "$PLUGIN_PROJECT_FILE" .csproj)
+ PLUGIN_NAME="${PLUGIN_NAME,,}" # Lowercase
+
+ if [[ "$PLUGIN_NAME" = "pluginshared" ]]; then
+ continue
+ fi
+
+ echo "Packaging $PLUGIN_NAME from $PLUGIN_PROJECT_FILE"
+
+ dotnet publish "$PLUGIN_PROJECT_FILE" \
+ --configuration $CONFIGURATION \
+ --framework $DOTNET \
+ --output "$PUBLISH_OUTPUT_DIRECTORY" \
+ -p:AssemblyVersion=$VERSION \
+ -p:FileVersion=$VERSION \
+ -p:InformationalVersion=$INFO
+
+ if [ $? -ne 0 ]; then
+ exit 1
+ fi
+
+ # if not NO_ZIP then zip the package
+ PACKAGE_FILE_PATH="$PUBLISH_ZIP_DIRECTORY/$PLUGIN_NAME.zip"
+ if [[ -z "$NO_ZIP" ]]; then
+ pushd "$PUBLISH_OUTPUT_DIRECTORY"
+ echo "Compressing to $PACKAGE_FILE_PATH"
+ zip -r $PACKAGE_FILE_PATH .
+ popd
+ fi
+done
diff --git a/scripts/deploy/package-webapi.ps1 b/scripts/deploy/package-webapi.ps1
new file mode 100755
index 000000000..94d325b56
--- /dev/null
+++ b/scripts/deploy/package-webapi.ps1
@@ -0,0 +1,96 @@
+<#
+.SYNOPSIS
+Package Chat Copilot application for deployment to Azure
+#>
+
+param(
+ [string]
+ # Build configuration to publish.
+ $BuildConfiguration = "Release",
+
+ [string]
+ # .NET framework to publish.
+ $DotNetFramework = "net8.0",
+
+ [string]
+ # Target runtime to publish.
+ $TargetRuntime = "win-x64",
+
+ [string]
+ # Output directory for published assets.
+ $OutputDirectory = "$PSScriptRoot",
+
+ [string]
+ # Version to give to assemblies and files.
+ $Version = "0.0.0",
+
+ [string]
+ # Additional information given in version info.
+ $InformationalVersion = "",
+
+ [bool]
+ # Whether to skip building frontend files (false by default)
+ $SkipFrontendFiles = $false
+)
+
+Write-Host "Building backend executables..."
+
+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) {
+ rm $publishOutputDirectory/* -r -force
+}
+
+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" /p:AssemblyVersion=$Version /p:FileVersion=$Version /p:InformationalVersion=$InformationalVersion
+if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+}
+
+if (-Not $SkipFrontendFiles) {
+ Write-Host "Building static frontend files..."
+
+ Push-Location -Path "$PSScriptRoot/../../webapp"
+
+ $filePath = "./.env.production"
+ if (Test-path $filePath -PathType leaf) {
+ Remove-Item $filePath
+ }
+
+ Add-Content -Path $filePath -Value "REACT_APP_BACKEND_URI="
+ Add-Content -Path $filePath -Value "REACT_APP_SK_VERSION=$Version"
+ Add-Content -Path $filePath -Value "REACT_APP_SK_BUILD_INFO=$InformationalVersion"
+
+ Write-Host "Installing yarn dependencies..."
+ yarn install
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+
+ Write-Host "Building webapp..."
+ yarn build
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+
+ Pop-Location
+
+ Write-Host "Copying frontend files to package"
+ Copy-Item -Path "$PSScriptRoot/../../webapp/build/*" -Destination "$publishOutputDirectory\wwwroot" -Recurse -Force
+}
+
+Write-Host "Compressing package to $publishedZipFilePath"
+Compress-Archive -Path $publishOutputDirectory\* -DestinationPath $publishedZipFilePath -Force
+
+Write-Host "Published package to '$publishedZipFilePath'"
\ No newline at end of file
diff --git a/scripts/deploy/package-webapi.sh b/scripts/deploy/package-webapi.sh
new file mode 100644
index 000000000..eb0429680
--- /dev/null
+++ b/scripts/deploy/package-webapi.sh
@@ -0,0 +1,150 @@
+#!/usr/bin/env bash
+
+# Package Chat Copilot application for deployment to Azure
+
+set -e
+
+SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+OUTPUT_DIRECTORY="$SCRIPT_ROOT"
+
+usage() {
+ echo "Usage: $0 [OPTIONS]"
+ echo ""
+ echo "Arguments:"
+ echo " -c, --configuration CONFIGURATION Build configuration (default: Release)"
+ echo " -d, --dotnet DOTNET_FRAMEWORK_VERSION Target dotnet framework (default: net8.0)"
+ echo " -r, --runtime TARGET_RUNTIME Runtime identifier (default: win-x64)"
+ echo " -o, --output OUTPUT_DIRECTORY Output directory (default: $SCRIPT_ROOT)"
+ echo " -v --version VERSION Version to set files to (default: 1.0.0)"
+ echo " -i --info INFO Additional info to put in version details"
+ echo " -nz, --no-zip Do not zip package (default: false)"
+ echo " -s, --skip-frontend Do not build frontend files"
+}
+
+# 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
+ ;;
+ -v | --version)
+ VERSION="$2"
+ shift
+ shift
+ ;;
+ -i | --info)
+ INFO="$2"
+ shift
+ shift
+ ;;
+ -nz | --no-zip)
+ NO_ZIP=true
+ shift
+ ;;
+ -s|--skip-frontend)
+ SKIP_FRONTEND=true
+ shift
+ ;;
+ *)
+ echo "Unknown option $1"
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+echo "Building backend executables..."
+
+# Set defaults
+: "${CONFIGURATION:="Release"}"
+: "${DOTNET:="net8.0"}"
+: "${RUNTIME:="win-x64"}"
+: "${VERSION:="0.0.0"}"
+: "${INFO:=""}"
+: "${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" \
+ -p:AssemblyVersion=$VERSION \
+ -p:FileVersion=$VERSION \
+ -p:InformationalVersion=$INFO
+
+if [ $? -ne 0 ]; then
+ exit 1
+fi
+
+if [[ -z "$SKIP_FRONTEND" ]]; then
+ echo "Building static frontend files..."
+
+ pushd "$SCRIPT_ROOT/../../webapp"
+
+ filePath="./.env.production"
+ if [ -f "$filePath" ]; then
+ rm "$filePath"
+ fi
+
+ echo "REACT_APP_BACKEND_URI=" >> "$filePath"
+ echo "REACT_APP_SK_VERSION=$Version" >> "$filePath"
+ echo "REACT_APP_SK_BUILD_INFO=$InformationalVersion" >> "$filePath"
+
+ echo "Installing yarn dependencies..."
+ yarn install
+ if [ $? -ne 0 ]; then
+ echo "Failed to install yarn dependencies"
+ exit 1
+ fi
+
+ echo "Building webapp..."
+ yarn build
+ if [ $? -ne 0 ]; then
+ echo "Failed to build webapp"
+ exit 1
+ fi
+
+ popd
+
+ echo "Copying frontend files to package"
+ cp -R "$SCRIPT_ROOT/../../webapp/build/." "$PUBLISH_OUTPUT_DIRECTORY/wwwroot"
+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
diff --git a/scripts/Install-Requirements-UbuntuDebian.sh b/scripts/install-apt.sh
old mode 100644
new mode 100755
similarity index 83%
rename from scripts/Install-Requirements-UbuntuDebian.sh
rename to scripts/install-apt.sh
index 5a012d029..5e66c27fd
--- a/scripts/Install-Requirements-UbuntuDebian.sh
+++ b/scripts/install-apt.sh
@@ -1,6 +1,6 @@
-#!/bin/bash
+#!/usr/bin/env bash
-# Installs the requirements for running Copilot Chat.
+# Installs the requirements for running Chat Copilot.
set -e
@@ -19,10 +19,10 @@ 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 dotnet-sdk-8.0 -y;
sudo apt install nodejs -y;
echo ""
echo "YARN $(yarn --version) installed."
echo "NODEJS $(node --version) installed."
-echo "DOTNET $(dotnet --version) installed."
\ No newline at end of file
+echo "DOTNET $(dotnet --version) installed."
diff --git a/scripts/install-brew.sh b/scripts/install-brew.sh
new file mode 100755
index 000000000..4b4be44e9
--- /dev/null
+++ b/scripts/install-brew.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+# Installs the requirements for running Chat Copilot.
+
+set -e
+
+# Install the requirements
+brew install yarn;
+brew install --cask dotnet-sdk;
+brew install nodejs;
+
+echo ""
+echo "YARN $(yarn --version) installed."
+echo "NODEJS $(node --version) installed."
+echo "DOTNET $(dotnet --version) installed."
diff --git a/scripts/start-backend.sh b/scripts/start-backend.sh
new file mode 100755
index 000000000..c5bc2b820
--- /dev/null
+++ b/scripts/start-backend.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+
+# Builds and runs the Chat Copilot backend.
+
+set -e
+
+# Stop any existing backend API process
+while pid=$(pgrep CopilotChatWebA); do
+ echo $pid | sed 's/\([0-9]\{4\}\) .*/\1/' | xargs kill
+done
+
+# Get defaults and constants
+SCRIPT_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+. $SCRIPT_DIRECTORY/.env
+cd "$SCRIPT_DIRECTORY/../webapi"
+
+# Environment variable `ASPNETCORE_ENVIRONMENT` required to override appsettings.json with
+# appsettings.$ENV_ASPNETCORE.json. See `webapi/ConfigurationExtensions.cs`
+export ASPNETCORE_ENVIRONMENT=$ENV_ASPNETCORE
+
+# Build and run the backend API server
+dotnet build && dotnet run
diff --git a/scripts/start-frontend.sh b/scripts/start-frontend.sh
new file mode 100755
index 000000000..0421a979a
--- /dev/null
+++ b/scripts/start-frontend.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+# Builds and runs the Chat Copilot frontend.
+
+set -e
+
+SCRIPT_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIRECTORY/../webapp"
+
+# Build and run the frontend application
+yarn install && yarn start
diff --git a/scripts/start.sh b/scripts/start.sh
new file mode 100755
index 000000000..9496ca11f
--- /dev/null
+++ b/scripts/start.sh
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+
+# Initializes and runs both the backend and frontend for Copilot Chat.
+
+set -e
+
+ScriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$ScriptDir"
+
+# get the port from the REACT_APP_BACKEND_URI env variable
+envContent=$(grep -v '^#' ../webapp/.env | xargs)
+backendPort=$(echo $envContent | sed -n 's/.*:\([0-9]*\).*/\1/p')
+
+# Start backend (in background)
+./start-backend.sh &
+
+maxRetries=5
+retryCount=0
+retryWait=5 # set the number of seconds to wait before retrying
+
+# while the backend is not running wait.
+while [ $retryCount -lt $maxRetries ]
+do
+ if nc -z localhost $backendPort
+ then
+ # if the backend is running, start the frontend and break out of the loop
+ ./start-frontend.sh
+ break
+ else
+ # if the backend is not running, sleep, then increment the retry count
+ sleep $retryWait
+ retryCount=$((retryCount+1))
+ fi
+done
+
+# if we have exceeded the number of retries
+if [ $retryCount -eq $maxRetries ]
+then
+# write to the console that the backend is not running and we have exceeded the number of retries and we are exiting
+ echo "*************************************************"
+ echo "Backend is not running and we have exceeded "
+ echo "the maximum number of retries."
+ echo ""
+ echo "Therefore, we are exiting."
+ echo "*************************************************"
+fi
diff --git a/shared/ConfigurationBuilderExtensions.cs b/shared/ConfigurationBuilderExtensions.cs
new file mode 100644
index 000000000..1bd786495
--- /dev/null
+++ b/shared/ConfigurationBuilderExtensions.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Reflection;
+using Microsoft.Extensions.Configuration;
+
+namespace CopilotChat.Shared;
+
+internal static class ConfigurationBuilderExtensions
+{
+ // ASP.NET env var
+ private const string AspnetEnvVar = "ASPNETCORE_ENVIRONMENT";
+
+ public static void AddKMConfigurationSources(
+ this IConfigurationBuilder builder,
+ bool useAppSettingsFiles = true,
+ bool useEnvVars = true,
+ bool useSecretManager = true,
+ string? settingsDirectory = null)
+ {
+ // Load env var name, either Development or Production
+ var env = Environment.GetEnvironmentVariable(AspnetEnvVar) ?? string.Empty;
+
+ // Detect the folder containing configuration files
+ settingsDirectory ??= Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
+ ?? Directory.GetCurrentDirectory();
+ builder.SetBasePath(settingsDirectory);
+
+ // Add configuration files as sources
+ if (useAppSettingsFiles)
+ {
+ // Add appsettings.json, typically used for default settings, without credentials
+ var main = Path.Join(settingsDirectory, "appsettings.json");
+ if (!File.Exists(main))
+ {
+ throw new FileNotFoundException($"appsettings.json not found. Directory: {settingsDirectory}");
+ }
+
+ builder.AddJsonFile(main, optional: false);
+
+ // Add appsettings.development.json, used for local overrides and credentials
+ if (env.Equals("development", StringComparison.OrdinalIgnoreCase))
+ {
+ var f1 = Path.Join(settingsDirectory, "appsettings.development.json");
+ var f2 = Path.Join(settingsDirectory, "appsettings.Development.json");
+ if (File.Exists(f1))
+ {
+ builder.AddJsonFile(f1, optional: false);
+ }
+ else if (File.Exists(f2))
+ {
+ builder.AddJsonFile(f2, optional: false);
+ }
+ }
+
+ // Add appsettings.production.json, used for production settings and credentials
+ if (env.Equals("production", StringComparison.OrdinalIgnoreCase))
+ {
+ var f1 = Path.Join(settingsDirectory, "appsettings.production.json");
+ var f2 = Path.Join(settingsDirectory, "appsettings.Production.json");
+ if (File.Exists(f1))
+ {
+ builder.AddJsonFile(f1, optional: false);
+ }
+ else if (File.Exists(f2))
+ {
+ builder.AddJsonFile(f2, optional: false);
+ }
+ }
+ }
+
+ // Add Secret Manager as source
+ if (useSecretManager)
+ {
+ // GetEntryAssembly method can return null if the library is loaded
+ // from an unmanaged application, in which case UserSecrets are not supported.
+ var entryAssembly = Assembly.GetEntryAssembly();
+
+ // Support for user secrets. Secret Manager doesn't encrypt the stored secrets and
+ // shouldn't be treated as a trusted store. It's for development purposes only.
+ // see: https://learn.microsoft.com/aspnet/core/security/app-secrets?#secret-manager
+ if (entryAssembly != null && env.Equals("development", StringComparison.OrdinalIgnoreCase))
+ {
+ builder.AddUserSecrets(entryAssembly, optional: true);
+ }
+ }
+
+ // Add environment variables as source.
+ // Environment variables can override all the settings provided by the previous sources.
+ if (useEnvVars)
+ {
+ // Support for environment variables overriding the config files
+ builder.AddEnvironmentVariables();
+ }
+ }
+}
diff --git a/shared/CopilotChatShared.csproj b/shared/CopilotChatShared.csproj
new file mode 100644
index 000000000..a89a74758
--- /dev/null
+++ b/shared/CopilotChatShared.csproj
@@ -0,0 +1,24 @@
+
+
+
+ CopilotChat.Shared
+ net8.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shared/KernelMemoryBuilderExtensions.cs b/shared/KernelMemoryBuilderExtensions.cs
new file mode 100644
index 000000000..1b82ff732
--- /dev/null
+++ b/shared/KernelMemoryBuilderExtensions.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.KernelMemory;
+
+namespace CopilotChat.Shared;
+
+///
+/// Kernel Memory builder extensions for apps using settings in appsettings.json
+/// and using IConfiguration.
+///
+public static class KernelMemoryBuilderExtensions
+{
+ ///
+ /// Configure the builder using settings stored in the specified directory.
+ /// If directory is empty, use the current assembly folder
+ ///
+ /// KernelMemory builder instance
+ /// Directory containing appsettings.json (incl. dev/prod)
+ public static IKernelMemoryBuilder FromAppSettings(this IKernelMemoryBuilder builder, string? settingsDirectory = null)
+ {
+ return new ServiceConfiguration(settingsDirectory).PrepareBuilder(builder);
+ }
+
+ ///
+ /// Configure the builder using settings from the given KernelMemoryConfig and IConfiguration instances.
+ ///
+ /// KernelMemory builder instance
+ /// KM configuration
+ /// Dependencies configuration, e.g. queue, embedding, storage, etc.
+ public static IKernelMemoryBuilder FromMemoryConfiguration(
+ this IKernelMemoryBuilder builder,
+ KernelMemoryConfig memoryConfiguration,
+ IConfiguration servicesConfiguration)
+ {
+ return new ServiceConfiguration(servicesConfiguration, memoryConfiguration).PrepareBuilder(builder);
+ }
+}
diff --git a/shared/MemoryConfiguration.cs b/shared/MemoryConfiguration.cs
new file mode 100644
index 000000000..285d12fce
--- /dev/null
+++ b/shared/MemoryConfiguration.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace CopilotChat.Shared;
+
+///
+/// Configuration constants for kernel memory.
+///
+public static class MemoryConfiguration
+{
+ public const string KernelMemorySection = "KernelMemory";
+ public const string ServicesSection = "Services";
+ public const string OrchestrationTypeDistributed = "Distributed";
+ public const string NoneType = "None";
+}
diff --git a/webapi/CopilotChat/Options/TesseractOptions.cs b/shared/Ocr/Tesseract/TesseractConfig.cs
similarity index 66%
rename from webapi/CopilotChat/Options/TesseractOptions.cs
rename to shared/Ocr/Tesseract/TesseractConfig.cs
index 0fe50f104..361d1fb86 100644
--- a/webapi/CopilotChat/Options/TesseractOptions.cs
+++ b/shared/Ocr/Tesseract/TesseractConfig.cs
@@ -1,26 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel.DataAnnotations;
-using SemanticKernel.Service.Options;
-namespace SemanticKernel.Service.CopilotChat.Options;
+namespace CopilotChat.Shared.Ocr.Tesseract;
///
/// Configuration options for Tesseract OCR support.
///
-public sealed class TesseractOptions
+public sealed class TesseractConfig
{
- public const string PropertyName = "Tesseract";
-
///
/// The file path where the Tesseract language file is stored (e.g. "./data")
///
- [Required, NotEmptyOrWhitespace]
+ [Required]
public string? FilePath { get; set; } = string.Empty;
///
/// The language file prefix name (e.g. "eng")
///
- [Required, NotEmptyOrWhitespace]
+ [Required]
public string? Language { get; set; } = string.Empty;
}
diff --git a/shared/Ocr/Tesseract/TesseractOcrEngine.cs b/shared/Ocr/Tesseract/TesseractOcrEngine.cs
new file mode 100644
index 000000000..6b5d7c7ae
--- /dev/null
+++ b/shared/Ocr/Tesseract/TesseractOcrEngine.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.KernelMemory.DataFormats;
+using Tesseract;
+
+namespace CopilotChat.Shared.Ocr.Tesseract;
+
+///
+/// Wrapper for the TesseractEngine within the Tesseract OCR library.
+///
+public sealed class TesseractOcrEngine : IOcrEngine, IDisposable
+{
+ private readonly TesseractEngine _engine;
+
+ ///
+ /// Creates a new instance of the TesseractEngineWrapper passing in a valid TesseractEngine.
+ ///
+ public TesseractOcrEngine(TesseractConfig tesseractConfig)
+ {
+ if (!Directory.Exists(tesseractConfig.FilePath))
+ {
+ throw new DirectoryNotFoundException($"{tesseractConfig.FilePath} dir not found");
+ }
+
+ this._engine = new TesseractEngine(tesseractConfig.FilePath, tesseractConfig.Language);
+ }
+
+ ///
+ public async Task ExtractTextFromImageAsync(Stream imageContent, CancellationToken cancellationToken = default)
+ {
+ await using (var imgStream = new MemoryStream())
+ {
+ await imageContent.CopyToAsync(imgStream, cancellationToken);
+ imgStream.Position = 0;
+
+ using var img = Pix.LoadFromMemory(imgStream.ToArray());
+
+ using var page = this._engine.Process(img);
+ return page.GetText();
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ this._engine.Dispose();
+ }
+}
diff --git a/shared/ServiceConfiguration.cs b/shared/ServiceConfiguration.cs
new file mode 100644
index 000000000..313de466f
--- /dev/null
+++ b/shared/ServiceConfiguration.cs
@@ -0,0 +1,539 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using CopilotChat.Shared.Ocr.Tesseract;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.KernelMemory;
+using Microsoft.KernelMemory.AI;
+using Microsoft.KernelMemory.AI.Ollama;
+using Microsoft.KernelMemory.AI.OpenAI;
+using Microsoft.KernelMemory.DocumentStorage.DevTools;
+using Microsoft.KernelMemory.MemoryDb.SQLServer;
+using Microsoft.KernelMemory.MemoryStorage;
+using Microsoft.KernelMemory.MemoryStorage.DevTools;
+using Microsoft.KernelMemory.Pipeline.Queue.DevTools;
+
+namespace CopilotChat.Shared;
+
+internal sealed class ServiceConfiguration
+{
+ // Content of appsettings.json, used to access dynamic data under "Services"
+ private IConfiguration _rawAppSettings;
+
+ // Normalized configuration
+ private KernelMemoryConfig _memoryConfiguration;
+
+ // appsettings.json root node name
+ private const string ConfigRoot = "KernelMemory";
+
+ // OpenAI env var
+ private const string OpenAIEnvVar = "OPENAI_API_KEY";
+
+ public ServiceConfiguration(string? settingsDirectory = null)
+ : this(ReadAppSettings(settingsDirectory))
+ {
+ }
+
+ public ServiceConfiguration(IConfiguration rawAppSettings)
+ : this(rawAppSettings,
+ rawAppSettings.GetSection(ConfigRoot).Get()
+ ?? throw new ConfigurationException("Unable to load Kernel Memory settings from the given configuration. " +
+ $"There should be a '{ConfigRoot}' root node, " +
+ $"with data mapping to '{nameof(KernelMemoryConfig)}'"))
+ {
+ }
+
+ public ServiceConfiguration(
+ IConfiguration rawAppSettings,
+ KernelMemoryConfig memoryConfiguration)
+ {
+ this._rawAppSettings = rawAppSettings ?? throw new ConfigurationException("The given app settings configuration is NULL");
+ this._memoryConfiguration = memoryConfiguration ?? throw new ConfigurationException("The given memory configuration is NULL");
+
+ if (!this.MinimumConfigurationIsAvailable(false)) { this.SetupForOpenAI(); }
+
+ this.MinimumConfigurationIsAvailable(true);
+ }
+
+ public IKernelMemoryBuilder PrepareBuilder(IKernelMemoryBuilder builder)
+ {
+ return this.BuildUsingConfiguration(builder);
+ }
+
+ private IKernelMemoryBuilder BuildUsingConfiguration(IKernelMemoryBuilder builder)
+ {
+ if (this._memoryConfiguration == null)
+ {
+ throw new ConfigurationException("The given memory configuration is NULL");
+ }
+
+ if (this._rawAppSettings == null)
+ {
+ throw new ConfigurationException("The given app settings configuration is NULL");
+ }
+
+ // Required by ctors expecting KernelMemoryConfig via DI
+ builder.AddSingleton(this._memoryConfiguration);
+
+ this.ConfigureMimeTypeDetectionDependency(builder);
+
+ this.ConfigureTextPartitioning(builder);
+
+ this.ConfigureQueueDependency(builder);
+
+ this.ConfigureStorageDependency(builder);
+
+ // The ingestion embedding generators is a list of generators that the "gen_embeddings" handler uses,
+ // to generate embeddings for each partition. While it's possible to use multiple generators (e.g. to compare embedding quality)
+ // only one generator is used when searching by similarity, and the generator used for search is not in this list.
+ // - config.DataIngestion.EmbeddingGeneratorTypes => list of generators, embeddings to generate and store in memory DB
+ // - config.Retrieval.EmbeddingGeneratorType => one embedding generator, used to search, and usually injected into Memory DB constructor
+
+ this.ConfigureIngestionEmbeddingGenerators(builder);
+
+ this.ConfigureSearchClient(builder);
+
+ this.ConfigureRetrievalEmbeddingGenerator(builder);
+
+ // The ingestion Memory DBs is a list of DBs where handlers write records to. While it's possible
+ // to write to multiple DBs, e.g. for replication purpose, there is only one Memory DB used to
+ // read/search, and it doesn't come from this list. See "config.Retrieval.MemoryDbType".
+ // Note: use the aux service collection to avoid mixing ingestion and retrieval dependencies.
+
+ this.ConfigureIngestionMemoryDb(builder);
+
+ this.ConfigureRetrievalMemoryDb(builder);
+
+ this.ConfigureTextGenerator(builder);
+
+ this.ConfigureImageOCR(builder);
+
+ return builder;
+ }
+
+ private static IConfiguration ReadAppSettings(string? settingsDirectory)
+ {
+ var builder = new ConfigurationBuilder();
+ builder.AddKMConfigurationSources(settingsDirectory: settingsDirectory);
+ return builder.Build();
+ }
+
+ private void ConfigureQueueDependency(IKernelMemoryBuilder builder)
+ {
+ if (string.Equals(this._memoryConfiguration.DataIngestion.OrchestrationType, "Distributed", StringComparison.OrdinalIgnoreCase))
+ {
+ switch (this._memoryConfiguration.DataIngestion.DistributedOrchestration.QueueType)
+ {
+ case string x when x.Equals("AzureQueue", StringComparison.Ordinal):
+ throw new ConfigurationException("The configuration key 'AzureQueue' has been deprecated. Please use 'AzureQueues' instead.'");
+
+ case string x when x.Equals("RabbitMq", StringComparison.Ordinal):
+ throw new ConfigurationException("The configuration key 'RabbitMq' has been deprecated. Please use 'RabbitMQ' instead.'");
+
+ case string x when x.Equals("AzureQueues", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddAzureQueuesOrchestration(this.GetServiceConfig("AzureQueues"));
+ break;
+
+ case string y when y.Equals("RabbitMQ", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddRabbitMQOrchestration(this.GetServiceConfig("RabbitMQ"));
+ break;
+
+ case string y when y.Equals("SimpleQueues", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddSimpleQueues(this.GetServiceConfig("SimpleQueues"));
+ break;
+
+ default:
+ // NOOP - allow custom implementations, via WithCustomIngestionQueueClientFactory()
+ break;
+ }
+ }
+ }
+
+ private void ConfigureStorageDependency(IKernelMemoryBuilder builder)
+ {
+ switch (this._memoryConfiguration.DocumentStorageType)
+ {
+ case string x when x.Equals("AzureBlob", StringComparison.OrdinalIgnoreCase):
+ throw new ConfigurationException("The configuration key 'AzureBlob' has been deprecated. Please use 'AzureBlobs' instead.'");
+
+ case string x when x.Equals("AzureBlobs", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddAzureBlobsAsDocumentStorage(this.GetServiceConfig("AzureBlobs"));
+ break;
+
+ case string x when x.Equals("SimpleFileStorage", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddSimpleFileStorageAsDocumentStorage(this.GetServiceConfig("SimpleFileStorage"));
+ break;
+
+ default:
+ // NOOP - allow custom implementations, via WithCustomStorage()
+ break;
+ }
+ }
+
+ private void ConfigureTextPartitioning(IKernelMemoryBuilder builder)
+ {
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ if (this._memoryConfiguration.DataIngestion.TextPartitioning != null)
+ {
+ this._memoryConfiguration.DataIngestion.TextPartitioning.Validate();
+ builder.WithCustomTextPartitioningOptions(this._memoryConfiguration.DataIngestion.TextPartitioning);
+ }
+ }
+
+ private void ConfigureMimeTypeDetectionDependency(IKernelMemoryBuilder builder)
+ {
+ builder.WithDefaultMimeTypeDetection();
+ }
+
+ private void ConfigureIngestionEmbeddingGenerators(IKernelMemoryBuilder builder)
+ {
+ // Note: using multiple embeddings is not fully supported yet and could cause write errors or incorrect search results
+ if (this._memoryConfiguration.DataIngestion.EmbeddingGeneratorTypes.Count > 1)
+ {
+ throw new NotSupportedException("Using multiple embedding generators is currently unsupported. " +
+ "You may contact the team if this feature is required, or workaround this exception " +
+ "using KernelMemoryBuilder methods explicitly.");
+ }
+
+ foreach (var type in this._memoryConfiguration.DataIngestion.EmbeddingGeneratorTypes)
+ {
+ switch (type)
+ {
+ case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase):
+ case string y when y.Equals("AzureOpenAIEmbedding", StringComparison.OrdinalIgnoreCase):
+ {
+ var instance = this.GetServiceInstance(builder,
+ s => s.AddAzureOpenAIEmbeddingGeneration(
+ config: this.GetServiceConfig("AzureOpenAIEmbedding"),
+ textTokenizer: new GPT4oTokenizer()));
+ builder.AddIngestionEmbeddingGenerator(instance);
+ break;
+ }
+
+ case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase):
+ {
+ var instance = this.GetServiceInstance(builder,
+ s => s.AddOpenAITextEmbeddingGeneration(
+ config: this.GetServiceConfig("OpenAI"),
+ textTokenizer: new GPT4oTokenizer()));
+ builder.AddIngestionEmbeddingGenerator(instance);
+ break;
+ }
+
+ case string x when x.Equals("Ollama", StringComparison.OrdinalIgnoreCase):
+ {
+ var instance = this.GetServiceInstance(builder,
+ s => s.AddOllamaTextEmbeddingGeneration(
+ config: this.GetServiceConfig("Ollama"),
+ textTokenizer: new GPT4oTokenizer()));
+ builder.AddIngestionEmbeddingGenerator(instance);
+ break;
+ }
+
+ default:
+ // NOOP - allow custom implementations, via WithCustomEmbeddingGeneration()
+ break;
+ }
+ }
+ }
+
+ private void ConfigureIngestionMemoryDb(IKernelMemoryBuilder builder)
+ {
+ foreach (var type in this._memoryConfiguration.DataIngestion.MemoryDbTypes)
+ {
+ switch (type)
+ {
+ default:
+ throw new ConfigurationException(
+ $"Unknown Memory DB option '{type}'. " +
+ "To use a custom Memory DB, set the configuration value to an empty string, " +
+ "and inject the custom implementation using `IKernelMemoryBuilder.WithCustomMemoryDb(...)`");
+
+ case "":
+ // NOOP - allow custom implementations, via WithCustomMemoryDb()
+ break;
+
+ case string x when x.Equals("AzureAISearch", StringComparison.OrdinalIgnoreCase):
+ {
+ var instance = this.GetServiceInstance(builder,
+ s => s.AddAzureAISearchAsMemoryDb(this.GetServiceConfig("AzureAISearch"))
+ );
+ builder.AddIngestionMemoryDb(instance);
+ break;
+ }
+
+ case string x when x.Equals("Postgres", StringComparison.OrdinalIgnoreCase):
+ {
+ var instance = this.GetServiceInstance(builder,
+ s => s.AddPostgresAsMemoryDb(this.GetServiceConfig("Postgres"))
+ );
+ builder.AddIngestionMemoryDb(instance);
+ break;
+ }
+
+ case string x when x.Equals("Qdrant", StringComparison.OrdinalIgnoreCase):
+ {
+ var instance = this.GetServiceInstance(builder,
+ s => s.AddQdrantAsMemoryDb(this.GetServiceConfig("Qdrant"))
+ );
+ builder.AddIngestionMemoryDb(instance);
+ break;
+ }
+
+ case string x when x.Equals("SimpleVectorDb", StringComparison.OrdinalIgnoreCase):
+ {
+ var instance = this.GetServiceInstance(builder,
+ s => s.AddSimpleVectorDbAsMemoryDb(this.GetServiceConfig("SimpleVectorDb"))
+ );
+ builder.AddIngestionMemoryDb(instance);
+ break;
+ }
+
+ case string x when x.Equals("SqlServer", StringComparison.OrdinalIgnoreCase):
+ {
+ var instance = this.GetServiceInstance(builder,
+ s => s.AddSqlServerAsMemoryDb(this.GetServiceConfig("SqlServer"))
+ );
+ builder.AddIngestionMemoryDb(instance);
+ break;
+ }
+ }
+ }
+ }
+
+ private void ConfigureSearchClient(IKernelMemoryBuilder builder)
+ {
+ // Search settings
+ builder.WithSearchClientConfig(this._memoryConfiguration.Retrieval.SearchClient);
+ }
+
+ private void ConfigureRetrievalEmbeddingGenerator(IKernelMemoryBuilder builder)
+ {
+ // Retrieval embeddings - ITextEmbeddingGeneration interface
+ switch (this._memoryConfiguration.Retrieval.EmbeddingGeneratorType)
+ {
+ case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase):
+ case string y when y.Equals("AzureOpenAIEmbedding", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddAzureOpenAIEmbeddingGeneration(
+ config: this.GetServiceConfig("AzureOpenAIEmbedding"),
+ textTokenizer: new GPT4oTokenizer());
+ break;
+
+ case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddOpenAITextEmbeddingGeneration(
+ config: this.GetServiceConfig("OpenAI"),
+ textTokenizer: new GPT4oTokenizer());
+ break;
+
+ case string x when x.Equals("Ollama", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddOllamaTextEmbeddingGeneration(
+ config: this.GetServiceConfig("Ollama"),
+ textTokenizer: new GPT4oTokenizer());
+ break;
+
+ default:
+ // NOOP - allow custom implementations, via WithCustomEmbeddingGeneration()
+ break;
+ }
+ }
+
+ private void ConfigureRetrievalMemoryDb(IKernelMemoryBuilder builder)
+ {
+ // Retrieval Memory DB - IMemoryDb interface
+ switch (this._memoryConfiguration.Retrieval.MemoryDbType)
+ {
+ case string x when x.Equals("AzureAISearch", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddAzureAISearchAsMemoryDb(this.GetServiceConfig("AzureAISearch"));
+ break;
+
+ case string x when x.Equals("Postgres", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddPostgresAsMemoryDb(this.GetServiceConfig("Postgres"));
+ break;
+
+ case string x when x.Equals("Qdrant", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddQdrantAsMemoryDb(this.GetServiceConfig("Qdrant"));
+ break;
+
+ case string x when x.Equals("SimpleVectorDb", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddSimpleVectorDbAsMemoryDb(this.GetServiceConfig("SimpleVectorDb"));
+ break;
+
+ case string x when x.Equals("SqlServer", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddSqlServerAsMemoryDb(this.GetServiceConfig("SqlServer"));
+ break;
+
+ default:
+ // NOOP - allow custom implementations, via WithCustomMemoryDb()
+ break;
+ }
+ }
+
+ private void ConfigureTextGenerator(IKernelMemoryBuilder builder)
+ {
+ // Text generation
+ switch (this._memoryConfiguration.TextGeneratorType)
+ {
+ case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase):
+ case string y when y.Equals("AzureOpenAIText", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddAzureOpenAITextGeneration(
+ config: this.GetServiceConfig("AzureOpenAIText"),
+ textTokenizer: new GPT4oTokenizer());
+ break;
+
+ case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddOpenAITextGeneration(
+ config: this.GetServiceConfig("OpenAI"),
+ textTokenizer: new GPT4oTokenizer());
+ break;
+
+ case string x when x.Equals("Ollama", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddOllamaTextGeneration(
+ config: this.GetServiceConfig("Ollama"),
+ textTokenizer: new GPT4oTokenizer());
+ break;
+
+ default:
+ // NOOP - allow custom implementations, via WithCustomTextGeneration()
+ break;
+ }
+ }
+
+ private void ConfigureImageOCR(IKernelMemoryBuilder builder)
+ {
+ // Image OCR
+ switch (this._memoryConfiguration.DataIngestion.ImageOcrType)
+ {
+ case string y when string.IsNullOrWhiteSpace(y):
+ case string x when x.Equals("None", StringComparison.OrdinalIgnoreCase):
+ break;
+
+ case string x when x.Equals("AzureAIDocIntel", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddAzureAIDocIntel(this.GetServiceConfig("AzureAIDocIntel"));
+ break;
+
+ case string x when x.Equals("Tesseract", StringComparison.OrdinalIgnoreCase):
+ builder.Services.AddSingleton(this.GetServiceConfig("Tesseract"));
+ builder.WithCustomImageOcr();
+ break;
+
+ default:
+ // NOOP - allow custom implementations, via WithCustomImageOCR()
+ break;
+ }
+ }
+
+ ///
+ /// Check the configuration for minimum requirements
+ ///
+ /// Whether to throw or return false when the config is incomplete
+ /// Whether the configuration is valid
+ private bool MinimumConfigurationIsAvailable(bool throwOnError)
+ {
+ // Check if text generation settings
+ if (string.IsNullOrEmpty(this._memoryConfiguration.TextGeneratorType))
+ {
+ if (!throwOnError) { return false; }
+
+ throw new ConfigurationException("Text generation (TextGeneratorType) is not configured in Kernel Memory.");
+ }
+
+ // Check embedding generation ingestion settings
+ if (this._memoryConfiguration.DataIngestion.EmbeddingGenerationEnabled)
+ {
+ if (this._memoryConfiguration.DataIngestion.EmbeddingGeneratorTypes.Count == 0)
+ {
+ if (!throwOnError) { return false; }
+
+ throw new ConfigurationException("Data ingestion embedding generation (DataIngestion.EmbeddingGeneratorTypes) is not configured in Kernel Memory.");
+ }
+ }
+
+ // Check embedding generation retrieval settings
+ if (string.IsNullOrEmpty(this._memoryConfiguration.Retrieval.EmbeddingGeneratorType))
+ {
+ if (!throwOnError) { return false; }
+
+ throw new ConfigurationException("Retrieval embedding generation (Retrieval.EmbeddingGeneratorType) is not configured in Kernel Memory.");
+ }
+
+ return true;
+ }
+
+ ///
+ /// Rewrite configuration using OpenAI, if possible.
+ ///
+ private void SetupForOpenAI()
+ {
+ string openAIKey = Environment.GetEnvironmentVariable(OpenAIEnvVar)?.Trim() ?? string.Empty;
+ if (string.IsNullOrEmpty(openAIKey))
+ {
+ return;
+ }
+
+ var inMemoryConfig = new Dictionary
+ {
+ { $"{ConfigRoot}:Services:OpenAI:APIKey", openAIKey },
+ { $"{ConfigRoot}:TextGeneratorType", "OpenAI" },
+ { $"{ConfigRoot}:DataIngestion:EmbeddingGeneratorTypes:0", "OpenAI" },
+ { $"{ConfigRoot}:Retrieval:EmbeddingGeneratorType", "OpenAI" }
+ };
+
+ var newAppSettings = new ConfigurationBuilder();
+ newAppSettings.AddConfiguration(this._rawAppSettings);
+ newAppSettings.AddInMemoryCollection(inMemoryConfig);
+
+ this._rawAppSettings = newAppSettings.Build();
+ this._memoryConfiguration = this._rawAppSettings.GetSection(ConfigRoot).Get()!;
+ }
+
+ ///
+ /// Get an instance of T, using dependencies available in the builder,
+ /// except for existing service descriptors for T. Replace/Use the
+ /// given action to define T's implementation.
+ /// Return an instance of T built using the definition provided by
+ /// the action.
+ ///
+ /// KM builder
+ /// Action used to configure the service collection
+ /// Target type/interface
+ private T GetServiceInstance(IKernelMemoryBuilder builder, Action addCustomService)
+ {
+ // Clone the list of service descriptors, skipping T descriptor
+ IServiceCollection services = new ServiceCollection();
+ foreach (ServiceDescriptor d in builder.Services)
+ {
+ if (d.ServiceType == typeof(T)) { continue; }
+
+ services.Add(d);
+ }
+
+ // Add the custom T descriptor
+ addCustomService.Invoke(services);
+
+ // Build and return an instance of T, as defined by `addCustomService`
+ return services.BuildServiceProvider().GetService()
+ ?? throw new ConfigurationException($"Unable to build {nameof(T)}");
+ }
+
+ ///
+ /// Read a dependency configuration from IConfiguration
+ /// Data is usually retrieved from KernelMemory:Services:{serviceName}, e.g. when using appsettings.json
+ /// {
+ /// "KernelMemory": {
+ /// "Services": {
+ /// "{serviceName}": {
+ /// ...
+ /// ...
+ /// }
+ /// }
+ /// }
+ /// }
+ ///
+ /// Name of the dependency
+ /// Type of configuration to return
+ /// Configuration instance, settings for the dependency specified
+ private T GetServiceConfig(string serviceName)
+ {
+ return this._memoryConfiguration.GetServiceConfig(this._rawAppSettings, serviceName);
+ }
+}
diff --git a/importdocument/Config.cs b/tools/importdocument/Config.cs
similarity index 63%
rename from importdocument/Config.cs
rename to tools/importdocument/Config.cs
index abf5f59c7..7c7565283 100644
--- a/importdocument/Config.cs
+++ b/tools/importdocument/Config.cs
@@ -12,26 +12,42 @@ public sealed class Config
///
/// Client ID for the app as registered in Azure AD.
///
+ public string AuthenticationType { get; set; } = "None";
+
+ ///
+ /// Client ID for the import document tool as registered in Azure AD.
+ ///
public string ClientId { get; set; } = string.Empty;
+ ///
+ /// Client ID for the backend web api as registered in Azure AD.
+ ///
+ public string BackendClientId { get; set; } = string.Empty;
+
+ ///
+ /// Tenant ID against which to authenticate users in Azure AD.
+ ///
+ public string TenantId { get; set; } = string.Empty;
+
+ ///
+ /// Azure AD cloud instance for authenticating users.
+ ///
+ public string Instance { get; set; } = string.Empty;
+
+ ///
+ /// Scopes that the client app requires to access the API.
+ ///
+ public string Scopes { get; set; } = string.Empty;
+
///
/// Redirect URI for the app as registered in Azure AD.
///
-#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
///
/// Uri for the service that is running the chat.
///
-#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
-
- ///
- /// Api key for the service that is running the chat.
- ///
- public string ApiKey { get; set; } = string.Empty;
///
/// Gets configuration from appsettings.json.
diff --git a/importdocument/ImportDocument.csproj b/tools/importdocument/ImportDocument.csproj
similarity index 58%
rename from importdocument/ImportDocument.csproj
rename to tools/importdocument/ImportDocument.csproj
index a9c42e30b..b2b3eacc6 100644
--- a/importdocument/ImportDocument.csproj
+++ b/tools/importdocument/ImportDocument.csproj
@@ -2,11 +2,6 @@
Exe
net6.0
- LatestMajor
- disable
- enable
- 10
- false
@@ -16,10 +11,10 @@
-
-
-
-
+
+
+
+
diff --git a/tools/importdocument/Program.cs b/tools/importdocument/Program.cs
new file mode 100644
index 000000000..5bbddc5e2
--- /dev/null
+++ b/tools/importdocument/Program.cs
@@ -0,0 +1,182 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.CommandLine;
+using Microsoft.Identity.Client;
+
+namespace ImportDocument;
+
+///
+/// This console app imports a list of files to Chat Copilot's WebAPI document memory store.
+///
+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>(name: "--files", description: "The files to import to document memory store.")
+ {
+ IsRequired = true,
+ AllowMultipleArgumentsPerToken = true,
+ };
+
+ var chatCollectionOption = new Option(
+ 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 Chat Copilot's WebAPI document memory store."
+ )
+ {
+ filesOption,
+ chatCollectionOption
+ };
+
+ rootCommand.SetHandler(async (files, chatCollectionId) =>
+ {
+ await ImportFilesAsync(files, config!, chatCollectionId);
+ },
+ filesOption,
+ chatCollectionOption
+ );
+
+ rootCommand.Invoke(args);
+ }
+
+ ///
+ /// Acquires a user account from Azure AD.
+ ///
+ /// The App configuration.
+ /// Sets the access token to the first account found.
+ /// True if the user account was acquired.
+ private static async Task AcquireTokenAsync(
+ Config config,
+ Action setAccessToken)
+ {
+ Console.WriteLine("Attempting to authenticate user...");
+
+ var webApiScope = $"api://{config.BackendClientId}/{config.Scopes}";
+ string[] scopes = { webApiScope };
+ try
+ {
+ var app = PublicClientApplicationBuilder.Create(config.ClientId)
+ .WithRedirectUri(config.RedirectUri)
+ .WithAuthority(config.Instance, config.TenantId)
+ .Build();
+ var result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();
+ setAccessToken(result.AccessToken);
+ return true;
+ }
+ catch (Exception ex) when (ex is MsalServiceException or MsalClientException)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Conditionally imports a list of files to the Document Store.
+ ///
+ /// A list of files to import.
+ /// Configuration.
+ /// Save the extracted context to an isolated chat collection.
+ private static async Task ImportFilesAsync(IEnumerable files, Config config, Guid chatCollectionId)
+ {
+ foreach (var file in files)
+ {
+ if (!file.Exists)
+ {
+ Console.WriteLine($"File {file.FullName} does not exist.");
+ return;
+ }
+ }
+
+ string? accessToken = null;
+ if (config.AuthenticationType == "AzureAd")
+ {
+ if (await AcquireTokenAsync(config, v => { accessToken = v; }) == false)
+ {
+ Console.WriteLine("Error: Failed to acquire access token.");
+ return;
+ }
+
+ Console.WriteLine("Successfully acquired access token. Continuing...");
+ }
+
+ using var formContent = new MultipartFormDataContent();
+ List 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);
+ }
+
+ if (chatCollectionId != Guid.Empty)
+ {
+ Console.WriteLine($"Uploading and parsing file to chat {chatCollectionId}...");
+
+ await UploadAsync(chatCollectionId);
+ }
+ else
+ {
+ Console.WriteLine("Uploading and parsing file to global collection...");
+
+ await UploadAsync();
+ }
+
+ // Dispose of all the file streams.
+ foreach (var fileContent in filesContent)
+ {
+ fileContent.Dispose();
+ }
+
+ async Task UploadAsync(Guid? chatId = null)
+ {
+ // 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
+ };
+
+ if (config.AuthenticationType == "AzureAd")
+ {
+ // Add required properties to the request header.
+ httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken!}");
+ }
+
+ string uriPath =
+ chatId.HasValue ? $"chats/{chatId}/documents" : "documents";
+
+ try
+ {
+ using HttpResponseMessage response = await httpClient.PostAsync(
+ new Uri(new Uri(config.ServiceUri), uriPath),
+ formContent);
+
+ 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}");
+ }
+ }
+ }
+}
diff --git a/tools/importdocument/README.md b/tools/importdocument/README.md
new file mode 100644
index 000000000..0952db9d4
--- /dev/null
+++ b/tools/importdocument/README.md
@@ -0,0 +1,112 @@
+# 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 Chat Copilot 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 Chat Copilot to have up-to-date knowledge of specific contexts, such as enterprise and personal data.
+
+
+## Running the app against a local Chat Copilot instance
+
+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:
+ - `ServiceUri` is the address the web api is running at
+ - `AuthenticationType` should be set to "None"
+ - The remaining variables can be left blank or with their default values
+
+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]`
+
+ > 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):
+
+ ![Document-Memory-Sample-1](https://github.com/microsoft/chat-copilot/assets/52973358/3d35df4d-40f1-4f12-8e40-fd190d5ce127)
+
+ With [Microsoft Responsible AI Standard v2 General Requirements.pdf](./sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf):
+
+ ![Document-Memory-Sample-2](https://github.com/microsoft/chat-copilot/assets/52973358/f0e95104-72ca-4a0a-9555-ee335d8df696)
+
+
+## Running the app against a deployed Chat Copilot instance
+
+### Configure your environment
+
+1. Create a registered app in Azure Portal (https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app).
+
+ > Note that this needs to be a separate app registration from those you created when deploying Chat Copilot.
+
+ - Select Mobile and desktop applications as platform type, and the Redirect URI will be `http://localhost`
+ - Select **`Accounts in this organizational directory only (Microsoft only - Single tenant)`** as the supported account
+ type for this sample.
+
+ > **IMPORTANT:** The supported account type should match that of the backend's app registration. If you changed this setting to allow allow multitenant and personal Microsoft accounts access to your Chat Copilot application, you should change it here as well.
+
+ - Note the **`Application (client) ID`** from your app registration.
+
+2. Update the API permissions in the app registration you just created.
+
+ - From the left-hand menu, select **API permissions** and then **Add a permission**.
+ - Select **My APIs** and then select the application corresponding to your backend web api.
+ - Check the box next to `access_as_user` and then press **Add permissions**.
+
+3. Update the authorized client applications for your backend web api.
+ - In the Azure portal, navigate to your backend web api's app registration.
+ - From the left-hand menu, select **Expose an API** and then **Add a client application**.
+ - Enter the client ID of the app registration you just created and check the box under **Authorized scopes**. Then press **Add application**.
+
+4. Configure the appsettings.json file under this folder root with the following variables:
+ - `ServiceUri` is the address the web api is running at
+ - `AuthenticationType` should be set to "AzureAd"
+ - `ClientId` is the **Application (client) ID** GUID from the app registration you just created in the Azure Portal
+ - `RedirectUri` is the Redirect URI also from the app registration you just created in the Azure Portal (e.g. `http://localhost`)
+ - `BackendClientId` is the **Application (client) ID** GUID from your backend web api's app registration in the Azure Portal,
+ - `TenantId` is the Azure AD tenant ID that you want to authenticate users against. For single-tenant applications, this is the same as the **Directory (tenant) ID** from your app registration in the Azure Portal.
+ - `Instance` is the Azure AD cloud instance to authenticate users against. For most users, this is `https://login.microsoftonline.com`.
+ - `Scopes` should be set to "access_as_user"
+
+### Run the app
+
+1. Change directory to this folder root.
+2. **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 both of these commands will open a browser window for you to sign in to ensure you have access to the Chat Copilot service.
+
+ > 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`
diff --git a/tools/importdocument/appsettings.json b/tools/importdocument/appsettings.json
new file mode 100644
index 000000000..8409065c4
--- /dev/null
+++ b/tools/importdocument/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Config": {
+ "ServiceUri": "https://localhost:40443",
+ "AuthenticationType": "None", // Supported authentication types are "None" or "AzureAd"
+ "ClientId": "",
+ "RedirectUri": "http://localhost",
+ "BackendClientId": "",
+ "TenantId": "",
+ "Instance": "https://login.microsoftonline.com",
+ "Scopes": "access_as_user" // Scopes that the client app requires to access the API
+ }
+}
\ No newline at end of file
diff --git a/importdocument/sample-docs/Lorem_ipsum.pdf b/tools/importdocument/sample-docs/Lorem_ipsum.pdf
similarity index 100%
rename from importdocument/sample-docs/Lorem_ipsum.pdf
rename to tools/importdocument/sample-docs/Lorem_ipsum.pdf
diff --git a/importdocument/sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf b/tools/importdocument/sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf
similarity index 100%
rename from importdocument/sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf
rename to tools/importdocument/sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf
diff --git a/importdocument/sample-docs/ms10k.txt b/tools/importdocument/sample-docs/ms10k.txt
similarity index 100%
rename from importdocument/sample-docs/ms10k.txt
rename to tools/importdocument/sample-docs/ms10k.txt
diff --git a/webapi/Auth/ApiKeyAuthenticationHandler.cs b/webapi/Auth/ApiKeyAuthenticationHandler.cs
deleted file mode 100644
index 550f862b1..000000000
--- a/webapi/Auth/ApiKeyAuthenticationHandler.cs
+++ /dev/null
@@ -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;
-
-///
-/// Class implementing API key authentication.
-///
-public class ApiKeyAuthenticationHandler : AuthenticationHandler
-{
- public const string AuthenticationScheme = "ApiKey";
- public const string ApiKeyHeaderName = "x-sk-api-key";
-
- ///
- /// Constructor
- ///
- public ApiKeyAuthenticationHandler(
- IOptionsMonitor options,
- ILoggerFactory loggerFactory,
- UrlEncoder encoder,
- ISystemClock clock) : base(options, loggerFactory, encoder, clock)
- {
- }
-
- protected override Task 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));
- }
-}
diff --git a/webapi/Auth/ApiKeyAuthenticationSchemeOptions.cs b/webapi/Auth/ApiKeyAuthenticationSchemeOptions.cs
deleted file mode 100644
index 33e8943f2..000000000
--- a/webapi/Auth/ApiKeyAuthenticationSchemeOptions.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.AspNetCore.Authentication;
-
-namespace SemanticKernel.Service.Auth;
-
-///
-/// Options for API key authentication.
-///
-public class ApiKeyAuthenticationSchemeOptions : AuthenticationSchemeOptions
-{
- ///
- /// The API key against which to authenticate.
- ///
- public string? ApiKey { get; set; }
-}
diff --git a/webapi/Auth/AuthInfo.cs b/webapi/Auth/AuthInfo.cs
new file mode 100644
index 000000000..bd7c689bc
--- /dev/null
+++ b/webapi/Auth/AuthInfo.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure.Identity;
+using Microsoft.Identity.Web;
+
+namespace CopilotChat.WebApi.Auth;
+
+///
+/// Class which provides validated security information for use in controllers.
+///
+public class AuthInfo : IAuthInfo
+{
+ private record struct AuthData(
+ string UserId,
+ string UserName);
+
+ private readonly Lazy _data;
+
+ public AuthInfo(IHttpContextAccessor httpContextAccessor)
+ {
+ this._data = new Lazy(() =>
+ {
+ var user = httpContextAccessor.HttpContext?.User;
+ if (user is null)
+ {
+ throw new InvalidOperationException("HttpContext must be present to inspect auth info.");
+ }
+
+ var userIdClaim = user.FindFirst(ClaimConstants.Oid)
+ ?? user.FindFirst(ClaimConstants.ObjectId)
+ ?? user.FindFirst(ClaimConstants.Sub)
+ ?? user.FindFirst(ClaimConstants.NameIdentifierId);
+ if (userIdClaim is null)
+ {
+ throw new CredentialUnavailableException("User Id was not present in the request token.");
+ }
+
+ var tenantIdClaim = user.FindFirst(ClaimConstants.Tid)
+ ?? user.FindFirst(ClaimConstants.TenantId);
+ var userNameClaim = user.FindFirst(ClaimConstants.Name);
+ if (userNameClaim is null)
+ {
+ throw new CredentialUnavailableException("User name was not present in the request token.");
+ }
+
+ return new AuthData
+ {
+ UserId = (tenantIdClaim is null) ? userIdClaim.Value : string.Join(".", userIdClaim.Value, tenantIdClaim.Value),
+ UserName = userNameClaim.Value,
+ };
+ }, isThreadSafe: false);
+ }
+
+ ///
+ /// The authenticated user's unique ID.
+ ///
+ public string UserId => this._data.Value.UserId;
+
+ ///
+ /// The authenticated user's name.
+ ///
+ public string Name => this._data.Value.UserName;
+}
diff --git a/webapi/Auth/AuthPolicyName.cs b/webapi/Auth/AuthPolicyName.cs
new file mode 100644
index 000000000..2d58aec32
--- /dev/null
+++ b/webapi/Auth/AuthPolicyName.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace CopilotChat.WebApi.Auth;
+
+///
+/// Holds the policy names for custom authorization policies.
+///
+public static class AuthPolicyName
+{
+ public const string RequireChatParticipant = "RequireChatParticipant";
+}
diff --git a/webapi/Auth/ChatParticipantAuthorizationHandler.cs b/webapi/Auth/ChatParticipantAuthorizationHandler.cs
new file mode 100644
index 000000000..add5a8d13
--- /dev/null
+++ b/webapi/Auth/ChatParticipantAuthorizationHandler.cs
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure.Identity;
+using CopilotChat.WebApi.Storage;
+using Microsoft.AspNetCore.Authorization;
+
+namespace CopilotChat.WebApi.Auth;
+
+///
+/// Class implementing "authorization" that validates the user has access to a chat.
+///
+public class ChatParticipantAuthorizationHandler : AuthorizationHandler
+{
+ private readonly IAuthInfo _authInfo;
+ private readonly ChatSessionRepository _chatSessionRepository;
+ private readonly ChatParticipantRepository _chatParticipantRepository;
+
+ ///
+ /// Constructor
+ ///
+ public ChatParticipantAuthorizationHandler(
+ IAuthInfo authInfo,
+ ChatSessionRepository chatSessionRepository,
+ ChatParticipantRepository chatParticipantRepository) : base()
+ {
+ this._authInfo = authInfo;
+ this._chatSessionRepository = chatSessionRepository;
+ this._chatParticipantRepository = chatParticipantRepository;
+ }
+
+ protected override async Task HandleRequirementAsync(
+ AuthorizationHandlerContext context,
+ ChatParticipantRequirement requirement,
+ HttpContext resource)
+ {
+ try
+ {
+ string? chatId = resource.GetRouteValue("chatId")?.ToString();
+ if (chatId == null)
+ {
+ // delegate to downstream validation
+ context.Succeed(requirement);
+ return;
+ }
+
+ var session = await this._chatSessionRepository.FindByIdAsync(chatId);
+ if (session == null)
+ {
+ // delegate to downstream validation
+ context.Succeed(requirement);
+ return;
+ }
+
+ bool isUserInChat = await this._chatParticipantRepository.IsUserInChatAsync(this._authInfo.UserId, chatId);
+ if (!isUserInChat)
+ {
+ context.Fail(new AuthorizationFailureReason(this, "User does not have access to the requested chat."));
+ }
+
+ context.Succeed(requirement);
+ }
+ catch (CredentialUnavailableException ex)
+ {
+ context.Fail(new AuthorizationFailureReason(this, ex.Message));
+ }
+ }
+}
diff --git a/webapi/Auth/ChatParticipantRequirement.cs b/webapi/Auth/ChatParticipantRequirement.cs
new file mode 100644
index 000000000..a28671643
--- /dev/null
+++ b/webapi/Auth/ChatParticipantRequirement.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.AspNetCore.Authorization;
+
+namespace CopilotChat.WebApi.Auth;
+
+///
+/// Used to require the chat to be owned by the authenticated user.
+///
+public class ChatParticipantRequirement : IAuthorizationRequirement
+{
+}
diff --git a/webapi/Auth/IAuthInfo.cs b/webapi/Auth/IAuthInfo.cs
new file mode 100644
index 000000000..6769144ac
--- /dev/null
+++ b/webapi/Auth/IAuthInfo.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace CopilotChat.WebApi.Auth;
+
+public interface IAuthInfo
+{
+ ///
+ /// The authenticated user's unique ID.
+ ///
+ public string UserId { get; }
+
+ ///
+ /// The authenticated user's name.
+ ///
+ public string Name { get; }
+}
diff --git a/webapi/Auth/PassThroughAuthenticationHandler.cs b/webapi/Auth/PassThroughAuthenticationHandler.cs
index e12320b89..661e79bf6 100644
--- a/webapi/Auth/PassThroughAuthenticationHandler.cs
+++ b/webapi/Auth/PassThroughAuthenticationHandler.cs
@@ -2,12 +2,11 @@
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.Identity.Web;
-namespace SemanticKernel.Service.Auth;
+namespace CopilotChat.WebApi.Auth;
///
/// Class implementing "authentication" that lets all requests pass through.
@@ -15,6 +14,8 @@ namespace SemanticKernel.Service.Auth;
public class PassThroughAuthenticationHandler : AuthenticationHandler
{
public const string AuthenticationScheme = "PassThrough";
+ private const string DefaultUserId = "c05c61eb-65e4-4223-915a-fe72b0c9ece1";
+ private const string DefaultUserName = "Default User";
///
/// Constructor
@@ -22,8 +23,7 @@ public class PassThroughAuthenticationHandler : AuthenticationHandler options,
ILoggerFactory loggerFactory,
- UrlEncoder encoder,
- ISystemClock clock) : base(options, loggerFactory, encoder, clock)
+ UrlEncoder encoder) : base(options, loggerFactory, encoder)
{
}
@@ -31,9 +31,17 @@ protected override Task HandleAuthenticateAsync()
{
this.Logger.LogInformation("Allowing request to pass through");
- var principal = new ClaimsPrincipal(new ClaimsIdentity(AuthenticationScheme));
- var ticket = new AuthenticationTicket(principal, this.Scheme.Name);
+ Claim userIdClaim = new(ClaimConstants.Sub, DefaultUserId);
+ Claim nameClaim = new(ClaimConstants.Name, DefaultUserName);
+ ClaimsIdentity identity = new(new Claim[] { userIdClaim, nameClaim }, AuthenticationScheme);
+ ClaimsPrincipal principal = new(identity);
+ AuthenticationTicket ticket = new(principal, this.Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
+
+ ///
+ /// Returns true if the given user ID is the default user guest ID.
+ ///
+ public static bool IsDefaultUser(string userId) => userId == DefaultUserId;
}
diff --git a/webapi/Controllers/ChatArchiveController.cs b/webapi/Controllers/ChatArchiveController.cs
new file mode 100644
index 000000000..7fcaf26c2
--- /dev/null
+++ b/webapi/Controllers/ChatArchiveController.cs
@@ -0,0 +1,174 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using CopilotChat.WebApi.Auth;
+using CopilotChat.WebApi.Extensions;
+using CopilotChat.WebApi.Models.Response;
+using CopilotChat.WebApi.Models.Storage;
+using CopilotChat.WebApi.Options;
+using CopilotChat.WebApi.Storage;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using Microsoft.KernelMemory;
+
+namespace CopilotChat.WebApi.Controllers;
+
+[ApiController]
+public class ChatArchiveController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private readonly IKernelMemory _memoryClient;
+ private readonly ChatSessionRepository _chatRepository;
+ private readonly ChatMessageRepository _chatMessageRepository;
+ private readonly ChatParticipantRepository _chatParticipantRepository;
+ private readonly ChatArchiveEmbeddingConfig _embeddingConfig;
+ private readonly PromptsOptions _promptOptions;
+
+ ///
+ /// Constructor.
+ ///
+ /// Memory client.
+ /// The chat session repository.
+ /// The chat message repository.
+ /// The chat participant repository.
+ /// The document memory options.
+ /// The logger.
+ public ChatArchiveController(
+ IKernelMemory memoryClient,
+ ChatSessionRepository chatRepository,
+ ChatMessageRepository chatMessageRepository,
+ ChatParticipantRepository chatParticipantRepository,
+ ChatArchiveEmbeddingConfig embeddingConfig,
+ IOptions promptOptions,
+ ILogger logger)
+ {
+ this._memoryClient = memoryClient;
+ this._logger = logger;
+ this._chatRepository = chatRepository;
+ this._chatMessageRepository = chatMessageRepository;
+ this._chatParticipantRepository = chatParticipantRepository;
+ this._embeddingConfig = embeddingConfig;
+ this._promptOptions = promptOptions.Value;
+ }
+
+ ///
+ /// Download a chat archive.
+ ///
+ /// The ID of chat to be downloaded.
+ /// Cancellation token.
+ /// The serialized chat archive object of the chat id.
+ [HttpGet]
+ [Route("chats/{chatId:guid}/archive")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Authorize(Policy = AuthPolicyName.RequireChatParticipant)]
+ public async Task> DownloadAsync(Guid chatId, CancellationToken cancellationToken = default)
+ {
+ this._logger.LogDebug("Received call to download a chat archive");
+
+ var chatArchive = await this.CreateChatArchiveAsync(chatId, cancellationToken);
+
+ return this.Ok(chatArchive);
+ }
+
+ ///
+ /// Prepare a chat archive.
+ ///
+ /// The chat id of the chat archive
+ /// Cancellation token.
+ /// A ChatArchive object that represents the chat session.
+ private async Task CreateChatArchiveAsync(Guid chatId, CancellationToken cancellationToken)
+ {
+ var chatIdString = chatId.ToString();
+ var chatArchive = new ChatArchive
+ {
+ // Get embedding configuration
+ EmbeddingConfigurations = this._embeddingConfig,
+ };
+
+ // get the chat title
+ ChatSession chat = await this._chatRepository.FindByIdAsync(chatIdString);
+ chatArchive.ChatTitle = chat.Title;
+
+ // get the system description
+ chatArchive.SystemDescription = chat.SafeSystemDescription;
+
+ // get the chat history
+ chatArchive.ChatHistory = await this.GetAllChatMessagesAsync(chatIdString);
+
+ foreach (var memory in this._promptOptions.MemoryMap.Keys)
+ {
+ chatArchive.Embeddings.Add(
+ memory,
+ await this.GetMemoryRecordsAndAppendToEmbeddingsAsync(chatIdString, memory, cancellationToken));
+ }
+
+ // get the document memory collection names (global scope)
+ chatArchive.DocumentEmbeddings.Add(
+ "GlobalDocuments",
+ await this.GetMemoryRecordsAndAppendToEmbeddingsAsync(
+ Guid.Empty.ToString(),
+ this._promptOptions.DocumentMemoryName,
+ cancellationToken));
+
+ // get the document memory collection names (user scope)
+ chatArchive.DocumentEmbeddings.Add(
+ "ChatDocuments",
+ await this.GetMemoryRecordsAndAppendToEmbeddingsAsync(
+ chatIdString,
+ this._promptOptions.DocumentMemoryName,
+ cancellationToken));
+
+ return chatArchive;
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The current collection name. Used to query the memory storage.
+ /// The embeddings list where we will append the fetched memory records.
+ ///
+ /// The new collection name when appends to the embeddings list. Will use the old collection name if not provided.
+ ///
+ private async Task> GetMemoryRecordsAndAppendToEmbeddingsAsync(
+ string chatId,
+ string memoryName,
+ CancellationToken cancellationToken)
+ {
+ List collectionMemoryRecords;
+ try
+ {
+ var result = await this._memoryClient.SearchMemoryAsync(
+ this._promptOptions.MemoryIndexName,
+ query: "*", // dummy query since we don't care about relevance. An empty string will cause exception.
+ relevanceThreshold: -1, // no relevance required since the collection only has one entry
+ chatId,
+ memoryName,
+ cancellationToken);
+
+ collectionMemoryRecords = result.Results;
+ }
+ catch (Exception connectorException) when (!connectorException.IsCriticalException())
+ {
+ // A store exception might be thrown if the collection does not exist, depending on the memory store connector.
+ this._logger.LogError(connectorException,
+ "Cannot search collection {0}",
+ memoryName);
+ collectionMemoryRecords = new();
+ }
+
+ return collectionMemoryRecords;
+ }
+
+ ///
+ /// Get chat messages of a given chat id.
+ ///
+ /// The chat id
+ /// The list of chat messages in descending order of the timestamp
+ private async Task> GetAllChatMessagesAsync(string chatId)
+ {
+ return (await this._chatMessageRepository.FindByChatIdAsync(chatId)).ToList();
+ }
+}
diff --git a/webapi/Controllers/ChatController.cs b/webapi/Controllers/ChatController.cs
new file mode 100644
index 000000000..49e177b6b
--- /dev/null
+++ b/webapi/Controllers/ChatController.cs
@@ -0,0 +1,499 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using CopilotChat.WebApi.Auth;
+using CopilotChat.WebApi.Hubs;
+using CopilotChat.WebApi.Models.Request;
+using CopilotChat.WebApi.Models.Response;
+using CopilotChat.WebApi.Models.Storage;
+using CopilotChat.WebApi.Options;
+using CopilotChat.WebApi.Plugins.Chat;
+using CopilotChat.WebApi.Services;
+using CopilotChat.WebApi.Storage;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.Options;
+using Microsoft.Graph;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Plugins.MsGraph;
+using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors;
+using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors.Client;
+using Microsoft.SemanticKernel.Plugins.OpenApi;
+
+namespace CopilotChat.WebApi.Controllers;
+
+///
+/// Controller responsible for handling chat messages and responses.
+///
+[ApiController]
+public class ChatController : ControllerBase, IDisposable
+{
+ private readonly ILogger _logger;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly List _disposables;
+ private readonly ITelemetryService _telemetryService;
+ private readonly ServiceOptions _serviceOptions;
+ private readonly MsGraphOboPluginOptions _msGraphOboPluginOptions;
+ private readonly PromptsOptions _promptsOptions;
+ private readonly IDictionary _plugins;
+
+ private const string ChatPluginName = nameof(ChatPlugin);
+ private const string ChatFunctionName = "Chat";
+ private const string GeneratingResponseClientCall = "ReceiveBotResponseStatus";
+
+ public ChatController(
+ ILogger logger,
+ IHttpClientFactory httpClientFactory,
+ ITelemetryService telemetryService,
+ IOptions serviceOptions,
+ IOptions msGraphOboPluginOptions,
+ IOptions promptsOptions,
+ IDictionary plugins)
+ {
+ this._logger = logger;
+ this._httpClientFactory = httpClientFactory;
+ this._telemetryService = telemetryService;
+ this._disposables = new List();
+ this._serviceOptions = serviceOptions.Value;
+ this._msGraphOboPluginOptions = msGraphOboPluginOptions.Value;
+ this._promptsOptions = promptsOptions.Value;
+ this._plugins = plugins;
+ }
+
+ ///
+ /// Invokes the chat function to get a response from the bot.
+ ///
+ /// Semantic kernel obtained through dependency injection.
+ /// Message Hub that performs the real time relay service.
+ /// Repository of chat sessions.
+ /// Repository of chat participants.
+ /// Auth info for the current request.
+ /// Prompt along with its parameters.
+ /// Chat ID.
+ /// Results containing the response from the model.
+ [Route("chats/{chatId:guid}/messages")]
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status504GatewayTimeout)]
+ public async Task ChatAsync(
+ [FromServices] Kernel kernel,
+ [FromServices] IHubContext messageRelayHubContext,
+ [FromServices] ChatSessionRepository chatSessionRepository,
+ [FromServices] ChatParticipantRepository chatParticipantRepository,
+ [FromServices] IAuthInfo authInfo,
+ [FromBody] Ask ask,
+ [FromRoute] Guid chatId)
+ {
+ this._logger.LogDebug("Chat message received.");
+
+ string chatIdString = chatId.ToString();
+
+ // Put ask's variables in the context we will use.
+ var contextVariables = GetContextVariables(ask, authInfo, chatIdString);
+
+ // Verify that the chat exists and that the user has access to it.
+ ChatSession? chat = null;
+ if (!(await chatSessionRepository.TryFindByIdAsync(chatIdString, callback: c => chat = c)))
+ {
+ return this.NotFound("Failed to find chat session for the chatId specified in variables.");
+ }
+
+ if (!(await chatParticipantRepository.IsUserInChatAsync(authInfo.UserId, chatIdString)))
+ {
+ return this.Forbid("User does not have access to the chatId specified in variables.");
+ }
+
+ // Register plugins that have been enabled
+ var openApiPluginAuthHeaders = this.GetPluginAuthHeaders(this.HttpContext.Request.Headers);
+ await this.RegisterFunctionsAsync(kernel, openApiPluginAuthHeaders, contextVariables);
+
+ // Register hosted plugins that have been enabled
+ await this.RegisterHostedFunctionsAsync(kernel, chat!.EnabledPlugins);
+
+ // Get the function to invoke
+ KernelFunction? chatFunction = kernel.Plugins.GetFunction(ChatPluginName, ChatFunctionName);
+
+ // Run the function.
+ FunctionResult? result = null;
+ try
+ {
+ using CancellationTokenSource? cts = this._serviceOptions.TimeoutLimitInS is not null
+ // Create a cancellation token source with the timeout if specified
+ ? new CancellationTokenSource(TimeSpan.FromSeconds((double)this._serviceOptions.TimeoutLimitInS))
+ : null;
+
+ result = await kernel.InvokeAsync(chatFunction!, contextVariables, cts?.Token ?? default);
+ this._telemetryService.TrackPluginFunction(ChatPluginName, ChatFunctionName, true);
+ }
+ catch (Exception ex)
+ {
+ if (ex is OperationCanceledException || ex.InnerException is OperationCanceledException)
+ {
+ // Log the timeout and return a 504 response
+ this._logger.LogError("The {FunctionName} operation timed out.", ChatFunctionName);
+ return this.StatusCode(StatusCodes.Status504GatewayTimeout, $"The chat {ChatFunctionName} timed out.");
+ }
+
+ this._telemetryService.TrackPluginFunction(ChatPluginName, ChatFunctionName, false);
+
+ throw;
+ }
+
+ AskResult chatAskResult = new()
+ {
+ Value = result.ToString() ?? string.Empty,
+ Variables = contextVariables.Select(v => new KeyValuePair(v.Key, v.Value))
+ };
+
+ // Broadcast AskResult to all users
+ await messageRelayHubContext.Clients.Group(chatIdString).SendAsync(GeneratingResponseClientCall, chatIdString, null);
+
+ return this.Ok(chatAskResult);
+ }
+
+ ///
+ /// Parse plugin auth values from request headers.
+ ///
+ private Dictionary GetPluginAuthHeaders(IHeaderDictionary headers)
+ {
+ // Create a regex to match the headers
+ var regex = new Regex("x-sk-copilot-(.*)-auth", RegexOptions.IgnoreCase);
+
+ // Create a dictionary to store the matched headers and values
+ var authHeaders = new Dictionary();
+
+ // Loop through the request headers and add the matched ones to the dictionary
+ foreach (var header in headers)
+ {
+ var match = regex.Match(header.Key);
+ if (match.Success)
+ {
+ // Use the first capture group as the key and the header value as the value
+ authHeaders.Add(match.Groups[1].Value.ToUpperInvariant(), header.Value!);
+ }
+ }
+
+ return authHeaders;
+ }
+
+ ///
+ /// Register functions with the kernel.
+ ///
+ private async Task RegisterFunctionsAsync(Kernel kernel, Dictionary authHeaders, KernelArguments variables)
+ {
+ // Register authenticated functions with the kernel only if the request includes an auth header for the plugin.
+
+ var tasks = new List();
+
+ // GitHub
+ if (authHeaders.TryGetValue("GITHUB", out string? githubAuthHeader))
+ {
+ tasks.Add(this.RegisterGithubPluginAsync(kernel, githubAuthHeader));
+ }
+
+ // Jira
+ if (authHeaders.TryGetValue("JIRA", out string? jiraAuthHeader))
+ {
+ tasks.Add(this.RegisterJiraPluginAsync(kernel, jiraAuthHeader, variables));
+ }
+
+ // Microsoft Graph
+ if (authHeaders.TryGetValue("GRAPH", out string? graphAuthHeader))
+ {
+ tasks.Add(this.RegisterMicrosoftGraphPlugins(kernel, graphAuthHeader));
+ }
+
+ // Microsoft Graph OBO
+ if (authHeaders.TryGetValue("MSGRAPHOBO", out string? graphOboAuthHeader))
+ {
+ tasks.Add(this.RegisterMicrosoftGraphOBOPlugins(kernel, graphOboAuthHeader));
+ }
+
+ if (variables.TryGetValue("customPlugins", out object? customPluginsString))
+ {
+ tasks.AddRange(this.RegisterCustomPlugins(kernel, customPluginsString, authHeaders));
+ }
+
+ await Task.WhenAll(tasks);
+ }
+
+ private async Task RegisterGithubPluginAsync(Kernel kernel, string githubAuthHeader)
+ {
+ this._logger.LogInformation("Enabling GitHub plugin.");
+ BearerAuthenticationProvider authenticationProvider = new(() => Task.FromResult(githubAuthHeader));
+ await kernel.ImportPluginFromOpenApiAsync(
+ pluginName: "GitHubPlugin",
+ filePath: GetPluginFullPath("OpenApi/GitHubPlugin/openapi.json"),
+ new OpenApiFunctionExecutionParameters
+ {
+ AuthCallback = authenticationProvider.AuthenticateRequestAsync,
+ });
+ }
+
+ private async Task RegisterJiraPluginAsync(Kernel kernel, string jiraAuthHeader, KernelArguments variables)
+ {
+ this._logger.LogInformation("Registering Jira plugin");
+ var authenticationProvider = new BasicAuthenticationProvider(() => { return Task.FromResult(jiraAuthHeader); });
+ var hasServerUrlOverride = variables.TryGetValue("jira-server-url", out object? serverUrlOverride);
+
+ await kernel.ImportPluginFromOpenApiAsync(
+ pluginName: "JiraPlugin",
+ filePath: GetPluginFullPath("OpenApi/JiraPlugin/openapi.json"),
+ new OpenApiFunctionExecutionParameters
+ {
+ AuthCallback = authenticationProvider.AuthenticateRequestAsync,
+ ServerUrlOverride = hasServerUrlOverride ? new Uri(serverUrlOverride!.ToString()!) : null,
+ });
+ ;
+ ;
+ }
+
+ private Task RegisterMicrosoftGraphPlugins(Kernel kernel, string graphAuthHeader)
+ {
+ this._logger.LogInformation("Enabling Microsoft Graph plugin(s).");
+ BearerAuthenticationProvider authenticationProvider = new(() => Task.FromResult(graphAuthHeader));
+ GraphServiceClient graphServiceClient = this.CreateGraphServiceClient(authenticationProvider.GraphClientAuthenticateRequestAsync);
+
+ kernel.ImportPluginFromObject(new TaskListPlugin(new MicrosoftToDoConnector(graphServiceClient)), "todo");
+ kernel.ImportPluginFromObject(new CalendarPlugin(new OutlookCalendarConnector(graphServiceClient)), "calendar");
+ kernel.ImportPluginFromObject(new EmailPlugin(new OutlookMailConnector(graphServiceClient)), "email");
+ return Task.CompletedTask;
+ }
+
+ private Task RegisterMicrosoftGraphOBOPlugins(Kernel kernel, string graphOboAuthHeader)
+ {
+ this._logger.LogInformation("Enabling Microsoft Graph OBO plugin(s).");
+ kernel.ImportPluginFromObject(
+ new MsGraphOboPlugin(graphOboAuthHeader, this._httpClientFactory, this._msGraphOboPluginOptions, this._promptsOptions.FunctionCallingTokenLimit, this._logger),
+ "msGraphObo");
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// Create a Microsoft Graph service client.
+ ///
+ /// The delegate to authenticate the request.
+ private GraphServiceClient CreateGraphServiceClient(AuthenticateRequestAsyncDelegate authenticateRequestAsyncDelegate)
+ {
+ MsGraphClientLoggingHandler graphLoggingHandler = new(this._logger);
+ this._disposables.Add(graphLoggingHandler);
+
+ IList graphMiddlewareHandlers =
+ GraphClientFactory.CreateDefaultHandlers(new DelegateAuthenticationProvider(authenticateRequestAsyncDelegate));
+ graphMiddlewareHandlers.Add(graphLoggingHandler);
+
+ HttpClient graphHttpClient = GraphClientFactory.Create(graphMiddlewareHandlers);
+ this._disposables.Add(graphHttpClient);
+
+ GraphServiceClient graphServiceClient = new(graphHttpClient);
+ return graphServiceClient;
+ }
+
+ private IEnumerable RegisterCustomPlugins(Kernel kernel, object? customPluginsString, Dictionary authHeaders)
+ {
+ CustomPlugin[]? customPlugins = JsonSerializer.Deserialize(customPluginsString!.ToString()!);
+
+ if (customPlugins != null)
+ {
+ foreach (CustomPlugin plugin in customPlugins)
+ {
+ if (authHeaders.TryGetValue(plugin.AuthHeaderTag.ToUpperInvariant(), out string? pluginAuthValue))
+ {
+ this._logger.LogError("ChatGPT plugins are deprecated. Skipping plugin: {0}", plugin.NameForHuman);
+
+ /*
+ // Register the ChatGPT plugin with the kernel.
+ this._logger.LogInformation("Enabling {0} plugin.", plugin.NameForHuman);
+
+ // TODO: [Issue #44] Support other forms of auth. Currently, we only support user PAT or no auth.
+ var requiresAuth = !plugin.AuthType.Equals("none", StringComparison.OrdinalIgnoreCase);
+
+ Task AuthCallback(HttpRequestMessage request, string _, OpenAIAuthenticationConfig __, CancellationToken ___ = default)
+ {
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", pluginAuthValue);
+
+ return Task.CompletedTask;
+ }
+
+ yield return kernel.ImportPluginFromOpenAIAsync(
+ $"{plugin.NameForModel}Plugin",
+ PluginUtils.GetPluginManifestUri(plugin.ManifestDomain),
+ new OpenAIFunctionExecutionParameters
+ {
+ HttpClient = this._httpClientFactory.CreateClient(),
+ IgnoreNonCompliantErrors = true,
+ AuthCallback = requiresAuth ? AuthCallback : null
+ });
+ */
+ }
+ }
+ }
+ else
+ {
+ this._logger.LogDebug("Failed to deserialize custom plugin details: {0}", customPluginsString);
+ }
+
+ yield return Task.CompletedTask;
+ }
+
+ private Task RegisterHostedFunctionsAsync(Kernel kernel, HashSet enabledPlugins)
+ {
+ foreach (string enabledPlugin in enabledPlugins)
+ {
+ if (this._plugins.TryGetValue(enabledPlugin, out Plugin? plugin))
+ {
+ this._logger.LogError("ChatGPT plugins are deprecated. Skipping plugin {0}", enabledPlugin);
+
+ /*
+ this._logger.LogDebug("Enabling hosted plugin {0}.", plugin.Name);
+
+ Task AuthCallback(HttpRequestMessage request, string _, OpenAIAuthenticationConfig __, CancellationToken ___ = default)
+ {
+ request.Headers.Add("X-Functions-Key", plugin.Key);
+
+ return Task.CompletedTask;
+ }
+
+ // Register the ChatGPT plugin with the kernel.
+ await kernel.ImportPluginFromOpenAIAsync(
+ PluginUtils.SanitizePluginName(plugin.Name),
+ PluginUtils.GetPluginManifestUri(plugin.ManifestDomain),
+ new OpenAIFunctionExecutionParameters
+ {
+ HttpClient = this._httpClientFactory.CreateClient(),
+ IgnoreNonCompliantErrors = true,
+ AuthCallback = AuthCallback
+ });
+ */
+ }
+ else
+ {
+ this._logger.LogWarning("Failed to find plugin {0}.", enabledPlugin);
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static KernelArguments GetContextVariables(Ask ask, IAuthInfo authInfo, string chatId)
+ {
+ const string UserIdKey = "userId";
+ const string UserNameKey = "userName";
+ const string ChatIdKey = "chatId";
+ const string MessageKey = "message";
+
+ var contextVariables = new KernelArguments();
+ foreach (var variable in ask.Variables)
+ {
+ contextVariables[variable.Key] = variable.Value;
+ }
+
+ contextVariables[UserIdKey] = authInfo.UserId;
+ contextVariables[UserNameKey] = authInfo.Name;
+ contextVariables[ChatIdKey] = chatId;
+ contextVariables[MessageKey] = ask.Input;
+
+ return contextVariables;
+ }
+
+ private static string GetPluginFullPath(string pluginPath)
+ {
+ return Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Plugins", pluginPath);
+ }
+
+ ///
+ /// Dispose of the object.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ foreach (IDisposable disposable in this._disposables)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ this.Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
+
+///
+/// Retrieves authentication content (e.g. username/password, API key) via the provided delegate and
+/// applies it to HTTP requests using the "basic" authentication scheme.
+///
+public class BasicAuthenticationProvider
+{
+ private readonly Func> _credentialsDelegate;
+
+ ///
+ /// Creates an instance of the class.
+ ///
+ /// Delegate for retrieving credentials.
+ public BasicAuthenticationProvider(Func> credentialsDelegate)
+ {
+ this._credentialsDelegate = credentialsDelegate;
+ }
+
+ ///
+ /// Applies the authentication content to the provided HTTP request message.
+ ///
+ /// The HTTP request message.
+ /// The cancellation token.
+ public async Task AuthenticateRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
+ {
+ // Base64 encode
+ string encodedContent = Convert.ToBase64String(Encoding.UTF8.GetBytes(await this._credentialsDelegate().ConfigureAwait(false)));
+ request.Headers.Authorization = new AuthenticationHeaderValue("Basic", encodedContent);
+ }
+}
+
+///
+/// Retrieves a token via the provided delegate and applies it to HTTP requests using the
+/// "bearer" authentication scheme.
+///
+public class BearerAuthenticationProvider
+{
+ private readonly Func> _bearerTokenDelegate;
+
+ ///
+ /// Creates an instance of the class.
+ ///
+ /// Delegate to retrieve the bearer token.
+ public BearerAuthenticationProvider(Func> bearerTokenDelegate)
+ {
+ this._bearerTokenDelegate = bearerTokenDelegate;
+ }
+
+ ///
+ /// Applies the token to the provided HTTP request message.
+ ///
+ /// The HTTP request message.
+ public async Task AuthenticateRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
+ {
+ var token = await this._bearerTokenDelegate().ConfigureAwait(false);
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ }
+
+ ///
+ /// Applies the token to the provided HTTP request message.
+ ///
+ /// The HTTP request message.
+ public async Task GraphClientAuthenticateRequestAsync(HttpRequestMessage request)
+ {
+ await this.AuthenticateRequestAsync(request);
+ }
+}
diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs
new file mode 100644
index 000000000..111b256b0
--- /dev/null
+++ b/webapi/Controllers/ChatHistoryController.cs
@@ -0,0 +1,345 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using CopilotChat.WebApi.Auth;
+using CopilotChat.WebApi.Extensions;
+using CopilotChat.WebApi.Hubs;
+using CopilotChat.WebApi.Models.Request;
+using CopilotChat.WebApi.Models.Response;
+using CopilotChat.WebApi.Models.Storage;
+using CopilotChat.WebApi.Options;
+using CopilotChat.WebApi.Plugins.Utils;
+using CopilotChat.WebApi.Storage;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.Options;
+using Microsoft.KernelMemory;
+
+namespace CopilotChat.WebApi.Controllers;
+
+///
+/// Controller for chat history.
+/// This controller is responsible for creating new chat sessions, retrieving chat sessions,
+/// retrieving chat messages, and editing chat sessions.
+///
+[ApiController]
+public class ChatHistoryController : ControllerBase
+{
+ private const string ChatEditedClientCall = "ChatEdited";
+ private const string ChatDeletedClientCall = "ChatDeleted";
+ private const string GetChatRoute = "GetChatRoute";
+
+ private readonly ILogger _logger;
+ private readonly IKernelMemory _memoryClient;
+ private readonly ChatSessionRepository _sessionRepository;
+ private readonly ChatMessageRepository _messageRepository;
+ private readonly ChatParticipantRepository _participantRepository;
+ private readonly ChatMemorySourceRepository _sourceRepository;
+ private readonly PromptsOptions _promptOptions;
+ private readonly IAuthInfo _authInfo;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// Memory client.
+ /// The chat session repository.
+ /// The chat message repository.
+ /// The chat participant repository.
+ /// The chat memory resource repository.
+ /// The prompts options.
+ /// The auth info for the current request.
+ public ChatHistoryController(
+ ILogger logger,
+ IKernelMemory memoryClient,
+ ChatSessionRepository sessionRepository,
+ ChatMessageRepository messageRepository,
+ ChatParticipantRepository participantRepository,
+ ChatMemorySourceRepository sourceRepository,
+ IOptions promptsOptions,
+ IAuthInfo authInfo)
+ {
+ this._logger = logger;
+ this._memoryClient = memoryClient;
+ this._sessionRepository = sessionRepository;
+ this._messageRepository = messageRepository;
+ this._participantRepository = participantRepository;
+ this._sourceRepository = sourceRepository;
+ this._promptOptions = promptsOptions.Value;
+ this._authInfo = authInfo;
+ }
+
+ ///
+ /// Create a new chat session and populate the session with the initial bot message.
+ ///
+ /// Contains the title of the chat.
+ /// The HTTP action result.
+ [HttpPost]
+ [Route("chats")]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task CreateChatSessionAsync(
+ [FromBody] CreateChatParameters chatParameters)
+ {
+ if (chatParameters.Title == null)
+ {
+ return this.BadRequest("Chat session parameters cannot be null.");
+ }
+
+ // Create a new chat session
+ var newChat = new ChatSession(chatParameters.Title, this._promptOptions.SystemDescription);
+ await this._sessionRepository.CreateAsync(newChat);
+
+ // Create initial bot message
+ var chatMessage = CopilotChatMessage.CreateBotResponseMessage(
+ newChat.Id,
+ this._promptOptions.InitialBotMessage,
+ string.Empty, // The initial bot message doesn't need a prompt.
+ null,
+ TokenUtils.EmptyTokenUsages());
+ await this._messageRepository.CreateAsync(chatMessage);
+
+ // Add the user to the chat session
+ await this._participantRepository.CreateAsync(new ChatParticipant(this._authInfo.UserId, newChat.Id));
+
+ this._logger.LogDebug("Created chat session with id {0}.", newChat.Id);
+
+ return this.CreatedAtRoute(GetChatRoute, new { chatId = newChat.Id }, new CreateChatResponse(newChat, chatMessage));
+ }
+
+ ///
+ /// Get a chat session by id.
+ ///
+ /// The chat id.
+ [HttpGet]
+ [Route("chats/{chatId:guid}", Name = GetChatRoute)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Authorize(Policy = AuthPolicyName.RequireChatParticipant)]
+ public async Task GetChatSessionByIdAsync(Guid chatId)
+ {
+ ChatSession? chat = null;
+ if (await this._sessionRepository.TryFindByIdAsync(chatId.ToString(), callback: v => chat = v))
+ {
+ return this.Ok(chat);
+ }
+
+ return this.NotFound($"No chat session found for chat id '{chatId}'.");
+ }
+
+ ///
+ /// Get all chat sessions associated with the logged in user. Return an empty list if no chats are found.
+ ///
+ /// The user id.
+ /// A list of chat sessions. An empty list if the user is not in any chat session.
+ [HttpGet]
+ [Route("chats")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetAllChatSessionsAsync()
+ {
+ // Get all participants that belong to the user.
+ // Then get all the chats from the list of participants.
+ var chatParticipants = await this._participantRepository.FindByUserIdAsync(this._authInfo.UserId);
+
+ var chats = new List();
+ foreach (var chatParticipant in chatParticipants)
+ {
+ ChatSession? chat = null;
+ if (await this._sessionRepository.TryFindByIdAsync(chatParticipant.ChatId, callback: v => chat = v))
+ {
+ chats.Add(chat!);
+ }
+ else
+ {
+ this._logger.LogDebug("Failed to find chat session with id {0}", chatParticipant.ChatId);
+ }
+ }
+
+ return this.Ok(chats);
+ }
+
+ ///
+ /// Get chat messages for a chat session.
+ /// Messages are returned ordered from most recent to oldest.
+ ///
+ /// The chat id.
+ /// Number of messages to skip before starting to return messages.
+ /// The number of messages to return. -1 returns all messages.
+ [HttpGet]
+ [Route("chats/{chatId:guid}/messages")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Authorize(Policy = AuthPolicyName.RequireChatParticipant)]
+ public async Task GetChatMessagesAsync(
+ [FromRoute] Guid chatId,
+ [FromQuery] int skip = 0,
+ [FromQuery] int count = -1)
+ {
+ var chatMessages = await this._messageRepository.FindByChatIdAsync(chatId.ToString(), skip, count);
+ if (!chatMessages.Any())
+ {
+ return this.NotFound($"No messages found for chat id '{chatId}'.");
+ }
+
+ return this.Ok(chatMessages);
+ }
+
+ ///
+ /// Edit a chat session.
+ ///
+ /// Object that contains the parameters to edit the chat.
+ [HttpPatch]
+ [Route("chats/{chatId:guid}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Authorize(Policy = AuthPolicyName.RequireChatParticipant)]
+ public async Task EditChatSessionAsync(
+ [FromServices] IHubContext messageRelayHubContext,
+ [FromBody] EditChatParameters chatParameters,
+ [FromRoute] Guid chatId)
+ {
+ ChatSession? chat = null;
+ if (await this._sessionRepository.TryFindByIdAsync(chatId.ToString(), callback: v => chat = v))
+ {
+ chat!.Title = chatParameters.Title ?? chat!.Title;
+ chat!.SystemDescription = chatParameters.SystemDescription ?? chat!.SafeSystemDescription;
+ chat!.MemoryBalance = chatParameters.MemoryBalance ?? chat!.MemoryBalance;
+ await this._sessionRepository.UpsertAsync(chat);
+ await messageRelayHubContext.Clients.Group(chatId.ToString()).SendAsync(ChatEditedClientCall, chat);
+
+ return this.Ok(chat);
+ }
+
+ return this.NotFound($"No chat session found for chat id '{chatId}'.");
+ }
+
+ ///
+ /// Gets list of imported documents for a given chat.
+ ///
+ [Route("chats/{chatId:guid}/documents")]
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Authorize(Policy = AuthPolicyName.RequireChatParticipant)]
+ public async Task>> GetSourcesAsync(Guid chatId)
+ {
+ this._logger.LogInformation("Get imported sources of chat session {0}", chatId);
+
+ if (await this._sessionRepository.TryFindByIdAsync(chatId.ToString()))
+ {
+ IEnumerable sources = await this._sourceRepository.FindByChatIdAsync(chatId.ToString());
+
+ return this.Ok(sources);
+ }
+
+ return this.NotFound($"No chat session found for chat id '{chatId}'.");
+ }
+
+ ///
+ /// Delete a chat session.
+ ///
+ /// The chat id.
+ [HttpDelete]
+ [Route("chats/{chatId:guid}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ [Authorize(Policy = AuthPolicyName.RequireChatParticipant)]
+ public async Task DeleteChatSessionAsync(
+ [FromServices] IHubContext messageRelayHubContext,
+ Guid chatId,
+ CancellationToken cancellationToken)
+ {
+ var chatIdString = chatId.ToString();
+ ChatSession? chatToDelete = null;
+ try
+ {
+ // Make sure the chat session exists
+ chatToDelete = await this._sessionRepository.FindByIdAsync(chatIdString);
+ }
+ catch (KeyNotFoundException)
+ {
+ return this.NotFound($"No chat session found for chat id '{chatId}'.");
+ }
+
+ // Delete any resources associated with the chat session.
+ try
+ {
+ await this.DeleteChatResourcesAsync(chatIdString, cancellationToken);
+ }
+ catch (AggregateException)
+ {
+ return this.StatusCode(500, $"Failed to delete resources for chat id '{chatId}'.");
+ }
+
+ // Delete chat session and broadcast update to all participants.
+ await this._sessionRepository.DeleteAsync(chatToDelete);
+ await messageRelayHubContext.Clients.Group(chatIdString).SendAsync(ChatDeletedClientCall, chatIdString, this._authInfo.UserId, cancellationToken: cancellationToken);
+
+ return this.NoContent();
+ }
+
+ ///
+ /// Deletes all associated resources (messages, memories, participants) associated with a chat session.
+ ///
+ /// The chat id.
+ private async Task DeleteChatResourcesAsync(string chatId, CancellationToken cancellationToken)
+ {
+ var cleanupTasks = new List();
+
+ // Create and store the tasks for deleting all users tied to the chat.
+ var participants = await this._participantRepository.FindByChatIdAsync(chatId);
+ foreach (var participant in participants)
+ {
+ cleanupTasks.Add(this._participantRepository.DeleteAsync(participant));
+ }
+
+ // Create and store the tasks for deleting chat messages.
+ var messages = await this._messageRepository.FindByChatIdAsync(chatId);
+ foreach (var message in messages)
+ {
+ cleanupTasks.Add(this._messageRepository.DeleteAsync(message));
+ }
+
+ // Create and store the tasks for deleting memory sources.
+ var sources = await this._sourceRepository.FindByChatIdAsync(chatId, false);
+ foreach (var source in sources)
+ {
+ cleanupTasks.Add(this._sourceRepository.DeleteAsync(source));
+ }
+
+ // Create and store the tasks for deleting semantic memories.
+ cleanupTasks.Add(this._memoryClient.RemoveChatMemoriesAsync(this._promptOptions.MemoryIndexName, chatId, cancellationToken));
+
+ // Create a task that represents the completion of all cleanupTasks
+ Task aggregationTask = Task.WhenAll(cleanupTasks);
+ try
+ {
+ // Await the completion of all tasks in parallel
+ await aggregationTask;
+ }
+ catch (Exception ex)
+ {
+ // Handle any exceptions that occurred during the tasks
+ if (aggregationTask?.Exception?.InnerExceptions != null && aggregationTask.Exception.InnerExceptions.Count != 0)
+ {
+ foreach (var innerEx in aggregationTask.Exception.InnerExceptions)
+ {
+ this._logger.LogInformation("Failed to delete an entity of chat {0}: {1}", chatId, innerEx.Message);
+ }
+
+ throw aggregationTask.Exception;
+ }
+
+ throw new AggregateException($"Resource deletion failed for chat {chatId}.", ex);
+ }
+ }
+}
diff --git a/webapi/Controllers/ChatMemoryController.cs b/webapi/Controllers/ChatMemoryController.cs
new file mode 100644
index 000000000..07c439a15
--- /dev/null
+++ b/webapi/Controllers/ChatMemoryController.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using CopilotChat.WebApi.Auth;
+using CopilotChat.WebApi.Extensions;
+using CopilotChat.WebApi.Models.Request;
+using CopilotChat.WebApi.Options;
+using CopilotChat.WebApi.Storage;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using Microsoft.KernelMemory;
+
+namespace CopilotChat.WebApi.Controllers;
+
+///
+/// Controller for retrieving kernel memory data of chat sessions.
+///
+[ApiController]
+public class ChatMemoryController : ControllerBase
+{
+ private readonly ILogger _logger;
+
+ private readonly PromptsOptions _promptOptions;
+
+ private readonly ChatSessionRepository _chatSessionRepository;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// The prompts options.
+ /// The chat session repository.
+ public ChatMemoryController(
+ ILogger logger,
+ IOptions promptsOptions,
+ ChatSessionRepository chatSessionRepository)
+ {
+ this._logger = logger;
+ this._promptOptions = promptsOptions.Value;
+ this._chatSessionRepository = chatSessionRepository;
+ }
+
+ ///
+ /// Gets the kernel memory for the chat session.
+ ///
+ /// The kernel memory client.
+ /// The chat id.
+ /// Type of memory. Must map to a member of .
+ [HttpGet]
+ [Route("chats/{chatId:guid}/memories")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [Authorize(Policy = AuthPolicyName.RequireChatParticipant)]
+ public async Task GetSemanticMemoriesAsync(
+ [FromServices] IKernelMemory memoryClient,
+ [FromRoute] string chatId,
+ [FromQuery] string type)
+ {
+ // Sanitize the log input by removing new line characters.
+ // https://github.com/microsoft/chat-copilot/security/code-scanning/1
+ var sanitizedChatId = GetSanitizedParameter(chatId);
+ var sanitizedMemoryType = GetSanitizedParameter(type);
+
+ // Map the requested memoryType to the memory store container name
+ if (!this._promptOptions.TryGetMemoryContainerName(type, out string memoryContainerName))
+ {
+ this._logger.LogWarning("Memory type: {0} is invalid.", sanitizedMemoryType);
+ return this.BadRequest($"Memory type: {sanitizedMemoryType} is invalid.");
+ }
+
+ // Make sure the chat session exists.
+ if (!await this._chatSessionRepository.TryFindByIdAsync(chatId))
+ {
+ this._logger.LogWarning("Chat session: {0} does not exist.", sanitizedChatId);
+ return this.BadRequest($"Chat session: {sanitizedChatId} does not exist.");
+ }
+
+ // Gather the requested kernel memory.
+ // Will use a dummy query since we don't care about relevance.
+ // minRelevanceScore is set to 0.0 to return all memories.
+ List memories = new();
+ try
+ {
+ // Search if there is already a memory item that has a high similarity score with the new item.
+ var filter = new MemoryFilter();
+ filter.ByTag("chatid", chatId);
+ filter.ByTag("memory", memoryContainerName);
+
+ var searchResult =
+ await memoryClient.SearchMemoryAsync(
+ this._promptOptions.MemoryIndexName,
+ "*",
+ relevanceThreshold: 0,
+ resultCount: -1,
+ chatId,
+ memoryContainerName);
+
+ foreach (var memory in searchResult.Results.SelectMany(c => c.Partitions))
+ {
+ memories.Add(memory.Text);
+ }
+ }
+ catch (Exception connectorException) when (!connectorException.IsCriticalException())
+ {
+ // A store exception might be thrown if the collection does not exist, depending on the memory store connector.
+ this._logger.LogError(connectorException, "Cannot search collection {0}", memoryContainerName);
+ }
+
+ return this.Ok(memories);
+ }
+
+ #region Private
+
+ private static string GetSanitizedParameter(string parameterValue)
+ {
+ return parameterValue.Replace(Environment.NewLine, string.Empty, StringComparison.Ordinal);
+ }
+
+ # endregion
+}
diff --git a/webapi/CopilotChat/Controllers/ChatParticipantController.cs b/webapi/Controllers/ChatParticipantController.cs
similarity index 75%
rename from webapi/CopilotChat/Controllers/ChatParticipantController.cs
rename to webapi/Controllers/ChatParticipantController.cs
index b08ff8c1f..cfd7b78bc 100644
--- a/webapi/CopilotChat/Controllers/ChatParticipantController.cs
+++ b/webapi/Controllers/ChatParticipantController.cs
@@ -1,17 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.
-using System;
-using System.Threading.Tasks;
+using CopilotChat.WebApi.Auth;
+using CopilotChat.WebApi.Hubs;
+using CopilotChat.WebApi.Models.Storage;
+using CopilotChat.WebApi.Storage;
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;
+namespace CopilotChat.WebApi.Controllers;
///
/// Controller for managing invitations and participants in a chat session.
@@ -21,7 +18,6 @@ namespace SemanticKernel.Service.CopilotChat.Controllers;
/// 3. Managing participants in a chat session.
///
[ApiController]
-[Authorize]
public class ChatParticipantController : ControllerBase
{
private const string UserJoinedClientCall = "UserJoined";
@@ -46,39 +42,41 @@ public ChatParticipantController(
}
///
- /// Join a use to a chat session given a chat id and a user id.
+ /// Join the logged in user to a chat session given a chat ID.
///
/// Message Hub that performs the real time relay service.
- /// Contains the user id and chat id.
+ /// The auth info for the current request.
+ /// The ID of the chat to join.
[HttpPost]
- [Route("chatParticipant/join")]
+ [Route("chats/{chatId:guid}/participants")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task JoinChatAsync(
[FromServices] IHubContext messageRelayHubContext,
- [FromBody] ChatParticipant chatParticipantParam)
+ [FromServices] IAuthInfo authInfo,
+ [FromRoute] Guid chatId)
{
- string userId = chatParticipantParam.UserId;
- string chatId = chatParticipantParam.ChatId;
+ string userId = authInfo.UserId;
// Make sure the chat session exists.
- if (!await this._chatSessionRepository.TryFindByIdAsync(chatId, v => _ = v))
+ if (!await this._chatSessionRepository.TryFindByIdAsync(chatId.ToString()))
{
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))
+ if (await this._chatParticipantRepository.IsUserInChatAsync(userId, chatId.ToString()))
{
- return this.BadRequest("User is already in the chat session.");
+ return this.Conflict("User is already in the chat session.");
}
- var chatParticipant = new ChatParticipant(userId, chatId);
+ var chatParticipant = new ChatParticipant(userId, chatId.ToString());
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);
+ await messageRelayHubContext.Clients.Group(chatId.ToString()).SendAsync(UserJoinedClientCall, chatId, userId);
return this.Ok(chatParticipant);
}
@@ -86,20 +84,22 @@ public async Task JoinChatAsync(
///
/// Get a list of chat participants that have the same chat id.
///
- /// The Id of the chat to get all the participants from.
+ /// The ID of the chat to get all the participants from.
[HttpGet]
- [Route("chatParticipant/getAllParticipants/{chatId:guid}")]
+ [Route("chats/{chatId:guid}/participants")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Authorize(Policy = AuthPolicyName.RequireChatParticipant)]
public async Task GetAllParticipantsAsync(Guid chatId)
{
// Make sure the chat session exists.
- if (!await this._chatSessionRepository.TryFindByIdAsync(chatId.ToString(), v => _ = v))
+ if (!await this._chatSessionRepository.TryFindByIdAsync(chatId.ToString()))
{
return this.NotFound("Chat session does not exist.");
}
var chatParticipants = await this._chatParticipantRepository.FindByChatIdAsync(chatId.ToString());
+
return this.Ok(chatParticipants);
}
}
diff --git a/webapi/Controllers/DocumentController.cs b/webapi/Controllers/DocumentController.cs
new file mode 100644
index 000000000..f70ecf1bb
--- /dev/null
+++ b/webapi/Controllers/DocumentController.cs
@@ -0,0 +1,502 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Globalization;
+using CopilotChat.WebApi.Auth;
+using CopilotChat.WebApi.Extensions;
+using CopilotChat.WebApi.Hubs;
+using CopilotChat.WebApi.Models.Request;
+using CopilotChat.WebApi.Models.Response;
+using CopilotChat.WebApi.Models.Storage;
+using CopilotChat.WebApi.Options;
+using CopilotChat.WebApi.Services;
+using CopilotChat.WebApi.Storage;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.Options;
+using Microsoft.KernelMemory;
+
+namespace CopilotChat.WebApi.Controllers;
+
+///
+/// Controller for importing documents.
+///
+///
+/// This controller is responsible for contracts that are not possible to fulfill by kernel memory components.
+///
+[ApiController]
+public class DocumentController : ControllerBase
+{
+ private const string GlobalDocumentUploadedClientCall = "GlobalDocumentUploaded";
+ private const string ReceiveMessageClientCall = "ReceiveMessage";
+
+ private readonly ILogger _logger;
+ private readonly PromptsOptions _promptOptions;
+ private readonly DocumentMemoryOptions _options;
+ private readonly ContentSafetyOptions _contentSafetyOptions;
+ private readonly ChatSessionRepository _sessionRepository;
+ private readonly ChatMemorySourceRepository _sourceRepository;
+ private readonly ChatMessageRepository _messageRepository;
+ private readonly ChatParticipantRepository _participantRepository;
+ private readonly DocumentTypeProvider _documentTypeProvider;
+ private readonly IAuthInfo _authInfo;
+ private readonly IContentSafetyService _contentSafetyService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DocumentController(
+ ILogger logger,
+ IAuthInfo authInfo,
+ IOptions documentMemoryOptions,
+ IOptions promptOptions,
+ IOptions contentSafetyOptions,
+ ChatSessionRepository sessionRepository,
+ ChatMemorySourceRepository sourceRepository,
+ ChatMessageRepository messageRepository,
+ ChatParticipantRepository participantRepository,
+ DocumentTypeProvider documentTypeProvider,
+ IContentSafetyService contentSafetyService)
+ {
+ this._logger = logger;
+ this._options = documentMemoryOptions.Value;
+ this._promptOptions = promptOptions.Value;
+ this._contentSafetyOptions = contentSafetyOptions.Value;
+ this._sessionRepository = sessionRepository;
+ this._sourceRepository = sourceRepository;
+ this._messageRepository = messageRepository;
+ this._participantRepository = participantRepository;
+ this._documentTypeProvider = documentTypeProvider;
+ this._authInfo = authInfo;
+ this._contentSafetyService = contentSafetyService;
+ }
+
+ ///
+ /// Service API for importing a document.
+ /// Documents imported through this route will be considered as global documents.
+ ///
+ [Route("documents")]
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public Task DocumentImportAsync(
+ [FromServices] IKernelMemory memoryClient,
+ [FromServices] IHubContext messageRelayHubContext,
+ [FromForm] DocumentImportForm documentImportForm)
+ {
+ return this.DocumentImportAsync(
+ memoryClient,
+ messageRelayHubContext,
+ DocumentScopes.Global,
+ DocumentMemoryOptions.GlobalDocumentChatId,
+ documentImportForm
+ );
+ }
+
+ ///
+ /// Service API for importing a document.
+ ///
+ [Route("chats/{chatId}/documents")]
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public Task DocumentImportAsync(
+ [FromServices] IKernelMemory memoryClient,
+ [FromServices] IHubContext messageRelayHubContext,
+ [FromRoute] Guid chatId,
+ [FromForm] DocumentImportForm documentImportForm)
+ {
+ return this.DocumentImportAsync(
+ memoryClient,
+ messageRelayHubContext,
+ DocumentScopes.Chat,
+ chatId,
+ documentImportForm);
+ }
+
+ private async Task DocumentImportAsync(
+ IKernelMemory memoryClient,
+ IHubContext messageRelayHubContext,
+ DocumentScopes documentScope,
+ Guid chatId,
+ DocumentImportForm documentImportForm)
+ {
+ try
+ {
+ await this.ValidateDocumentImportFormAsync(chatId, documentScope, documentImportForm);
+ }
+ catch (ArgumentException ex)
+ {
+ return this.BadRequest(ex.Message);
+ }
+
+ this._logger.LogInformation("Importing {0} document(s)...", documentImportForm.FormFiles.Count());
+
+ // Pre-create chat-message
+ DocumentMessageContent documentMessageContent = new();
+
+ var importResults = await this.ImportDocumentsAsync(memoryClient, chatId, documentImportForm, documentMessageContent);
+
+ var chatMessage = await this.TryCreateDocumentUploadMessageAsync(chatId, documentMessageContent);
+
+ if (chatMessage == null)
+ {
+ this._logger.LogWarning("Failed to create document upload message - {Content}", documentMessageContent.ToString());
+ return this.BadRequest();
+ }
+
+ // Broadcast the document uploaded event to other users.
+ if (documentScope == DocumentScopes.Chat)
+ {
+ // If chat message isn't created, it is still broadcast and visible in the documents tab.
+ // The chat message won't, however, be displayed when the chat is freshly rendered.
+
+ var userId = this._authInfo.UserId;
+ await messageRelayHubContext.Clients.Group(chatId.ToString())
+ .SendAsync(ReceiveMessageClientCall, chatId, userId, chatMessage);
+
+ this._logger.LogInformation("Local upload chat message: {0}", chatMessage.ToString());
+
+ return this.Ok(chatMessage);
+ }
+
+ await messageRelayHubContext.Clients.All.SendAsync(
+ GlobalDocumentUploadedClientCall,
+ documentMessageContent.ToFormattedStringNamesOnly(),
+ this._authInfo.Name
+ );
+
+ this._logger.LogInformation("Global upload chat message: {0}", chatMessage.ToString());
+
+ return this.Ok(chatMessage);
+ }
+
+ private async Task> ImportDocumentsAsync(IKernelMemory memoryClient, Guid chatId, DocumentImportForm documentImportForm, DocumentMessageContent messageContent)
+ {
+ IEnumerable importResults = new List();
+
+ await Task.WhenAll(
+ documentImportForm.FormFiles.Select(
+ async formFile =>
+ await this.ImportDocumentAsync(formFile, memoryClient, chatId).ContinueWith(
+ task =>
+ {
+ var importResult = task.Result;
+ if (importResult != null)
+ {
+ messageContent.AddDocument(
+ formFile.FileName,
+ this.GetReadableByteString(formFile.Length),
+ importResult.IsSuccessful);
+
+ importResults = importResults.Append(importResult);
+ }
+ },
+ TaskScheduler.Default)));
+
+ return importResults.ToArray();
+ }
+
+ private async Task ImportDocumentAsync(IFormFile formFile, IKernelMemory memoryClient, Guid chatId)
+ {
+ this._logger.LogInformation("Importing document {0}", formFile.FileName);
+
+ // Create memory source
+ MemorySource memorySource = new(
+ chatId.ToString(),
+ formFile.FileName,
+ this._authInfo.UserId,
+ MemorySourceType.File,
+ formFile.Length,
+ hyperlink: null
+ );
+
+ if (!(await this.TryUpsertMemorySourceAsync(memorySource)))
+ {
+ this._logger.LogDebug("Failed to upsert memory source for file {0}.", formFile.FileName);
+
+ return ImportResult.Fail;
+ }
+
+ if (!(await TryStoreMemoryAsync()))
+ {
+ await this.TryRemoveMemoryAsync(memorySource);
+ }
+
+ return new ImportResult(memorySource.Id);
+
+ async Task TryStoreMemoryAsync()
+ {
+ try
+ {
+ using var stream = formFile.OpenReadStream();
+ await memoryClient.StoreDocumentAsync(
+ this._promptOptions.MemoryIndexName,
+ memorySource.Id,
+ chatId.ToString(),
+ this._promptOptions.DocumentMemoryName,
+ formFile.FileName,
+ stream);
+
+ return true;
+ }
+ catch (Exception ex) when (ex is not SystemException)
+ {
+ return false;
+ }
+ }
+ }
+
+ #region Private
+
+ ///
+ /// A class to store a document import results.
+ ///
+ private sealed class ImportResult
+ {
+ ///
+ /// A boolean indicating whether the import is successful.
+ ///
+ public bool IsSuccessful => !string.IsNullOrWhiteSpace(this.CollectionName);
+
+ ///
+ /// The name of the collection that the document is inserted to.
+ ///
+ public string CollectionName { get; set; }
+
+ ///
+ /// Create a new instance of the class.
+ ///
+ /// The name of the collection that the document is inserted to.
+ public ImportResult(string collectionName)
+ {
+ this.CollectionName = collectionName;
+ }
+
+ ///
+ /// Create a new instance of the class representing a failed import.
+ ///
+ public static ImportResult Fail { get; } = new(string.Empty);
+ }
+
+ ///
+ /// Validates the document import form.
+ ///
+ /// The document import form.
+ ///
+ /// Throws ArgumentException if validation fails.
+ private async Task ValidateDocumentImportFormAsync(Guid chatId, DocumentScopes scope, DocumentImportForm documentImportForm)
+ {
+ // Make sure the user has access to the chat session if the document is uploaded to a chat session.
+ if (scope == DocumentScopes.Chat
+ && !(await this.UserHasAccessToChatAsync(this._authInfo.UserId, 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 = Path.GetExtension(formFile.FileName);
+ if (!this._documentTypeProvider.IsSupported(fileType, out bool isSafetyTarget))
+ {
+ throw new ArgumentException($"Unsupported file type: {fileType}");
+ }
+
+ if (isSafetyTarget && documentImportForm.UseContentSafety)
+ {
+ if (!this._contentSafetyOptions.Enabled)
+ {
+ throw new ArgumentException("Unable to analyze image. Content Safety is currently disabled in the backend.");
+ }
+
+ var violations = new List();
+ try
+ {
+ // Call the content safety controller to analyze the image
+ var imageAnalysisResponse = await this._contentSafetyService.ImageAnalysisAsync(formFile, default);
+ violations = this._contentSafetyService.ParseViolatedCategories(imageAnalysisResponse, this._contentSafetyOptions.ViolationThreshold);
+ }
+ catch (Exception ex) when (!ex.IsCriticalException())
+ {
+ this._logger.LogError(ex, "Failed to analyze image {0} with Content Safety. Details: {{1}}", formFile.FileName, ex.Message);
+ throw new AggregateException($"Failed to analyze image {formFile.FileName} with Content Safety.", ex);
+ }
+
+ if (violations.Count > 0)
+ {
+ throw new ArgumentException($"Unable to upload image {formFile.FileName}. Detected undesirable content with potential risk: {string.Join(", ", violations)}");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Validates the document import form.
+ ///
+ /// The document import form.
+ ///
+ /// Throws ArgumentException if validation fails.
+ private async Task ValidateDocumentStatusFormAsync(DocumentStatusForm documentStatusForm)
+ {
+ // Make sure the user has access to the chat session if the document is uploaded to a chat session.
+ if (documentStatusForm.DocumentScope == DocumentScopes.Chat
+ && !(await this.UserHasAccessToChatAsync(documentStatusForm.UserId, documentStatusForm.ChatId)))
+ {
+ throw new ArgumentException("User does not have access to the chat session.");
+ }
+
+ var fileReferences = documentStatusForm.FileReferences;
+
+ if (!fileReferences.Any())
+ {
+ throw new ArgumentException("No files identified.");
+ }
+ else if (fileReferences.Count() > this._options.FileCountLimit)
+ {
+ throw new ArgumentException($"Too many files requested. Max file count is {this._options.FileCountLimit}.");
+ }
+
+ // Loop through the uploaded files and validate them before importing.
+ foreach (var fileReference in fileReferences)
+ {
+ if (string.IsNullOrWhiteSpace(fileReference))
+ {
+ throw new ArgumentException($"File {fileReference} is empty.");
+ }
+ }
+ }
+
+ ///
+ /// Try to upsert a memory source.
+ ///
+ /// The memory source to be uploaded
+ /// True if upsert is successful. False otherwise.
+ private async Task TryUpsertMemorySourceAsync(MemorySource memorySource)
+ {
+ try
+ {
+ await this._sourceRepository.UpsertAsync(memorySource);
+ return true;
+ }
+ catch (Exception ex) when (ex is not SystemException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Try to upsert a memory source.
+ ///
+ /// The memory source to be uploaded
+ /// True if upsert is successful. False otherwise.
+ private async Task TryRemoveMemoryAsync(MemorySource memorySource)
+ {
+ try
+ {
+ await this._sourceRepository.DeleteAsync(memorySource);
+ return true;
+ }
+ catch (Exception ex) when (ex is ArgumentOutOfRangeException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Try to upsert a memory source.
+ ///
+ /// The memory source to be uploaded
+ /// True if upsert is successful. False otherwise.
+ private async Task TryStoreMemoryAsync(MemorySource memorySource)
+ {
+ try
+ {
+ await this._sourceRepository.UpsertAsync(memorySource);
+ return true;
+ }
+ catch (Exception ex) when (ex is ArgumentOutOfRangeException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Try to create a chat message that represents document upload.
+ ///
+ /// The target chat-id
+ /// The document message content
+ /// A ChatMessage object if successful, null otherwise
+ private async Task TryCreateDocumentUploadMessageAsync(
+ Guid chatId,
+ DocumentMessageContent messageContent)
+ {
+ var chatMessage = CopilotChatMessage.CreateDocumentMessage(
+ this._authInfo.UserId,
+ this._authInfo.Name, // User name
+ chatId.ToString(),
+ messageContent);
+
+ try
+ {
+ await this._messageRepository.CreateAsync(chatMessage);
+ return chatMessage;
+ }
+ catch (Exception ex) when (ex is ArgumentOutOfRangeException)
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Converts a `long` byte count to a human-readable string.
+ ///
+ /// Byte count
+ /// Human-readable string of bytes
+ 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]);
+ }
+
+ ///
+ /// Check if the user has access to the chat session.
+ ///
+ /// The user ID.
+ /// The chat session ID.
+ /// A boolean indicating whether the user has access to the chat session.
+ private async Task UserHasAccessToChatAsync(string userId, Guid chatId)
+ {
+ return await this._participantRepository.IsUserInChatAsync(userId, chatId.ToString());
+ }
+
+ #endregion
+}
diff --git a/webapi/Controllers/MaintenanceController.cs b/webapi/Controllers/MaintenanceController.cs
new file mode 100644
index 000000000..551b3689e
--- /dev/null
+++ b/webapi/Controllers/MaintenanceController.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using CopilotChat.WebApi.Models.Response;
+using CopilotChat.WebApi.Options;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+
+namespace CopilotChat.WebApi.Controllers;
+
+///
+/// Controller for reporting the status of chat migration.
+///
+[ApiController]
+public class MaintenanceController : ControllerBase
+{
+ internal const string GlobalSiteMaintenance = "GlobalSiteMaintenance";
+
+ private readonly ILogger _logger;
+ private readonly IOptions _serviceOptions;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MaintenanceController(
+ ILogger logger,
+ IOptions serviceOptions)
+ {
+ this._logger = logger;
+ this._serviceOptions = serviceOptions;
+ }
+
+ ///
+ /// Route for reporting the status of site maintenance.
+ ///
+ [Route("maintenanceStatus")]
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public ActionResult GetMaintenanceStatusAsync(
+ CancellationToken cancellationToken = default)
+ {
+ MaintenanceResult? result = null;
+
+ if (this._serviceOptions.Value.InMaintenance)
+ {
+ result = new MaintenanceResult(); // Default maintenance message
+ }
+
+ if (result != null)
+ {
+ return this.Ok(result);
+ }
+
+ return this.Ok();
+ }
+}
diff --git a/webapi/Controllers/PluginController.cs b/webapi/Controllers/PluginController.cs
new file mode 100644
index 000000000..f7d48a700
--- /dev/null
+++ b/webapi/Controllers/PluginController.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using CopilotChat.WebApi.Auth;
+using CopilotChat.WebApi.Hubs;
+using CopilotChat.WebApi.Models.Storage;
+using CopilotChat.WebApi.Options;
+using CopilotChat.WebApi.Storage;
+using CopilotChat.WebApi.Utilities;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.SignalR;
+
+namespace CopilotChat.WebApi.Controllers;
+
+///
+/// Controller responsible for returning the service options to the client.
+///
+[ApiController]
+public class PluginController : ControllerBase
+{
+ private const string PluginStateChanged = "PluginStateChanged";
+ private readonly ILogger _logger;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly IDictionary _availablePlugins;
+ private readonly ChatSessionRepository _sessionRepository;
+
+ public PluginController(
+ ILogger logger,
+ IHttpClientFactory httpClientFactory,
+ IDictionary availablePlugins,
+ ChatSessionRepository sessionRepository)
+ {
+ this._logger = logger;
+ this._httpClientFactory = httpClientFactory;
+ this._availablePlugins = availablePlugins;
+ this._sessionRepository = sessionRepository;
+ }
+
+ ///
+ /// Fetches a plugin's manifest.
+ ///
+ /// The domain of the manifest.
+ /// The plugin's manifest JSON.
+ [HttpGet]
+ [Route("pluginManifests")] // TODO: Fix name and test - use singular?
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task GetPluginManifestAsync([FromQuery] Uri manifestDomain)
+ {
+ using var request = new HttpRequestMessage(HttpMethod.Get, PluginUtils.GetPluginManifestUri(manifestDomain));
+ // Need to set the user agent to avoid 403s from some sites.
+ request.Headers.Add("User-Agent", "Semantic-Kernel");
+
+ using HttpClient client = this._httpClientFactory.CreateClient();
+ var response = await client.SendAsync(request);
+ if (!response.IsSuccessStatusCode)
+ {
+ return this.StatusCode((int)response.StatusCode, await response.Content.ReadAsStringAsync());
+ }
+
+ return this.Ok(await response.Content.ReadAsStringAsync());
+ }
+
+ ///
+ /// Enable or disable a plugin for a chat session.
+ ///
+ [HttpPut]
+ [Route("chats/{chatId:guid}/plugins/{pluginName}/{enabled:bool}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [Authorize(Policy = AuthPolicyName.RequireChatParticipant)]
+ public async Task SetPluginStateAsync(
+ [FromServices] IHubContext messageRelayHubContext,
+ Guid chatId,
+ string pluginName,
+ bool enabled)
+ {
+ if (!this._availablePlugins.ContainsKey(pluginName))
+ {
+ return this.NotFound("Plugin not found.");
+ }
+
+ var chatIdString = chatId.ToString();
+ ChatSession? chat = null;
+#pragma warning disable CA1508 // Avoid dead conditional code. It's giving out false positives on chat == null.
+ if (!(await this._sessionRepository.TryFindByIdAsync(chatIdString, callback: v => chat = v)) || chat == null)
+ {
+ return this.NotFound("Chat not found.");
+ }
+
+ if (enabled)
+ {
+ chat.EnabledPlugins.Add(pluginName);
+ }
+ else
+ {
+ chat.EnabledPlugins.Remove(pluginName);
+ }
+
+ await this._sessionRepository.UpsertAsync(chat);
+ await messageRelayHubContext.Clients.Group(chatIdString).SendAsync(PluginStateChanged, chatIdString, pluginName, enabled);
+
+ return this.NoContent();
+ }
+}
diff --git a/webapi/Controllers/ServiceInfoController.cs b/webapi/Controllers/ServiceInfoController.cs
new file mode 100644
index 000000000..a00c785da
--- /dev/null
+++ b/webapi/Controllers/ServiceInfoController.cs
@@ -0,0 +1,119 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics;
+using System.Reflection;
+using CopilotChat.WebApi.Models.Response;
+using CopilotChat.WebApi.Options;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using Microsoft.KernelMemory;
+
+namespace CopilotChat.WebApi.Controllers;
+
+///
+/// Controller responsible for returning information on the service.
+///
+[ApiController]
+public class ServiceInfoController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private readonly IConfiguration _configuration;
+ private readonly KernelMemoryConfig _memoryOptions;
+ private readonly ChatAuthenticationOptions _chatAuthenticationOptions;
+ private readonly FrontendOptions _frontendOptions;
+ private readonly IEnumerable _availablePlugins;
+ private readonly ContentSafetyOptions _contentSafetyOptions;
+
+ public ServiceInfoController(
+ ILogger logger,
+ IConfiguration configuration,
+ IOptions memoryOptions,
+ IOptions chatAuthenticationOptions,
+ IOptions frontendOptions,
+ IDictionary availablePlugins,
+ IOptions contentSafetyOptions)
+ {
+ this._logger = logger;
+ this._configuration = configuration;
+ this._memoryOptions = memoryOptions.Value;
+ this._chatAuthenticationOptions = chatAuthenticationOptions.Value;
+ this._frontendOptions = frontendOptions.Value;
+ this._availablePlugins = this.SanitizePlugins(availablePlugins);
+ this._contentSafetyOptions = contentSafetyOptions.Value;
+ }
+
+ ///
+ /// Return information on running service.
+ ///
+ [Route("info")]
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public IActionResult GetServiceInfo()
+ {
+ var response = new ServiceInfoResponse()
+ {
+ MemoryStore = new MemoryStoreInfoResponse()
+ {
+ Types = Enum.GetNames(typeof(MemoryStoreType)),
+ SelectedType = this._memoryOptions.GetMemoryStoreType(this._configuration).ToString(),
+ },
+ AvailablePlugins = this._availablePlugins,
+ Version = GetAssemblyFileVersion(),
+ IsContentSafetyEnabled = this._contentSafetyOptions.Enabled
+ };
+
+ return this.Ok(response);
+ }
+
+ ///
+ /// Return the auth config to be used by the frontend client to access this service.
+ ///
+ [Route("authConfig")]
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [AllowAnonymous]
+ public IActionResult GetAuthConfig()
+ {
+ string authorityUriString = string.Empty;
+ if (!string.IsNullOrEmpty(this._chatAuthenticationOptions.AzureAd!.Instance) &&
+ !string.IsNullOrEmpty(this._chatAuthenticationOptions.AzureAd!.TenantId))
+ {
+ var authorityUri = new Uri(this._chatAuthenticationOptions.AzureAd!.Instance);
+ authorityUri = new Uri(authorityUri, this._chatAuthenticationOptions.AzureAd!.TenantId);
+ authorityUriString = authorityUri.ToString();
+ }
+
+ var config = new FrontendAuthConfig
+ {
+ AuthType = this._chatAuthenticationOptions.Type.ToString(),
+ AadAuthority = authorityUriString,
+ AadClientId = this._frontendOptions.AadClientId,
+ AadApiScope = $"api://{this._chatAuthenticationOptions.AzureAd!.ClientId}/{this._chatAuthenticationOptions.AzureAd!.Scopes}",
+ };
+
+ return this.Ok(config);
+ }
+
+ private static string GetAssemblyFileVersion()
+ {
+ Assembly assembly = Assembly.GetExecutingAssembly();
+ FileVersionInfo fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location);
+
+ return fileVersion.FileVersion ?? string.Empty;
+ }
+
+ ///
+ /// Sanitize the plugins to only return the name and url.
+ ///
+ /// The plugins to sanitize.
+ ///
+ private IEnumerable SanitizePlugins(IDictionary plugins)
+ {
+ return plugins.Select(p => new Plugin()
+ {
+ Name = p.Value.Name,
+ ManifestDomain = p.Value.ManifestDomain,
+ });
+ }
+}
diff --git a/webapi/CopilotChat/Controllers/SpeechTokenController.cs b/webapi/Controllers/SpeechTokenController.cs
similarity index 70%
rename from webapi/CopilotChat/Controllers/SpeechTokenController.cs
rename to webapi/Controllers/SpeechTokenController.cs
index 8dea7b3b9..2fb25fb84 100644
--- a/webapi/CopilotChat/Controllers/SpeechTokenController.cs
+++ b/webapi/Controllers/SpeechTokenController.cs
@@ -1,20 +1,13 @@
// 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 CopilotChat.WebApi.Models.Response;
+using CopilotChat.WebApi.Options;
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;
+namespace CopilotChat.WebApi.Controllers;
-[Authorize]
[ApiController]
public class SpeechTokenController : ControllerBase
{
@@ -25,11 +18,15 @@ private sealed class TokenResult
}
private readonly ILogger _logger;
+ private readonly IHttpClientFactory _httpClientFactory;
private readonly AzureSpeechOptions _options;
- public SpeechTokenController(IOptions options, ILogger logger)
+ public SpeechTokenController(IOptions options,
+ ILogger logger,
+ IHttpClientFactory httpClientFactory)
{
this._logger = logger;
+ this._httpClientFactory = httpClientFactory;
this._options = options.Value;
}
@@ -57,20 +54,20 @@ public async Task> GetAsync()
private async Task 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);
+ using var client = this._httpClientFactory.CreateClient();
- var result = await client.PostAsync(uriBuilder.Uri, null);
+ using var request = new HttpRequestMessage(HttpMethod.Post, fetchUri);
+ request.Headers.Add("Ocp-Apim-Subscription-Key", subscriptionKey);
+
+ var result = await client.SendAsync(request);
if (result.IsSuccessStatusCode)
{
var response = result.EnsureSuccessStatusCode();
- this._logger.LogDebug("Token Uri: {0}", uriBuilder.Uri.AbsoluteUri);
+ this._logger.LogDebug("Token Uri: {0}", fetchUri);
string token = await result.Content.ReadAsStringAsync();
return new TokenResult { Token = token, ResponseCode = response.StatusCode };
}
- return new TokenResult { Token = "", ResponseCode = HttpStatusCode.NotFound };
+ return new TokenResult { ResponseCode = HttpStatusCode.NotFound };
}
}
diff --git a/webapi/CopilotChat/Controllers/BotController.cs b/webapi/CopilotChat/Controllers/BotController.cs
deleted file mode 100644
index a9ab3ac76..000000000
--- a/webapi/CopilotChat/Controllers/BotController.cs
+++ /dev/null
@@ -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 _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;
-
- ///
- /// The constructor of BotController.
- ///
- /// 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.
- ///
- /// The chat session repository.
- /// The chat message repository.
- /// The chat participant repository.
- /// The AI service options where we need the embedding settings from.
- /// The bot schema options.
- /// The document memory options.
- /// The logger.
- public BotController(
- OptionalIMemoryStore optionalIMemoryStore,
- ISemanticTextMemory semanticMemory,
- ChatSessionRepository chatRepository,
- ChatMessageRepository chatMessageRepository,
- ChatParticipantRepository chatParticipantRepository,
- IOptions aiServiceOptions,
- IOptions botSchemaOptions,
- IOptions documentMemoryOptions,
- ILogger 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;
- }
-
- ///
- /// Upload a bot.
- ///
- /// The Semantic Kernel instance.
- /// The user id.
- /// The bot object from the message body
- /// The cancellation token.
- /// The HTTP action result with new chat session object.
- [Authorize]
- [HttpPost]
- [Route("bot/upload")]
- [ProducesResponseType(StatusCodes.Status201Created)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task> 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);
- }
-
- ///
- /// Download a bot.
- ///
- /// The Semantic Kernel instance.
- /// The chat id to be downloaded.
- /// The serialized Bot object of the chat id.
- [Authorize]
- [HttpGet]
- [ActionName("DownloadAsync")]
- [Route("bot/download/{chatId:guid}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task> 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);
- }
-
- ///
- /// Check if an external bot file is compatible with the application.
- ///
- ///
- /// If the embeddings are not generated from the same model, the bot file is not compatible.
- ///
- /// The external bot schema.
- /// The external bot embedding configuration.
- /// The embedding options.
- /// The bot schema options.
- /// True if the bot file is compatible with the app; otherwise false.
- 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);
- }
-
- ///
- /// 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.
- ///
- /// The Semantic Kernel instance.
- /// The current collection name. Used to query the memory storage.
- /// The embeddings list where we will append the fetched memory records.
- ///
- /// The new collection name when appends to the embeddings list. Will use the old collection name if not provided.
- ///
- private static async Task GetMemoryRecordsAndAppendToEmbeddingsAsync(
- IKernel kernel,
- string collectionName,
- List>> embeddings,
- string newCollectionName = "")
- {
- List 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.IsNullOrEmpty(newCollectionName) ? collectionName : newCollectionName,
- collectionMemoryRecords));
- }
-
- ///
- /// Prepare the bot information of a given chat.
- ///
- /// The semantic kernel object.
- /// The chat id of the bot
- /// A Bot object that represents the chat session.
- private async Task 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;
- }
-
- ///
- /// Get chat messages of a given chat id.
- ///
- /// The chat id
- /// The list of chat messages in descending order of the timestamp
- private async Task> 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();
- }
-
- ///
- /// Bulk upsert memory records into memory store.
- ///
- /// The original chat id of the memory records.
- /// The new chat id that will replace the original chat id.
- /// The list of embeddings of the chat id.
- /// The function doesn't return anything.
- private async Task BulkUpsertMemoryRecordsAsync(string oldChatId, string chatId, List>> 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);
- }
- }
- }
- }
- }
-}
diff --git a/webapi/CopilotChat/Controllers/ChatController.cs b/webapi/CopilotChat/Controllers/ChatController.cs
deleted file mode 100644
index ae4d795f2..000000000
--- a/webapi/CopilotChat/Controllers/ChatController.cs
+++ /dev/null
@@ -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;
-
-///
-/// Controller responsible for handling chat messages and responses.
-///
-[ApiController]
-public class ChatController : ControllerBase, IDisposable
-{
- private readonly ILogger _logger;
- private readonly List _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 logger, ITelemetryService telemetryService)
- {
- this._logger = logger;
- this._telemetryService = telemetryService;
- this._disposables = new List();
- }
-
- ///
- /// Invokes the chat skill to get a response from the bot.
- ///
- /// Semantic kernel obtained through dependency injection.
- /// Message Hub that performs the real time relay service.
- /// Planner to use to create function sequences.
- /// Prompt along with its parameters.
- /// Authentication headers to connect to OpenAPI Skills.
- /// Results containing the response from the model.
- [Authorize]
- [Route("chat")]
- [HttpPost]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task ChatAsync(
- [FromServices] IKernel kernel,
- [FromServices] IHubContext 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(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);
- }
-
- ///
- /// Register skills with the planner's kernel.
- ///
- 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");
- }
- }
-
- ///
- /// Create a Microsoft Graph service client.
- ///
- /// The delegate to authenticate the request.
- private GraphServiceClient CreateGraphServiceClient(AuthenticateRequestAsyncDelegate authenticateRequestAsyncDelegate)
- {
- MsGraphClientLoggingHandler graphLoggingHandler = new(this._logger);
- this._disposables.Add(graphLoggingHandler);
-
- IList graphMiddlewareHandlers =
- GraphClientFactory.CreateDefaultHandlers(new DelegateAuthenticationProvider(authenticateRequestAsyncDelegate));
- graphMiddlewareHandlers.Add(graphLoggingHandler);
-
- HttpClient graphHttpClient = GraphClientFactory.Create(graphMiddlewareHandlers);
- this._disposables.Add(graphHttpClient);
-
- GraphServiceClient graphServiceClient = new(graphHttpClient);
- return graphServiceClient;
- }
-
- ///
- /// Dispose of the object.
- ///
- protected virtual void Dispose(bool disposing)
- {
- if (disposing)
- {
- foreach (IDisposable disposable in this._disposables)
- {
- disposable.Dispose();
- }
- }
- }
-
- ///
- public void Dispose()
- {
- // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
- this.Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
-}
diff --git a/webapi/CopilotChat/Controllers/ChatHistoryController.cs b/webapi/CopilotChat/Controllers/ChatHistoryController.cs
deleted file mode 100644
index 8f911fec4..000000000
--- a/webapi/CopilotChat/Controllers/ChatHistoryController.cs
+++ /dev/null
@@ -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;
-
-///
-/// Controller for chat history.
-/// This controller is responsible for creating new chat sessions, retrieving chat sessions,
-/// retrieving chat messages, and editing chat sessions.
-///
-[ApiController]
-[Authorize]
-public class ChatHistoryController : ControllerBase
-{
- private readonly ILogger _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";
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The chat session repository.
- /// The chat message repository.
- /// The chat participant repository.
- /// The chat memory resource repository.
- /// The prompts options.
- public ChatHistoryController(
- ILogger logger,
- ChatSessionRepository sessionRepository,
- ChatMessageRepository messageRepository,
- ChatParticipantRepository participantRepository,
- ChatMemorySourceRepository sourceRepository,
- IOptions promptsOptions)
- {
- this._logger = logger;
- this._sessionRepository = sessionRepository;
- this._messageRepository = messageRepository;
- this._participantRepository = participantRepository;
- this._sourceRepository = sourceRepository;
- this._promptOptions = promptsOptions.Value;
- }
-
- ///
- /// Create a new chat session and populate the session with the initial bot message.
- ///
- /// Contains the title of the chat.
- /// The HTTP action result.
- [HttpPost]
- [Route("chatSession/create")]
- [ProducesResponseType(StatusCodes.Status201Created)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task 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);
- }
-
- ///
- /// Get a chat session by id.
- ///
- /// The chat id.
- [HttpGet]
- [ActionName("GetChatSessionByIdAsync")]
- [Route("chatSession/getChat/{chatId:guid}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task 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}'.");
- }
-
- ///
- /// 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.
- ///
- /// The user id.
- /// A list of chat sessions. An empty list if the user is not in any chat session.
- [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 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();
- 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);
- }
-
- ///
- /// Get all chat messages for a chat session.
- /// The list will be ordered with the first entry being the most recent message.
- ///
- /// The chat id.
- /// The start index at which the first message will be returned.
- /// The number of messages to return. -1 will return all messages starting from startIdx.
- /// [Authorize]
- [HttpGet]
- [Route("chatSession/getChatMessages/{chatId:guid}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task 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);
- }
-
- ///
- /// Edit a chat session.
- ///
- /// Object that contains the parameters to edit the chat.
- [HttpPost]
- [Route("chatSession/edit")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task EditChatSessionAsync(
- [FromServices] IHubContext 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}'.");
- }
-
- ///
- /// Service API to get a list of imported sources.
- ///
- [Authorize]
- [Route("chatSession/{chatId:guid}/sources")]
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- public async Task>> 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}'.");
- }
-}
diff --git a/webapi/CopilotChat/Controllers/DocumentImportController.cs b/webapi/CopilotChat/Controllers/DocumentImportController.cs
deleted file mode 100644
index d53020553..000000000
--- a/webapi/CopilotChat/Controllers/DocumentImportController.cs
+++ /dev/null
@@ -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;
-
-///
-/// Controller for importing documents.
-///
-[ApiController]
-public class DocumentImportController : ControllerBase
-{
- ///
- /// Supported file types for import.
- ///
- private enum SupportedFileType
- {
- ///
- /// .txt
- ///
- Txt,
-
- ///
- /// .pdf
- ///
- Pdf,
-
- ///
- /// .jpg
- ///
- Jpg,
-
- ///
- /// .png
- ///
- Png,
-
- ///
- /// .tif or .tiff
- ///
- Tiff
- };
-
- private readonly ILogger _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;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public DocumentImportController(
- ILogger logger,
- IOptions 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;
- }
-
- ///
- /// Service API for importing a document.
- ///
- [Authorize]
- [Route("importDocuments")]
- [HttpPost]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task ImportDocumentsAsync(
- [FromServices] IKernel kernel,
- [FromServices] IHubContext 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 importResults = new List();
- 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
-
- ///
- /// A class to store a document import results.
- ///
- private sealed class ImportResult
- {
- ///
- /// A boolean indicating whether the import is successful.
- ///
- public bool IsSuccessful => this.Keys.Any();
-
- ///
- /// The name of the collection that the document is inserted to.
- ///
- public string CollectionName { get; set; }
-
- ///
- /// The keys of the inserted document chunks.
- ///
- public IEnumerable Keys { get; set; } = new List();
-
- ///
- /// Create a new instance of the