From e9acc9791405372311bd51ef0b48fc8ac30e95d4 Mon Sep 17 00:00:00 2001 From: Phil Jirsa Date: Fri, 26 Apr 2024 15:56:11 -0500 Subject: [PATCH 1/4] Add copilot-chat sample app --- .dockerignore | 3 + .editorconfig | 627 + .gitattributes | 5 + .github/CODEOWNERS | 2 + .github/ISSUE_TEMPLATE/bug_report.md | 33 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/_typos.toml | 34 + .github/dependabot.yml | 29 + .github/labeler.yml | 29 + .github/pull_request_template.md | 23 + .github/workflows/codeql-analysis.yml | 65 + .github/workflows/copilot-build-backend.yml | 70 + .github/workflows/copilot-build-frontend.yml | 35 + .github/workflows/copilot-build-images.yml | 80 + .../copilot-build-memorypipeline.yml | 69 + .github/workflows/copilot-build-plugins.yml | 74 + .github/workflows/copilot-deploy-backend.yml | 76 + .../workflows/copilot-deploy-environment.yml | 55 + .github/workflows/copilot-deploy-infra.yml | 54 + .../copilot-deploy-memorypipeline.yml | 68 + .github/workflows/copilot-deploy-pipeline.yml | 49 + .github/workflows/copilot-deploy-plugins.yml | 63 + .../copilot-run-integration-tests.yml | 47 + .github/workflows/copilot-test-e2e.yml | 112 + .github/workflows/dotnet-format.yml | 75 + .github/workflows/label-pr.yml | 22 + .../workflows/markdown-link-check-config.json | 39 + .github/workflows/markdown-link-check.yml | 23 + .github/workflows/merge-gatekeeper.yml | 30 + .github/workflows/typos.yaml | 29 + .gitignore | 102 + .vscode/extensions.json | 8 + .vscode/launch.json | 17 + .vscode/settings.json | 54 + .vscode/tasks.json | 134 + CopilotChat.sln | 60 + docker/docker-compose.yaml | 139 + docker/memorypipeline/.env.example | 13 + docker/memorypipeline/Dockerfile | 28 + docker/plugins/web-searcher/.env.example | 1 + docker/plugins/web-searcher/Dockerfile | 23 + docker/webapi/.env.example | 23 + docker/webapi/Dockerfile | 27 + docker/webapp/Dockerfile | 19 + docker/webapp/Dockerfile.nginx | 21 + .../ChatCopilotIntegrationTest.cs | 96 + .../ChatCopilotIntegrationTests.csproj | 39 + integration-tests/ChatTests.cs | 50 + integration-tests/HealthzTests.cs | 20 + integration-tests/README.md | 55 + integration-tests/ServiceInfoTests.cs | 45 + integration-tests/SpeechTokenTests.cs | 27 + integration-tests/StaticFiles.cs | 21 + integration-tests/testsettings.json | 8 + .../CopilotChatMemoryPipeline.csproj | 22 + memorypipeline/Program.cs | 54 + memorypipeline/README.md | 108 + memorypipeline/appsettings.json | 227 + plugins/README.md | 51 + plugins/shared/PluginApi.cs | 19 + plugins/shared/PluginAuth.cs | 40 + plugins/shared/PluginManifest.cs | 75 + plugins/shared/PluginShared.csproj | 9 + plugins/web-searcher/Icons/bing.png | Bin 0 -> 9653 bytes .../web-searcher/Models/BingSearchResponse.cs | 53 + plugins/web-searcher/Models/PluginConfig.cs | 19 + plugins/web-searcher/PluginEndpoint.cs | 199 + plugins/web-searcher/Program.cs | 55 + plugins/web-searcher/README.md | 48 + plugins/web-searcher/WebSearcher.csproj | 36 + plugins/web-searcher/host.json | 22 + plugins/web-searcher/local.settings.json | 13 + scripts/Configure.ps1 | 212 + scripts/Install.ps1 | 33 + scripts/README.md | 3 + scripts/Start-Backend.ps1 | 19 + scripts/Start-Frontend.ps1 | 8 + scripts/Start.ps1 | 52 + scripts/Variables.ps1 | 15 + scripts/configure.sh | 226 + scripts/deploy/README.md | 232 + scripts/deploy/deploy-azure.ps1 | 169 + scripts/deploy/deploy-azure.sh | 225 + scripts/deploy/deploy-memorypipeline.ps1 | 63 + scripts/deploy/deploy-memorypipeline.sh | 94 + scripts/deploy/deploy-plugins.ps1 | 113 + scripts/deploy/deploy-plugins.sh | 137 + scripts/deploy/deploy-webapi.ps1 | 163 + scripts/deploy/deploy-webapi.sh | 203 + scripts/deploy/main.bicep | 1148 + scripts/deploy/main.json | 1159 + scripts/deploy/package-memorypipeline.ps1 | 56 + scripts/deploy/package-memorypipeline.sh | 109 + scripts/deploy/package-plugins.ps1 | 65 + scripts/deploy/package-plugins.sh | 114 + scripts/deploy/package-webapi.ps1 | 96 + scripts/deploy/package-webapi.sh | 150 + scripts/install-apt.sh | 28 + scripts/install-brew.sh | 15 + scripts/start-backend.sh | 22 + scripts/start-frontend.sh | 11 + scripts/start.sh | 46 + shared/ConfigurationBuilderExtensions.cs | 98 + shared/CopilotChatShared.csproj | 19 + shared/KernelMemoryBuilderExtensions.cs | 38 + shared/MemoryClientBuilderExtensions.cs | 24 + shared/MemoryConfiguration.cs | 13 + shared/Ocr/ConfigurationExtensions.cs | 42 + shared/Ocr/Tesseract/TesseractOcrEngine.cs | 40 + shared/Ocr/Tesseract/TesseractOptions.cs | 25 + shared/ServiceConfiguration.cs | 513 + tools/importdocument/Config.cs | 74 + tools/importdocument/ImportDocument.csproj | 30 + tools/importdocument/Program.cs | 191 + tools/importdocument/README.md | 112 + tools/importdocument/appsettings.json | 12 + .../sample-docs/Lorem_ipsum.pdf | Bin 0 -> 70011 bytes ...le-AI-Standard-v2-General-Requirements.pdf | Bin 0 -> 712896 bytes tools/importdocument/sample-docs/ms10k.txt | 18675 ++++++++++++++++ webapi/Auth/AuthInfo.cs | 62 + webapi/Auth/AuthPolicyName.cs | 11 + .../ChatParticipantAuthorizationHandler.cs | 69 + webapi/Auth/ChatParticipantRequirement.cs | 12 + webapi/Auth/IAuthInfo.cs | 16 + .../Auth/PassThroughAuthenticationHandler.cs | 50 + webapi/Controllers/ChatArchiveController.cs | 181 + webapi/Controllers/ChatController.cs | 484 + webapi/Controllers/ChatHistoryController.cs | 352 + webapi/Controllers/ChatMemoryController.cs | 126 + .../Controllers/ChatParticipantController.cs | 109 + webapi/Controllers/DocumentController.cs | 509 + webapi/Controllers/MaintenanceController.cs | 59 + webapi/Controllers/PluginController.cs | 111 + webapi/Controllers/ServiceInfoController.cs | 127 + webapi/Controllers/SpeechTokenController.cs | 77 + webapi/CopilotChatWebApi.csproj | 99 + webapi/Extensions/ConfigurationExtensions.cs | 53 + webapi/Extensions/ExceptionExtensions.cs | 29 + .../Extensions/IAsyncEnumerableExtensions.cs | 26 + .../ISemanticMemoryClientExtensions.cs | 191 + webapi/Extensions/SemanticKernelExtensions.cs | 242 + webapi/Extensions/ServiceExtensions.cs | 334 + webapi/Hubs/MessageRelayHub.cs | 61 + webapi/Models/Request/Ask.cs | 16 + webapi/Models/Request/CreateChatParameters.cs | 16 + webapi/Models/Request/CustomPlugin.cs | 42 + webapi/Models/Request/DocumentData.cs | 27 + webapi/Models/Request/DocumentImportForm.cs | 23 + webapi/Models/Request/DocumentScopes.cs | 12 + webapi/Models/Request/DocumentStatusForm.cs | 43 + webapi/Models/Request/EditChatParameters.cs | 26 + webapi/Models/Request/SemanticMemoryType.cs | 12 + webapi/Models/Response/AskResult.cs | 13 + webapi/Models/Response/BotResponsePrompt.cs | 66 + webapi/Models/Response/ChatArchive.cs | 49 + .../Response/ChatArchiveEmbeddingConfig.cs | 40 + webapi/Models/Response/CreateChatResponse.cs | 32 + .../Models/Response/DocumentMessageContent.cs | 104 + webapi/Models/Response/FrontendAuthConfig.cs | 36 + .../Models/Response/ImageAnalysisResponse.cs | 37 + webapi/Models/Response/MaintenanceResult.cs | 33 + webapi/Models/Response/ServiceInfoResponse.cs | 56 + webapi/Models/Response/SpeechTokenResponse.cs | 13 + webapi/Models/Storage/ChatParticipant.cs | 42 + webapi/Models/Storage/ChatSession.cs | 78 + webapi/Models/Storage/CitationSource.cs | 55 + webapi/Models/Storage/CopilotChatMessage.cs | 221 + webapi/Models/Storage/MemorySource.cs | 102 + webapi/Models/Storage/MemoryTags.cs | 19 + webapi/Options/AzureSpeechOptions.cs | 21 + webapi/Options/ChatArchiveSchemaInfo.cs | 23 + webapi/Options/ChatAuthenticationOptions.cs | 61 + webapi/Options/ChatStoreOptions.cs | 49 + webapi/Options/ContentSafetyOptions.cs | 38 + webapi/Options/CosmosOptions.cs | 47 + webapi/Options/DocumentMemoryOptions.cs | 49 + webapi/Options/FileSystemOptions.cs | 17 + webapi/Options/FrontendOptions.cs | 16 + webapi/Options/MemoryStoreType.cs | 75 + .../Options/NotEmptyOrWhitespaceAttribute.cs | 33 + webapi/Options/PluginOptions.cs | 26 + webapi/Options/PromptsOptions.cs | 185 + .../RequiredOnPropertyValueAttribute.cs | 80 + webapi/Options/ServiceOptions.cs | 40 + webapi/Plugins/Chat/ChatPlugin.cs | 728 + webapi/Plugins/Chat/SemanticChatMemory.cs | 50 + .../Chat/SemanticChatMemoryExtractor.cs | 141 + webapi/Plugins/Chat/SemanticChatMemoryItem.cs | 43 + .../Plugins/Chat/SemanticMemoryRetriever.cs | 262 + .../OpenApi/GitHubPlugin/Model/Label.cs | 42 + .../OpenApi/GitHubPlugin/Model/PullRequest.cs | 120 + .../OpenApi/GitHubPlugin/Model/Repo.cs | 34 + .../OpenApi/GitHubPlugin/Model/User.cs | 58 + .../Plugins/OpenApi/GitHubPlugin/openapi.json | 5935 +++++ .../OpenApi/JiraPlugin/Model/CommentAuthor.cs | 26 + .../JiraPlugin/Model/CommentResponse.cs | 27 + .../JiraPlugin/Model/IndividualComments.cs | 34 + .../OpenApi/JiraPlugin/Model/IssueResponse.cs | 50 + .../JiraPlugin/Model/IssueResponseFIeld.cs | 50 + .../Plugins/OpenApi/JiraPlugin/openapi.json | 1251 ++ webapi/Plugins/OpenApi/README.md | 23 + webapi/Plugins/Utils/AsyncUtils.cs | 53 + webapi/Plugins/Utils/PromptUtils.cs | 19 + webapi/Plugins/Utils/TokenUtils.cs | 126 + webapi/Program.cs | 131 + webapi/README.md | 320 + .../Services/AppInsightsTelemetryService.cs | 87 + ...InsightsUserTelemetryInitializerService.cs | 35 + webapi/Services/AzureContentSafety.cs | 146 + webapi/Services/DocumentTypeProvider.cs | 49 + webapi/Services/IContentSafetyService.cs | 32 + webapi/Services/IMaintenanceAction.cs | 19 + webapi/Services/ITelemetryService.cs | 17 + webapi/Services/MaintenanceMiddleware.cs | 73 + webapi/Services/SemanticKernelProvider.cs | 69 + webapi/Storage/ChatMemorySourceRepository.cs | 53 + webapi/Storage/ChatMessageRepository.cs | 47 + webapi/Storage/ChatParticipantRepository.cs | 56 + webapi/Storage/ChatSessionRepository.cs | 31 + webapi/Storage/CosmosDbContext.cs | 147 + webapi/Storage/FileSystemContext.cs | 194 + webapi/Storage/IRepository.cs | 47 + webapi/Storage/IStorageContext.cs | 61 + webapi/Storage/IStorageEntity.cs | 16 + webapi/Storage/Repository.cs | 100 + webapi/Storage/VolatileContext.cs | 113 + webapi/Utilities/PluginUtils.cs | 47 + webapi/appsettings.json | 400 + webapi/data/README.md | 6 + webapi/nuget.config | 15 + webapi/wwwroot/favicon.ico | Bin 0 -> 4286 bytes webapp/.env.example | 23 + webapp/.eslintrc.cjs | 52 + webapp/.prettierrc.cjs | 8 + webapp/.vscode/launch.json | 15 + webapp/README.md | 157 + webapp/package.json | 62 + webapp/playwright.config.ts | 65 + webapp/public/index.html | 11 + webapp/src/App.tsx | 211 + webapp/src/Constants.ts | 50 + webapp/src/assets/bot-icons/bot-icon-1.png | Bin 0 -> 971 bytes webapp/src/assets/bot-icons/bot-icon-2.png | Bin 0 -> 995 bytes webapp/src/assets/bot-icons/bot-icon-3.png | Bin 0 -> 1026 bytes webapp/src/assets/bot-icons/bot-icon-4.png | Bin 0 -> 1031 bytes webapp/src/assets/bot-icons/bot-icon-5.png | Bin 0 -> 1024 bytes webapp/src/assets/custom.d.ts | 16 + webapp/src/assets/plugin-icons/add-plugin.png | Bin 0 -> 24749 bytes webapp/src/assets/plugin-icons/github.png | Bin 0 -> 4387 bytes webapp/src/assets/plugin-icons/jira.png | Bin 0 -> 6321 bytes webapp/src/assets/plugin-icons/ms-graph.png | Bin 0 -> 7408 bytes webapp/src/assets/strings.ts | 8 + webapp/src/assets/typing-balls-light.svg | 1 + webapp/src/components/FileUploader.tsx | 54 + webapp/src/components/chat/ChatInput.tsx | 271 + webapp/src/components/chat/ChatRoom.tsx | 116 + webapp/src/components/chat/ChatStatus.tsx | 59 + webapp/src/components/chat/ChatWindow.tsx | 217 + .../chat/chat-history/ChatHistory.tsx | 37 + .../ChatHistoryDocumentContent.tsx | 117 + .../chat/chat-history/ChatHistoryItem.tsx | 238 + .../chat-history/ChatHistoryTextContent.tsx | 28 + .../chat/chat-history/CitationCards.tsx | 97 + .../chat/chat-history/UserFeedbackActions.tsx | 69 + .../components/chat/chat-list/ChatList.tsx | 223 + .../chat/chat-list/ChatListItem.tsx | 169 + .../chat/chat-list/ChatListSection.tsx | 71 + .../chat/chat-list/ListItemActions.tsx | 104 + .../chat/chat-list/bot-menu/NewBotMenu.tsx | 76 + .../bot-menu/SimplifiedNewBotMenu.tsx | 72 + .../chat-list/dialogs/DeleteChatDialog.tsx | 69 + .../chat/controls/ParticipantsList.tsx | 100 + .../components/chat/controls/ShareBotMenu.tsx | 77 + .../InvitationCreateDialog.tsx | 74 + .../InvitationJoinDialog.tsx | 89 + .../chat/persona/MemoryBiasSlider.tsx | 99 + .../components/chat/persona/PromptEditor.tsx | 120 + .../components/chat/plan-viewer/PlanBody.tsx | 48 + .../chat/plan-viewer/PlanDialogView.tsx | 183 + .../chat/plan-viewer/PlanStepCard.tsx | 218 + .../chat/plan-viewer/PlanStepInput.tsx | 164 + .../chat/plan-viewer/PlanViewer.tsx | 139 + .../chat/prompt-dialog/PromptDialog.tsx | 181 + .../stepwise-planner/StepwiseStepView.tsx | 96 + .../StepwiseThoughtProcessView.tsx | 86 + .../components/chat/shared/EditChatName.tsx | 112 + .../src/components/chat/tabs/DocumentsTab.tsx | 396 + .../src/components/chat/tabs/PersonaTab.tsx | 75 + webapp/src/components/chat/tabs/PlansTab.tsx | 185 + webapp/src/components/chat/tabs/TabView.tsx | 42 + .../chat/typing-indicator/TypingIndicator.tsx | 34 + .../components/header/UserSettingsMenu.tsx | 124 + .../header/settings-dialog/SettingSection.tsx | 85 + .../header/settings-dialog/SettingsDialog.tsx | 131 + .../open-api-plugins/PluginConnector.tsx | 329 + .../open-api-plugins/PluginGallery.tsx | 180 + .../open-api-plugins/cards/AddPluginCard.tsx | 28 + .../open-api-plugins/cards/BaseCard.tsx | 83 + .../open-api-plugins/cards/PluginCard.tsx | 69 + .../plugin-wizard/PluginWizard.tsx | 234 + .../plugin-wizard/steps/EnterManifestStep.tsx | 107 + .../steps/ValidateManifestStep.tsx | 161 + webapp/src/components/shared/Alerts.tsx | 64 + webapp/src/components/shared/BundledIcons.tsx | 49 + .../components/token-usage/TokenUsageBar.tsx | 36 + .../token-usage/TokenUsageGraph.tsx | 187 + .../token-usage/TokenUsageLegendItem.tsx | 75 + .../token-usage/TokenUsageLegendLabel.tsx | 23 + webapp/src/components/utils/PluginUtils.ts | 67 + webapp/src/components/utils/TextUtils.tsx | 101 + webapp/src/components/views/BackendProbe.tsx | 109 + webapp/src/components/views/ChatView.tsx | 28 + webapp/src/components/views/Error.tsx | 20 + webapp/src/components/views/Loading.tsx | 18 + webapp/src/components/views/Login.tsx | 38 + .../views/MissingEnvVariablesError.tsx | 31 + webapp/src/components/views/index.ts | 6 + webapp/src/index.css | 27 + webapp/src/index.tsx | 76 + webapp/src/libs/auth/AuthHelper.ts | 124 + webapp/src/libs/auth/TokenHelper.ts | 102 + webapp/src/libs/hooks/index.ts | 4 + webapp/src/libs/hooks/useChat.ts | 489 + webapp/src/libs/hooks/useFile.ts | 111 + webapp/src/libs/hooks/useGraph.ts | 165 + webapp/src/libs/hooks/usePlugins.ts | 51 + webapp/src/libs/models/AlertType.ts | 8 + webapp/src/libs/models/BotResponsePrompt.ts | 66 + webapp/src/libs/models/ChatArchive.ts | 11 + webapp/src/libs/models/ChatMemorySource.ts | 12 + webapp/src/libs/models/ChatMessage.ts | 70 + webapp/src/libs/models/ChatParticipant.ts | 6 + webapp/src/libs/models/ChatSession.ts | 16 + webapp/src/libs/models/ChatUser.ts | 10 + webapp/src/libs/models/Plan.ts | 90 + .../src/libs/models/PlanExecutionMetadata.ts | 16 + webapp/src/libs/models/PluginManifest.ts | 61 + webapp/src/libs/models/ServiceInfo.ts | 18 + webapp/src/libs/models/StepwiseStep.ts | 19 + webapp/src/libs/models/TokenUsage.ts | 25 + webapp/src/libs/semantic-kernel/model/Ask.ts | 11 + .../libs/semantic-kernel/model/AskResult.ts | 13 + .../semantic-kernel/model/CustomPlugin.ts | 7 + .../libs/semantic-kernel/model/KeyConfig.ts | 28 + webapp/src/libs/services/BaseService.ts | 91 + .../src/libs/services/ChatArchiveService.ts | 33 + webapp/src/libs/services/ChatService.ts | 257 + .../libs/services/DocumentImportService.ts | 42 + webapp/src/libs/services/GraphService.ts | 46 + .../src/libs/services/MaintenanceService.ts | 22 + webapp/src/libs/services/PluginService.ts | 34 + webapp/src/libs/services/SpeechService.ts | 41 + webapp/src/libs/utils/PlanUtils.ts | 14 + webapp/src/ms-symbollockup_signin_light.svg | 1 + webapp/src/redux/app/hooks.ts | 9 + webapp/src/redux/app/rootReducer.ts | 37 + webapp/src/redux/app/store.ts | 42 + webapp/src/redux/features/app/AppState.ts | 154 + webapp/src/redux/features/app/appSlice.ts | 139 + .../redux/features/conversations/ChatState.ts | 22 + .../conversations/ConversationsState.ts | 42 + .../conversations/conversationsSlice.ts | 253 + .../message-relay/signalRHubConnection.ts | 252 + .../message-relay/signalRMiddleware.ts | 67 + .../redux/features/plugins/PluginsState.ts | 140 + .../redux/features/plugins/pluginsSlice.ts | 49 + webapp/src/redux/features/users/UsersState.ts | 17 + webapp/src/redux/features/users/usersSlice.ts | 18 + webapp/src/styles.tsx | 116 + webapp/tests/README.md | 25 + webapp/tests/chat.test.ts | 61 + webapp/tests/testsBasic.ts | 72 + webapp/tests/testsMultiuser.ts | 64 + webapp/tests/testsPlanner.ts | 102 + webapp/tests/utils.ts | 176 + webapp/tsconfig.json | 36 + webapp/yarn.lock | 12251 ++++++++++ 377 files changed, 69686 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/_typos.toml create mode 100644 .github/dependabot.yml create mode 100644 .github/labeler.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/copilot-build-backend.yml create mode 100644 .github/workflows/copilot-build-frontend.yml create mode 100644 .github/workflows/copilot-build-images.yml create mode 100644 .github/workflows/copilot-build-memorypipeline.yml create mode 100644 .github/workflows/copilot-build-plugins.yml create mode 100644 .github/workflows/copilot-deploy-backend.yml create mode 100644 .github/workflows/copilot-deploy-environment.yml create mode 100644 .github/workflows/copilot-deploy-infra.yml create mode 100644 .github/workflows/copilot-deploy-memorypipeline.yml create mode 100644 .github/workflows/copilot-deploy-pipeline.yml create mode 100644 .github/workflows/copilot-deploy-plugins.yml create mode 100644 .github/workflows/copilot-run-integration-tests.yml create mode 100644 .github/workflows/copilot-test-e2e.yml create mode 100644 .github/workflows/dotnet-format.yml create mode 100644 .github/workflows/label-pr.yml create mode 100644 .github/workflows/markdown-link-check-config.json create mode 100644 .github/workflows/markdown-link-check.yml create mode 100644 .github/workflows/merge-gatekeeper.yml create mode 100644 .github/workflows/typos.yaml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CopilotChat.sln create mode 100644 docker/docker-compose.yaml create mode 100644 docker/memorypipeline/.env.example create mode 100644 docker/memorypipeline/Dockerfile create mode 100644 docker/plugins/web-searcher/.env.example create mode 100644 docker/plugins/web-searcher/Dockerfile create mode 100644 docker/webapi/.env.example create mode 100644 docker/webapi/Dockerfile create mode 100644 docker/webapp/Dockerfile create mode 100644 docker/webapp/Dockerfile.nginx create mode 100644 integration-tests/ChatCopilotIntegrationTest.cs create mode 100644 integration-tests/ChatCopilotIntegrationTests.csproj create mode 100644 integration-tests/ChatTests.cs create mode 100644 integration-tests/HealthzTests.cs create mode 100644 integration-tests/README.md create mode 100644 integration-tests/ServiceInfoTests.cs create mode 100644 integration-tests/SpeechTokenTests.cs create mode 100644 integration-tests/StaticFiles.cs create mode 100644 integration-tests/testsettings.json create mode 100644 memorypipeline/CopilotChatMemoryPipeline.csproj create mode 100644 memorypipeline/Program.cs create mode 100644 memorypipeline/README.md create mode 100644 memorypipeline/appsettings.json create mode 100644 plugins/README.md create mode 100644 plugins/shared/PluginApi.cs create mode 100644 plugins/shared/PluginAuth.cs create mode 100644 plugins/shared/PluginManifest.cs create mode 100644 plugins/shared/PluginShared.csproj create mode 100644 plugins/web-searcher/Icons/bing.png create mode 100644 plugins/web-searcher/Models/BingSearchResponse.cs create mode 100644 plugins/web-searcher/Models/PluginConfig.cs create mode 100644 plugins/web-searcher/PluginEndpoint.cs create mode 100644 plugins/web-searcher/Program.cs create mode 100644 plugins/web-searcher/README.md create mode 100644 plugins/web-searcher/WebSearcher.csproj create mode 100644 plugins/web-searcher/host.json create mode 100644 plugins/web-searcher/local.settings.json create mode 100644 scripts/Configure.ps1 create mode 100644 scripts/Install.ps1 create mode 100644 scripts/README.md create mode 100644 scripts/Start-Backend.ps1 create mode 100644 scripts/Start-Frontend.ps1 create mode 100644 scripts/Start.ps1 create mode 100644 scripts/Variables.ps1 create mode 100644 scripts/configure.sh create mode 100644 scripts/deploy/README.md create mode 100644 scripts/deploy/deploy-azure.ps1 create mode 100644 scripts/deploy/deploy-azure.sh create mode 100644 scripts/deploy/deploy-memorypipeline.ps1 create mode 100644 scripts/deploy/deploy-memorypipeline.sh create mode 100644 scripts/deploy/deploy-plugins.ps1 create mode 100644 scripts/deploy/deploy-plugins.sh create mode 100644 scripts/deploy/deploy-webapi.ps1 create mode 100644 scripts/deploy/deploy-webapi.sh create mode 100644 scripts/deploy/main.bicep create mode 100644 scripts/deploy/main.json create mode 100644 scripts/deploy/package-memorypipeline.ps1 create mode 100644 scripts/deploy/package-memorypipeline.sh create mode 100644 scripts/deploy/package-plugins.ps1 create mode 100644 scripts/deploy/package-plugins.sh create mode 100644 scripts/deploy/package-webapi.ps1 create mode 100644 scripts/deploy/package-webapi.sh create mode 100644 scripts/install-apt.sh create mode 100644 scripts/install-brew.sh create mode 100644 scripts/start-backend.sh create mode 100644 scripts/start-frontend.sh create mode 100644 scripts/start.sh create mode 100644 shared/ConfigurationBuilderExtensions.cs create mode 100644 shared/CopilotChatShared.csproj create mode 100644 shared/KernelMemoryBuilderExtensions.cs create mode 100644 shared/MemoryClientBuilderExtensions.cs create mode 100644 shared/MemoryConfiguration.cs create mode 100644 shared/Ocr/ConfigurationExtensions.cs create mode 100644 shared/Ocr/Tesseract/TesseractOcrEngine.cs create mode 100644 shared/Ocr/Tesseract/TesseractOptions.cs create mode 100644 shared/ServiceConfiguration.cs create mode 100644 tools/importdocument/Config.cs create mode 100644 tools/importdocument/ImportDocument.csproj create mode 100644 tools/importdocument/Program.cs create mode 100644 tools/importdocument/README.md create mode 100644 tools/importdocument/appsettings.json create mode 100644 tools/importdocument/sample-docs/Lorem_ipsum.pdf create mode 100644 tools/importdocument/sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf create mode 100644 tools/importdocument/sample-docs/ms10k.txt create mode 100644 webapi/Auth/AuthInfo.cs create mode 100644 webapi/Auth/AuthPolicyName.cs create mode 100644 webapi/Auth/ChatParticipantAuthorizationHandler.cs create mode 100644 webapi/Auth/ChatParticipantRequirement.cs create mode 100644 webapi/Auth/IAuthInfo.cs create mode 100644 webapi/Auth/PassThroughAuthenticationHandler.cs create mode 100644 webapi/Controllers/ChatArchiveController.cs create mode 100644 webapi/Controllers/ChatController.cs create mode 100644 webapi/Controllers/ChatHistoryController.cs create mode 100644 webapi/Controllers/ChatMemoryController.cs create mode 100644 webapi/Controllers/ChatParticipantController.cs create mode 100644 webapi/Controllers/DocumentController.cs create mode 100644 webapi/Controllers/MaintenanceController.cs create mode 100644 webapi/Controllers/PluginController.cs create mode 100644 webapi/Controllers/ServiceInfoController.cs create mode 100644 webapi/Controllers/SpeechTokenController.cs create mode 100644 webapi/CopilotChatWebApi.csproj create mode 100644 webapi/Extensions/ConfigurationExtensions.cs create mode 100644 webapi/Extensions/ExceptionExtensions.cs create mode 100644 webapi/Extensions/IAsyncEnumerableExtensions.cs create mode 100644 webapi/Extensions/ISemanticMemoryClientExtensions.cs create mode 100644 webapi/Extensions/SemanticKernelExtensions.cs create mode 100644 webapi/Extensions/ServiceExtensions.cs create mode 100644 webapi/Hubs/MessageRelayHub.cs create mode 100644 webapi/Models/Request/Ask.cs create mode 100644 webapi/Models/Request/CreateChatParameters.cs create mode 100644 webapi/Models/Request/CustomPlugin.cs create mode 100644 webapi/Models/Request/DocumentData.cs create mode 100644 webapi/Models/Request/DocumentImportForm.cs create mode 100644 webapi/Models/Request/DocumentScopes.cs create mode 100644 webapi/Models/Request/DocumentStatusForm.cs create mode 100644 webapi/Models/Request/EditChatParameters.cs create mode 100644 webapi/Models/Request/SemanticMemoryType.cs create mode 100644 webapi/Models/Response/AskResult.cs create mode 100644 webapi/Models/Response/BotResponsePrompt.cs create mode 100644 webapi/Models/Response/ChatArchive.cs create mode 100644 webapi/Models/Response/ChatArchiveEmbeddingConfig.cs create mode 100644 webapi/Models/Response/CreateChatResponse.cs create mode 100644 webapi/Models/Response/DocumentMessageContent.cs create mode 100644 webapi/Models/Response/FrontendAuthConfig.cs create mode 100644 webapi/Models/Response/ImageAnalysisResponse.cs create mode 100644 webapi/Models/Response/MaintenanceResult.cs create mode 100644 webapi/Models/Response/ServiceInfoResponse.cs create mode 100644 webapi/Models/Response/SpeechTokenResponse.cs create mode 100644 webapi/Models/Storage/ChatParticipant.cs create mode 100644 webapi/Models/Storage/ChatSession.cs create mode 100644 webapi/Models/Storage/CitationSource.cs create mode 100644 webapi/Models/Storage/CopilotChatMessage.cs create mode 100644 webapi/Models/Storage/MemorySource.cs create mode 100644 webapi/Models/Storage/MemoryTags.cs create mode 100644 webapi/Options/AzureSpeechOptions.cs create mode 100644 webapi/Options/ChatArchiveSchemaInfo.cs create mode 100644 webapi/Options/ChatAuthenticationOptions.cs create mode 100644 webapi/Options/ChatStoreOptions.cs create mode 100644 webapi/Options/ContentSafetyOptions.cs create mode 100644 webapi/Options/CosmosOptions.cs create mode 100644 webapi/Options/DocumentMemoryOptions.cs create mode 100644 webapi/Options/FileSystemOptions.cs create mode 100644 webapi/Options/FrontendOptions.cs create mode 100644 webapi/Options/MemoryStoreType.cs create mode 100644 webapi/Options/NotEmptyOrWhitespaceAttribute.cs create mode 100644 webapi/Options/PluginOptions.cs create mode 100644 webapi/Options/PromptsOptions.cs create mode 100644 webapi/Options/RequiredOnPropertyValueAttribute.cs create mode 100644 webapi/Options/ServiceOptions.cs create mode 100644 webapi/Plugins/Chat/ChatPlugin.cs create mode 100644 webapi/Plugins/Chat/SemanticChatMemory.cs create mode 100644 webapi/Plugins/Chat/SemanticChatMemoryExtractor.cs create mode 100644 webapi/Plugins/Chat/SemanticChatMemoryItem.cs create mode 100644 webapi/Plugins/Chat/SemanticMemoryRetriever.cs create mode 100644 webapi/Plugins/OpenApi/GitHubPlugin/Model/Label.cs create mode 100644 webapi/Plugins/OpenApi/GitHubPlugin/Model/PullRequest.cs create mode 100644 webapi/Plugins/OpenApi/GitHubPlugin/Model/Repo.cs create mode 100644 webapi/Plugins/OpenApi/GitHubPlugin/Model/User.cs create mode 100644 webapi/Plugins/OpenApi/GitHubPlugin/openapi.json create mode 100644 webapi/Plugins/OpenApi/JiraPlugin/Model/CommentAuthor.cs create mode 100644 webapi/Plugins/OpenApi/JiraPlugin/Model/CommentResponse.cs create mode 100644 webapi/Plugins/OpenApi/JiraPlugin/Model/IndividualComments.cs create mode 100644 webapi/Plugins/OpenApi/JiraPlugin/Model/IssueResponse.cs create mode 100644 webapi/Plugins/OpenApi/JiraPlugin/Model/IssueResponseFIeld.cs create mode 100644 webapi/Plugins/OpenApi/JiraPlugin/openapi.json create mode 100644 webapi/Plugins/OpenApi/README.md create mode 100644 webapi/Plugins/Utils/AsyncUtils.cs create mode 100644 webapi/Plugins/Utils/PromptUtils.cs create mode 100644 webapi/Plugins/Utils/TokenUtils.cs create mode 100644 webapi/Program.cs create mode 100644 webapi/README.md create mode 100644 webapi/Services/AppInsightsTelemetryService.cs create mode 100644 webapi/Services/AppInsightsUserTelemetryInitializerService.cs create mode 100644 webapi/Services/AzureContentSafety.cs create mode 100644 webapi/Services/DocumentTypeProvider.cs create mode 100644 webapi/Services/IContentSafetyService.cs create mode 100644 webapi/Services/IMaintenanceAction.cs create mode 100644 webapi/Services/ITelemetryService.cs create mode 100644 webapi/Services/MaintenanceMiddleware.cs create mode 100644 webapi/Services/SemanticKernelProvider.cs create mode 100644 webapi/Storage/ChatMemorySourceRepository.cs create mode 100644 webapi/Storage/ChatMessageRepository.cs create mode 100644 webapi/Storage/ChatParticipantRepository.cs create mode 100644 webapi/Storage/ChatSessionRepository.cs create mode 100644 webapi/Storage/CosmosDbContext.cs create mode 100644 webapi/Storage/FileSystemContext.cs create mode 100644 webapi/Storage/IRepository.cs create mode 100644 webapi/Storage/IStorageContext.cs create mode 100644 webapi/Storage/IStorageEntity.cs create mode 100644 webapi/Storage/Repository.cs create mode 100644 webapi/Storage/VolatileContext.cs create mode 100644 webapi/Utilities/PluginUtils.cs create mode 100644 webapi/appsettings.json create mode 100644 webapi/data/README.md create mode 100644 webapi/nuget.config create mode 100644 webapi/wwwroot/favicon.ico create mode 100644 webapp/.env.example create mode 100644 webapp/.eslintrc.cjs create mode 100644 webapp/.prettierrc.cjs create mode 100644 webapp/.vscode/launch.json create mode 100644 webapp/README.md create mode 100644 webapp/package.json create mode 100644 webapp/playwright.config.ts create mode 100644 webapp/public/index.html create mode 100644 webapp/src/App.tsx create mode 100644 webapp/src/Constants.ts create mode 100644 webapp/src/assets/bot-icons/bot-icon-1.png create mode 100644 webapp/src/assets/bot-icons/bot-icon-2.png create mode 100644 webapp/src/assets/bot-icons/bot-icon-3.png create mode 100644 webapp/src/assets/bot-icons/bot-icon-4.png create mode 100644 webapp/src/assets/bot-icons/bot-icon-5.png create mode 100644 webapp/src/assets/custom.d.ts create mode 100644 webapp/src/assets/plugin-icons/add-plugin.png create mode 100644 webapp/src/assets/plugin-icons/github.png create mode 100644 webapp/src/assets/plugin-icons/jira.png create mode 100644 webapp/src/assets/plugin-icons/ms-graph.png create mode 100644 webapp/src/assets/strings.ts create mode 100644 webapp/src/assets/typing-balls-light.svg create mode 100644 webapp/src/components/FileUploader.tsx create mode 100644 webapp/src/components/chat/ChatInput.tsx create mode 100644 webapp/src/components/chat/ChatRoom.tsx create mode 100644 webapp/src/components/chat/ChatStatus.tsx create mode 100644 webapp/src/components/chat/ChatWindow.tsx create mode 100644 webapp/src/components/chat/chat-history/ChatHistory.tsx create mode 100644 webapp/src/components/chat/chat-history/ChatHistoryDocumentContent.tsx create mode 100644 webapp/src/components/chat/chat-history/ChatHistoryItem.tsx create mode 100644 webapp/src/components/chat/chat-history/ChatHistoryTextContent.tsx create mode 100644 webapp/src/components/chat/chat-history/CitationCards.tsx create mode 100644 webapp/src/components/chat/chat-history/UserFeedbackActions.tsx create mode 100644 webapp/src/components/chat/chat-list/ChatList.tsx create mode 100644 webapp/src/components/chat/chat-list/ChatListItem.tsx create mode 100644 webapp/src/components/chat/chat-list/ChatListSection.tsx create mode 100644 webapp/src/components/chat/chat-list/ListItemActions.tsx create mode 100644 webapp/src/components/chat/chat-list/bot-menu/NewBotMenu.tsx create mode 100644 webapp/src/components/chat/chat-list/bot-menu/SimplifiedNewBotMenu.tsx create mode 100644 webapp/src/components/chat/chat-list/dialogs/DeleteChatDialog.tsx create mode 100644 webapp/src/components/chat/controls/ParticipantsList.tsx create mode 100644 webapp/src/components/chat/controls/ShareBotMenu.tsx create mode 100644 webapp/src/components/chat/invitation-dialog/InvitationCreateDialog.tsx create mode 100644 webapp/src/components/chat/invitation-dialog/InvitationJoinDialog.tsx create mode 100644 webapp/src/components/chat/persona/MemoryBiasSlider.tsx create mode 100644 webapp/src/components/chat/persona/PromptEditor.tsx create mode 100644 webapp/src/components/chat/plan-viewer/PlanBody.tsx create mode 100644 webapp/src/components/chat/plan-viewer/PlanDialogView.tsx create mode 100644 webapp/src/components/chat/plan-viewer/PlanStepCard.tsx create mode 100644 webapp/src/components/chat/plan-viewer/PlanStepInput.tsx create mode 100644 webapp/src/components/chat/plan-viewer/PlanViewer.tsx create mode 100644 webapp/src/components/chat/prompt-dialog/PromptDialog.tsx create mode 100644 webapp/src/components/chat/prompt-dialog/stepwise-planner/StepwiseStepView.tsx create mode 100644 webapp/src/components/chat/prompt-dialog/stepwise-planner/StepwiseThoughtProcessView.tsx create mode 100644 webapp/src/components/chat/shared/EditChatName.tsx create mode 100644 webapp/src/components/chat/tabs/DocumentsTab.tsx create mode 100644 webapp/src/components/chat/tabs/PersonaTab.tsx create mode 100644 webapp/src/components/chat/tabs/PlansTab.tsx create mode 100644 webapp/src/components/chat/tabs/TabView.tsx create mode 100644 webapp/src/components/chat/typing-indicator/TypingIndicator.tsx create mode 100644 webapp/src/components/header/UserSettingsMenu.tsx create mode 100644 webapp/src/components/header/settings-dialog/SettingSection.tsx create mode 100644 webapp/src/components/header/settings-dialog/SettingsDialog.tsx create mode 100644 webapp/src/components/open-api-plugins/PluginConnector.tsx create mode 100644 webapp/src/components/open-api-plugins/PluginGallery.tsx create mode 100644 webapp/src/components/open-api-plugins/cards/AddPluginCard.tsx create mode 100644 webapp/src/components/open-api-plugins/cards/BaseCard.tsx create mode 100644 webapp/src/components/open-api-plugins/cards/PluginCard.tsx create mode 100644 webapp/src/components/open-api-plugins/plugin-wizard/PluginWizard.tsx create mode 100644 webapp/src/components/open-api-plugins/plugin-wizard/steps/EnterManifestStep.tsx create mode 100644 webapp/src/components/open-api-plugins/plugin-wizard/steps/ValidateManifestStep.tsx create mode 100644 webapp/src/components/shared/Alerts.tsx create mode 100644 webapp/src/components/shared/BundledIcons.tsx create mode 100644 webapp/src/components/token-usage/TokenUsageBar.tsx create mode 100644 webapp/src/components/token-usage/TokenUsageGraph.tsx create mode 100644 webapp/src/components/token-usage/TokenUsageLegendItem.tsx create mode 100644 webapp/src/components/token-usage/TokenUsageLegendLabel.tsx create mode 100644 webapp/src/components/utils/PluginUtils.ts create mode 100644 webapp/src/components/utils/TextUtils.tsx create mode 100644 webapp/src/components/views/BackendProbe.tsx create mode 100644 webapp/src/components/views/ChatView.tsx create mode 100644 webapp/src/components/views/Error.tsx create mode 100644 webapp/src/components/views/Loading.tsx create mode 100644 webapp/src/components/views/Login.tsx create mode 100644 webapp/src/components/views/MissingEnvVariablesError.tsx create mode 100644 webapp/src/components/views/index.ts create mode 100644 webapp/src/index.css create mode 100644 webapp/src/index.tsx create mode 100644 webapp/src/libs/auth/AuthHelper.ts create mode 100644 webapp/src/libs/auth/TokenHelper.ts create mode 100644 webapp/src/libs/hooks/index.ts create mode 100644 webapp/src/libs/hooks/useChat.ts create mode 100644 webapp/src/libs/hooks/useFile.ts create mode 100644 webapp/src/libs/hooks/useGraph.ts create mode 100644 webapp/src/libs/hooks/usePlugins.ts create mode 100644 webapp/src/libs/models/AlertType.ts create mode 100644 webapp/src/libs/models/BotResponsePrompt.ts create mode 100644 webapp/src/libs/models/ChatArchive.ts create mode 100644 webapp/src/libs/models/ChatMemorySource.ts create mode 100644 webapp/src/libs/models/ChatMessage.ts create mode 100644 webapp/src/libs/models/ChatParticipant.ts create mode 100644 webapp/src/libs/models/ChatSession.ts create mode 100644 webapp/src/libs/models/ChatUser.ts create mode 100644 webapp/src/libs/models/Plan.ts create mode 100644 webapp/src/libs/models/PlanExecutionMetadata.ts create mode 100644 webapp/src/libs/models/PluginManifest.ts create mode 100644 webapp/src/libs/models/ServiceInfo.ts create mode 100644 webapp/src/libs/models/StepwiseStep.ts create mode 100644 webapp/src/libs/models/TokenUsage.ts create mode 100644 webapp/src/libs/semantic-kernel/model/Ask.ts create mode 100644 webapp/src/libs/semantic-kernel/model/AskResult.ts create mode 100644 webapp/src/libs/semantic-kernel/model/CustomPlugin.ts create mode 100644 webapp/src/libs/semantic-kernel/model/KeyConfig.ts create mode 100644 webapp/src/libs/services/BaseService.ts create mode 100644 webapp/src/libs/services/ChatArchiveService.ts create mode 100644 webapp/src/libs/services/ChatService.ts create mode 100644 webapp/src/libs/services/DocumentImportService.ts create mode 100644 webapp/src/libs/services/GraphService.ts create mode 100644 webapp/src/libs/services/MaintenanceService.ts create mode 100644 webapp/src/libs/services/PluginService.ts create mode 100644 webapp/src/libs/services/SpeechService.ts create mode 100644 webapp/src/libs/utils/PlanUtils.ts create mode 100644 webapp/src/ms-symbollockup_signin_light.svg create mode 100644 webapp/src/redux/app/hooks.ts create mode 100644 webapp/src/redux/app/rootReducer.ts create mode 100644 webapp/src/redux/app/store.ts create mode 100644 webapp/src/redux/features/app/AppState.ts create mode 100644 webapp/src/redux/features/app/appSlice.ts create mode 100644 webapp/src/redux/features/conversations/ChatState.ts create mode 100644 webapp/src/redux/features/conversations/ConversationsState.ts create mode 100644 webapp/src/redux/features/conversations/conversationsSlice.ts create mode 100644 webapp/src/redux/features/message-relay/signalRHubConnection.ts create mode 100644 webapp/src/redux/features/message-relay/signalRMiddleware.ts create mode 100644 webapp/src/redux/features/plugins/PluginsState.ts create mode 100644 webapp/src/redux/features/plugins/pluginsSlice.ts create mode 100644 webapp/src/redux/features/users/UsersState.ts create mode 100644 webapp/src/redux/features/users/usersSlice.ts create mode 100644 webapp/src/styles.tsx create mode 100644 webapp/tests/README.md create mode 100644 webapp/tests/chat.test.ts create mode 100644 webapp/tests/testsBasic.ts create mode 100644 webapp/tests/testsMultiuser.ts create mode 100644 webapp/tests/testsPlanner.ts create mode 100644 webapp/tests/utils.ts create mode 100644 webapp/tsconfig.json create mode 100644 webapp/yarn.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e002d7e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +**/node_modules +**/bin +**/obj \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6021d36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,627 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +############################### +# Core EditorConfig Options # +############################### +root = true +# All files +[*] +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 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +tab_width = 2 +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +# JSON config files +[*.json] +tab_width = 2 +indent_size = 2 +insert_final_newline = false +trim_trailing_whitespace = true + +# Typescript files +[*.{ts,tsx}] +insert_final_newline = true +trim_trailing_whitespace = true +tab_width = 4 +indent_size = 4 +file_header_template = Copyright (c) Microsoft. All rights reserved. + +# Stylesheet files +[*.{css,scss,sass,less}] +insert_final_newline = true +trim_trailing_whitespace = true +tab_width = 4 +indent_size = 4 + +# Code files +[*.{cs,csx,vb,vbx}] +tab_width = 4 +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8-bom +file_header_template = Copyright (c) Microsoft. All rights reserved. + +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] +# Organize usings +dotnet_sort_system_directives_first = true +# this. preferences +dotnet_style_qualification_for_field = true:error +dotnet_style_qualification_for_property = true:error +dotnet_style_qualification_for_method = true:error +dotnet_style_qualification_for_event = true:error +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:error +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:silent +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +# Code quality rules +dotnet_code_quality_unused_parameters = all:suggestion + +[*.cs] + +# TODO: enable this but stop "dotnet format" from applying incorrect fixes and introducing a lot of unwanted changes. +dotnet_analyzer_diagnostic.severity = none + +# Note: these settings cause "dotnet format" to fix the code. You should review each change if you uses "dotnet format". +dotnet_diagnostic.RCS1036.severity = warning # Remove unnecessary blank line. +dotnet_diagnostic.RCS1037.severity = warning # Remove trailing white-space. +dotnet_diagnostic.RCS1097.severity = warning # Remove redundant 'ToString' call. +dotnet_diagnostic.RCS1138.severity = warning # Add summary to documentation comment. +dotnet_diagnostic.RCS1139.severity = warning # Add summary element to documentation comment. +dotnet_diagnostic.RCS1168.severity = warning # Parameter name 'foo' differs from base name 'bar'. +dotnet_diagnostic.RCS1175.severity = warning # Unused 'this' parameter 'operation'. +dotnet_diagnostic.RCS1192.severity = warning # Unnecessary usage of verbatim string literal. +dotnet_diagnostic.RCS1194.severity = warning # Implement exception constructors. +dotnet_diagnostic.RCS1211.severity = warning # Remove unnecessary else clause. +dotnet_diagnostic.RCS1214.severity = warning # Unnecessary interpolated string. +dotnet_diagnostic.RCS1225.severity = warning # Make class sealed. +dotnet_diagnostic.RCS1232.severity = warning # Order elements in documentation comment. + +# Commented out because `dotnet format` change can be disruptive. +# dotnet_diagnostic.RCS1085.severity = warning # Use auto-implemented property. + +# Commented out because `dotnet format` removes the xmldoc element, while we should add the missing documentation instead. +# dotnet_diagnostic.RCS1228.severity = warning # Unused element in documentation comment. + +# Diagnostics elevated as warnings +dotnet_diagnostic.CA1000.severity = warning # Do not declare static members on generic types +dotnet_diagnostic.CA1031.severity = warning # Do not catch general exception types +dotnet_diagnostic.CA1050.severity = warning # Declare types in namespaces +dotnet_diagnostic.CA1063.severity = warning # Implement IDisposable correctly +dotnet_diagnostic.CA1064.severity = warning # Exceptions should be public +dotnet_diagnostic.CA1416.severity = warning # Validate platform compatibility +dotnet_diagnostic.CA1508.severity = warning # Avoid dead conditional code +dotnet_diagnostic.CA1852.severity = warning # Sealed classes +dotnet_diagnostic.CA1859.severity = warning # Use concrete types when possible for improved performance +dotnet_diagnostic.CA1860.severity = warning # Prefer comparing 'Count' to 0 rather than using 'Any()', both for clarity and for performance +dotnet_diagnostic.CA2000.severity = warning # Call System.IDisposable.Dispose on object before all references to it are out of scope +dotnet_diagnostic.CA2201.severity = warning # Exception type System.Exception is not sufficiently specific + +dotnet_diagnostic.IDE0001.severity = warning # Simplify name +dotnet_diagnostic.IDE0005.severity = warning # Remove unnecessary using directives +dotnet_diagnostic.IDE0009.severity = warning # Add this or Me qualification +dotnet_diagnostic.IDE0011.severity = warning # Add braces +dotnet_diagnostic.IDE0018.severity = warning # Inline variable declaration +dotnet_diagnostic.IDE0032.severity = warning # Use auto-implemented property +dotnet_diagnostic.IDE0034.severity = warning # Simplify 'default' expression +dotnet_diagnostic.IDE0035.severity = warning # Remove unreachable code +dotnet_diagnostic.IDE0040.severity = warning # Add accessibility modifiers +dotnet_diagnostic.IDE0049.severity = warning # Use language keywords instead of framework type names for type references +dotnet_diagnostic.IDE0050.severity = warning # Convert anonymous type to tuple +dotnet_diagnostic.IDE0051.severity = warning # Remove unused private member +dotnet_diagnostic.IDE0055.severity = warning # Formatting rule +dotnet_diagnostic.IDE0060.severity = warning # Remove unused parameter +dotnet_diagnostic.IDE0070.severity = warning # Use 'System.HashCode.Combine' +dotnet_diagnostic.IDE0071.severity = warning # Simplify interpolation +dotnet_diagnostic.IDE0073.severity = warning # Require file header +dotnet_diagnostic.IDE0082.severity = warning # Convert typeof to nameof +dotnet_diagnostic.IDE0090.severity = warning # Simplify new expression +dotnet_diagnostic.IDE0130.severity = warning # Namespace does not match folder structure +dotnet_diagnostic.IDE0161.severity = warning # Use file-scoped namespace + +# Suppressed diagnostics +dotnet_diagnostic.CA1002.severity = none # Change 'List' in '...' to use 'Collection' ... +dotnet_diagnostic.CA1032.severity = none # We're using RCS1194 which seems to cover more ctors +dotnet_diagnostic.CA1034.severity = none # Do not nest type. Alternatively, change its accessibility so that it is not externally visible +dotnet_diagnostic.CA1062.severity = none # Disable null check, C# already does it for us +dotnet_diagnostic.CA1303.severity = none # Do not pass literals as localized parameters +dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member +dotnet_diagnostic.CA1805.severity = none # Member is explicitly initialized to its default value +dotnet_diagnostic.CA1822.severity = none # Member does not access instance data and can be marked as static +dotnet_diagnostic.CA1848.severity = none # For improved performance, use the LoggerMessage delegates +dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task +dotnet_diagnostic.CA2225.severity = none # Operator overloads have named alternates +dotnet_diagnostic.CA2227.severity = none # Change to be read-only by removing the property setter +dotnet_diagnostic.CA2253.severity = none # Named placeholders in the logging message template should not be comprised of only numeric characters + +dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave +dotnet_diagnostic.xUnit1004.severity = none # Test methods should not be skipped. Remove the Skip property to start running the test again. + +dotnet_diagnostic.RCS1021.severity = none # Use expression-bodied lambda. +dotnet_diagnostic.RCS1032.severity = none # Remove redundant parentheses. +dotnet_diagnostic.RCS1061.severity = none # Merge 'if' with nested 'if'. +dotnet_diagnostic.RCS1069.severity = none # Remove unnecessary case label. +dotnet_diagnostic.RCS1074.severity = none # Remove redundant constructor. +dotnet_diagnostic.RCS1077.severity = none # Optimize LINQ method call. +dotnet_diagnostic.RCS1118.severity = none # Mark local variable as const. +dotnet_diagnostic.RCS1124.severity = none # Inline local variable. +dotnet_diagnostic.RCS1129.severity = none # Remove redundant field initialization. +dotnet_diagnostic.RCS1140.severity = none # Add exception to documentation comment. +dotnet_diagnostic.RCS1141.severity = none # Add 'param' element to documentation comment. +dotnet_diagnostic.RCS1142.severity = none # Add 'typeparam' element to documentation comment. +dotnet_diagnostic.RCS1146.severity = none # Use conditional access. +dotnet_diagnostic.RCS1151.severity = none # Remove redundant cast. +dotnet_diagnostic.RCS1158.severity = none # Static member in generic type should use a type parameter. +dotnet_diagnostic.RCS1161.severity = none # Enum should declare explicit value +dotnet_diagnostic.RCS1163.severity = none # Unused parameter 'foo'. +dotnet_diagnostic.RCS1170.severity = none # Use read-only auto-implemented property. +dotnet_diagnostic.RCS1173.severity = none # Use coalesce expression instead of 'if'. +dotnet_diagnostic.RCS1181.severity = none # Convert comment to documentation comment. +dotnet_diagnostic.RCS1186.severity = none # Use Regex instance instead of static method. +dotnet_diagnostic.RCS1188.severity = none # Remove redundant auto-property initialization. +dotnet_diagnostic.RCS1189.severity = none # Add region name to #endregion. +dotnet_diagnostic.RCS1197.severity = none # Optimize StringBuilder.AppendLine call. +dotnet_diagnostic.RCS1201.severity = none # Use method chaining. +dotnet_diagnostic.RCS1205.severity = none # Order named arguments according to the order of parameters. +dotnet_diagnostic.RCS1212.severity = none # Remove redundant assignment. +dotnet_diagnostic.RCS1217.severity = none # Convert interpolated string to concatenation. +dotnet_diagnostic.RCS1222.severity = none # Merge preprocessor directives. +dotnet_diagnostic.RCS1226.severity = none # Add paragraph to documentation comment. +dotnet_diagnostic.RCS1229.severity = none # Use async/await when necessary. +dotnet_diagnostic.RCS1234.severity = none # Enum duplicate value +dotnet_diagnostic.RCS1238.severity = none # Avoid nested ?: operators. +dotnet_diagnostic.RCS1241.severity = none # Implement IComparable when implementing IComparable. + +dotnet_diagnostic.IDE0001.severity = none # Simplify name +dotnet_diagnostic.IDE0002.severity = none # Simplify member access +dotnet_diagnostic.IDE0004.severity = none # Remove unnecessary cast +dotnet_diagnostic.IDE0035.severity = none # Remove unreachable code +dotnet_diagnostic.IDE0051.severity = none # Remove unused private member +dotnet_diagnostic.IDE0052.severity = none # Remove unread private member +dotnet_diagnostic.IDE0058.severity = none # Remove unused expression value +dotnet_diagnostic.IDE0059.severity = none # Unnecessary assignment of a value +dotnet_diagnostic.IDE0060.severity = none # Remove unused parameter +dotnet_diagnostic.IDE0080.severity = none # Remove unnecessary suppression operator +dotnet_diagnostic.IDE0100.severity = none # Remove unnecessary equality operator +dotnet_diagnostic.IDE0110.severity = none # Remove unnecessary discards +dotnet_diagnostic.IDE0032.severity = none # Use auto property +dotnet_diagnostic.IDE0160.severity = none # Use block-scoped namespace + +############################### +# Naming Conventions # +############################### + +# Styles + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +dotnet_naming_style.static_underscored.capitalization = camel_case +dotnet_naming_style.static_underscored.required_prefix = s_ + +dotnet_naming_style.underscored.capitalization = camel_case +dotnet_naming_style.underscored.required_prefix = _ + +dotnet_naming_style.uppercase_with_underscore_separator.capitalization = all_upper +dotnet_naming_style.uppercase_with_underscore_separator.word_separator = _ + +dotnet_naming_style.end_in_async.required_prefix = +dotnet_naming_style.end_in_async.required_suffix = Async +dotnet_naming_style.end_in_async.capitalization = pascal_case +dotnet_naming_style.end_in_async.word_separator = + +# Symbols + +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const + +dotnet_naming_symbols.local_constant.applicable_kinds = local +dotnet_naming_symbols.local_constant.applicable_accessibilities = * +dotnet_naming_symbols.local_constant.required_modifiers = const + +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_symbols.any_async_methods.applicable_kinds = method +dotnet_naming_symbols.any_async_methods.applicable_accessibilities = * +dotnet_naming_symbols.any_async_methods.required_modifiers = async + +# Rules + +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error + +dotnet_naming_rule.local_constant_should_be_pascal_case.symbols = local_constant +dotnet_naming_rule.local_constant_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.local_constant_should_be_pascal_case.severity = error + +dotnet_naming_rule.private_static_fields_underscored.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_underscored.style = static_underscored +dotnet_naming_rule.private_static_fields_underscored.severity = error + +dotnet_naming_rule.private_fields_underscored.symbols = private_fields +dotnet_naming_rule.private_fields_underscored.style = underscored +dotnet_naming_rule.private_fields_underscored.severity = error + +dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods +dotnet_naming_rule.async_methods_end_in_async.style = end_in_async +dotnet_naming_rule.async_methods_end_in_async.severity = error + +############################### +# C# Coding Conventions # +############################### + +# var preferences +csharp_style_var_for_built_in_types = false:none +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = false:none +# Expression-bodied members +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +# Expression-level preferences +csharp_prefer_braces = true:error +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:error +csharp_style_inlined_variable_declaration = true:suggestion + +############################### +# C# Formatting Rules # +############################### + +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = false # Does not work with resharper, forcing code to be on long lines instead of wrapping +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation preferences +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true +csharp_using_directive_placement = outside_namespace:warning +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent + + +############################### +# Java Coding Conventions # +############################### +[*.java] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = false +tab_width = 2 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_smart_tabs = false +ij_visual_guides = none + +max_line_length = 100 +ij_continuation_indent_size = 4 +ij_formatter_tags_enabled = false +ij_wrap_on_typing = false + +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = false +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = false +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_resources = false +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = normal +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = true +ij_java_binary_operation_wrap = normal +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 1 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_at_first_column = true +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = normal +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_class_names_in_javadoc = 1 +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_while_brace_force = always +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = normal +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = always +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = normal +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = always +ij_java_imports_layout = $*, |, * +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = true +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 1 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_control_statement_in_one_line = false +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_at_first_column = true +ij_java_message_dd_suffix = EJB +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = normal +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = normal +ij_java_modifier_list_wrap = false +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_parameter_annotation_wrap = off +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = true +ij_java_ternary_operation_wrap = normal +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = normal +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = true +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..367c088 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Auto-detect text files, ensure they use LF. +* text=auto eol=lf working-tree-encoding=UTF-8 + +# Bash scripts +*.sh text eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..9fba897 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# @microsoft/octo-semantickernel-pr-apps owns any files in the repo +@microsoft/octo-semantickernel-pr-apps diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..374f1b3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Platform** + - OS: [e.g. Windows, Mac] + - IDE: [e.g. Visual Studio, VS Code] + - Language: [e.g. C#, Python] + - Source: [e.g. NuGet package version 0.1.0, pip package version 0.1.0, main branch of repository] + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2d49007 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +--- +name: Feature request +about: Suggest an idea for this project + +--- + + + + + + diff --git a/.github/_typos.toml b/.github/_typos.toml new file mode 100644 index 0000000..8298df7 --- /dev/null +++ b/.github/_typos.toml @@ -0,0 +1,34 @@ +# Typos configuration file +# +# Info: https://github.com/marketplace/actions/typos-action +# Install: brew install typos-cli +# Install: conda install typos +# Run: typos -c .github/_typos.toml + +[files] +extend-exclude = [ + "_typos.toml", + "package-lock.json", + "*.bicep", + "encoder.json", + "vocab.bpe", + "GPT3TokenizerTests.cs", + "CodeTokenizerTests.cs", + "test_code_tokenizer.py", +] + +[default.extend-words] +ACI = "ACI" # Azure Container Instance + +[default.extend-identifiers] +ags = "ags" # Azure Graph Service + +[type.jupyter] +extend-ignore-re = [ + '"[A-Fa-f0-9]{8}"', # cell id strings +] + +[type.msbuild] +extend-ignore-re = [ + 'Version=".*"', # ignore package version numbers +] \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d988049 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,29 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + # Maintain dependencies for nuget + - package-ecosystem: "nuget" + directory: "webapi" + schedule: + interval: "weekly" + day: "monday" + + # Maintain dependencies for npm + - package-ecosystem: "npm" + directory: "webapp" + schedule: + interval: "weekly" + day: "monday" + + # Maintain dependencies for github-actions + - package-ecosystem: "github-actions" + # Workflow files stored in the + # default location of `.github/workflows` + directory: "/" + schedule: + interval: "weekly" + day: "monday" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..c60b4a0 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,29 @@ +# 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: + - 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 new file mode 100644 index 0000000..b083a7b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,23 @@ +### Motivation and Context + + + +### Description + + + +### Contribution Checklist + + + +- [ ] The code builds clean without any errors or warnings +- [ ] 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 new file mode 100644 index 0000000..3e96860 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,65 @@ +# CodeQL is the code analysis engine developed by GitHub to automate security checks. +# The results are shown as code scanning alerts in GitHub. For more details, visit: +# https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/about-code-scanning-with-codeql + +name: "CodeQL" + +on: + push: + branches: ["main", "experimental*", "feature*"] + schedule: + - cron: "17 11 * * 2" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + 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@v4 + + # 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 + + # 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 + + # ℹ️ 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. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - 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 0000000..bdb4690 --- /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: "6.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@v1 + with: + versionSpec: "5.x" + + - name: Determine version + id: gitversion + uses: gittools/actions/gitversion/execute@v1 + + - 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 net6.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 0000000..9a5b5b5 --- /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 0000000..aa73cc4 --- /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@v5 + 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 0000000..d45011a --- /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@v1 + with: + versionSpec: "5.x" + + - name: Determine version + id: gitversion + uses: gittools/actions/gitversion/execute@v1 + + - 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: 6.0.x + + - name: Package Copilot Chat Memory Pipeline + run: | + scripts\deploy\package-memorypipeline.ps1 -Configuration Release -DotnetFramework net6.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 0000000..57c0c44 --- /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@v1 + with: + versionSpec: "5.x" + + - name: Determine version + id: gitversion + uses: gittools/actions/gitversion/execute@v1 + + - 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: 6.0.x + + - name: Package Copilot Chat Plugins + run: | + scripts\deploy\package-plugins.ps1 ` + -BuildConfiguration Release ` + -DotNetFramework net6.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-deploy-backend.yml b/.github/workflows/copilot-deploy-backend.yml new file mode 100644 index 0000000..8394878 --- /dev/null +++ b/.github/workflows/copilot-deploy-backend.yml @@ -0,0 +1,76 @@ +name: copilot-deploy-backend + +on: + workflow_call: + inputs: + ARTIFACT_NAME: + required: true + type: string + ENVIRONMENT: + required: true + type: string + DEPLOYMENT_NAME: + required: true + type: string + outputs: + backend-host: + description: "Host to which backend is deployed" + value: ${{jobs.webapi.outputs.backend-host}} + +permissions: + contents: read + id-token: write + +jobs: + webapi: + environment: ${{inputs.ENVIRONMENT}} + strategy: + fail-fast: false + matrix: + include: + - { dotnet: "6.0", configuration: Release, os: ubuntu-latest } + # Map the job output to step output + outputs: + backend-host: ${{steps.app-name.outputs.backend-host}} + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + scripts + + - uses: actions/download-artifact@v4 + with: + name: ${{inputs.ARTIFACT_NAME}} + path: "${{ github.workspace }}/${{inputs.ARTIFACT_NAME}}" + + - name: "Display downloaded content" + run: ls -R + working-directory: "${{ github.workspace }}/${{inputs.ARTIFACT_NAME}}" + + - name: Azure login + uses: azure/login@v2 + with: + client-id: ${{vars.AZURE_GITHUB_ACCESS_APP_ID}} + tenant-id: ${{vars.AZURE_GITHUB_ACCESS_TENANT_ID}} + subscription-id: ${{vars.AZURE_GITHUB_ACCESS_SUB_ID}} + enable-AzPSSession: false + + - name: Get app name + id: app-name + run: | + WEB_APP_NAME=$(az deployment group show --name ${{inputs.DEPLOYMENT_NAME}} --resource-group ${{vars.CC_DEPLOYMENT_GROUP_NAME}} --output json | jq -r '.properties.outputs.webapiName.value') + echo "AZURE_WEBAPP_NAME=$WEB_APP_NAME" >> $GITHUB_ENV + echo "backend-host=$WEB_APP_NAME" >> $GITHUB_OUTPUT + + - name: Enable Run From Package + uses: azure/CLI@v2 + with: + azcliversion: 2.30.0 + inlineScript: | + az webapp config appsettings set --resource-group ${{vars.CC_DEPLOYMENT_GROUP_NAME}} --name ${{ env.AZURE_WEBAPP_NAME }} --settings WEBSITE_RUN_FROM_PACKAGE="1" -o none + + - name: "Deploy" + run: | + scripts/deploy/deploy-webapi.sh -p "${{ github.workspace }}/${{inputs.ARTIFACT_NAME}}/webapi.zip" -d ${{inputs.DEPLOYMENT_NAME}} -s ${{vars.AZURE_SUBSCRIPTION_ID}} -rg ${{vars.CC_DEPLOYMENT_GROUP_NAME}} --skip-app-registration diff --git a/.github/workflows/copilot-deploy-environment.yml b/.github/workflows/copilot-deploy-environment.yml new file mode 100644 index 0000000..c29b4ba --- /dev/null +++ b/.github/workflows/copilot-deploy-environment.yml @@ -0,0 +1,55 @@ +name: copilot-deploy-environment + +on: + workflow_call: + inputs: + ENVIRONMENT: + required: true + type: string + WEBAPI_ARTIFACT_NAME: + required: true + type: string + MEMORYPIPELINE_ARTIFACT_NAME: + required: true + type: string + PLUGINS_ARTIFACT_NAME: + required: true + type: string + outputs: + backend-host: + description: "Host on which backend runs" + value: ${{jobs.deploy-backend.outputs.backend-host}} + +permissions: + contents: read + id-token: write + +jobs: + deploy-infra: + uses: ./.github/workflows/copilot-deploy-infra.yml + with: + ENVIRONMENT: ${{inputs.ENVIRONMENT}} + + deploy-backend: + needs: [deploy-infra] + uses: ./.github/workflows/copilot-deploy-backend.yml + with: + ARTIFACT_NAME: ${{inputs.WEBAPI_ARTIFACT_NAME}} + DEPLOYMENT_NAME: ${{needs.deploy-infra.outputs.deployment-id}} + ENVIRONMENT: ${{inputs.ENVIRONMENT}} + + deploy-memorypipeline: + needs: [deploy-infra] + uses: ./.github/workflows/copilot-deploy-memorypipeline.yml + with: + ARTIFACT_NAME: ${{inputs.MEMORYPIPELINE_ARTIFACT_NAME}} + DEPLOYMENT_NAME: ${{needs.deploy-infra.outputs.deployment-id}} + ENVIRONMENT: ${{inputs.ENVIRONMENT}} + + deploy-plugins: + needs: [deploy-infra] + uses: ./.github/workflows/copilot-deploy-plugins.yml + with: + ARTIFACT_NAME: ${{inputs.PLUGINS_ARTIFACT_NAME}} + DEPLOYMENT_NAME: ${{needs.deploy-infra.outputs.deployment-id}} + ENVIRONMENT: ${{inputs.ENVIRONMENT}} diff --git a/.github/workflows/copilot-deploy-infra.yml b/.github/workflows/copilot-deploy-infra.yml new file mode 100644 index 0000000..c7e13d7 --- /dev/null +++ b/.github/workflows/copilot-deploy-infra.yml @@ -0,0 +1,54 @@ +name: copilot-deploy-infra + +on: + workflow_call: + inputs: + ENVIRONMENT: + required: true + type: string + outputs: + deployment-id: + description: "The Id of the current deployment." + value: ${{jobs.deploy.outputs.deployment-id}} + +jobs: + deploy: + environment: ${{inputs.ENVIRONMENT}} + permissions: + contents: read + id-token: write + strategy: + fail-fast: false + matrix: + include: + - { dotnet: "6.0", configuration: Release, os: ubuntu-latest } + outputs: + deployment-id: ${{steps.deployment-id.outputs.deployment_name}} + + runs-on: ${{ matrix.os }} + steps: + - name: Generate Deployment Id + id: deployment-id + run: echo "deployment_name=${{ vars.CC_DEPLOYMENT_NAME }}-$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v4 + with: + clean: true + + - name: Azure login + uses: azure/login@v2 + with: + client-id: ${{vars.AZURE_GITHUB_ACCESS_APP_ID}} + tenant-id: ${{vars.AZURE_GITHUB_ACCESS_TENANT_ID}} + subscription-id: ${{vars.AZURE_GITHUB_ACCESS_SUB_ID}} + enable-AzPSSession: false + + - name: deploy-infra + uses: azure/CLI@v2 + with: + azcliversion: 2.30.0 + inlineScript: | + AI_SERVICE_KEY=$(az cognitiveservices account keys list --name ${{vars.AZURE_OPENAI_NAME}} --resource-group ${{vars.AZUREOPENAI_DEPLOYMENT_GROUP_NAME}} | jq -r '.key1') + echo "::add-mask::$AI_SERVICE_KEY" + APP_TENANT_ID=${{vars.APPLICATION_TENANT_ID}} + scripts/deploy/deploy-azure.sh --subscription ${{vars.AZURE_SUBSCRIPTION_ID}} --resource-group ${{vars.CC_DEPLOYMENT_GROUP_NAME}} --deployment-name ${{steps.deployment-id.outputs.deployment_name}} --region ${{vars.CC_DEPLOYMENT_REGION}} --client-id ${{vars.BACKEND_CLIENT_ID}} --frontend-client-id ${{vars.APPLICATION_CLIENT_ID}} --tenant-id $APP_TENANT_ID --instance ${{vars.AZURE_INSTANCE}} --ai-service AzureOpenAI --ai-endpoint ${{vars.AZURE_OPENAI_ENDPOINT}} --ai-service-key $AI_SERVICE_KEY --app-service-sku ${{vars.WEBAPP_API_SKU}} --no-deploy-package --debug-deployment --deploy-web-searcher-plugin diff --git a/.github/workflows/copilot-deploy-memorypipeline.yml b/.github/workflows/copilot-deploy-memorypipeline.yml new file mode 100644 index 0000000..998751e --- /dev/null +++ b/.github/workflows/copilot-deploy-memorypipeline.yml @@ -0,0 +1,68 @@ +name: copilot-deploy-memorypipeline + +on: + workflow_call: + inputs: + ARTIFACT_NAME: + required: true + type: string + ENVIRONMENT: + required: true + type: string + DEPLOYMENT_NAME: + required: true + type: string + +permissions: + contents: read + id-token: write + +jobs: + memorypipeline: + environment: ${{inputs.ENVIRONMENT}} + strategy: + fail-fast: false + matrix: + include: + - { dotnet: "6.0", configuration: Release, os: ubuntu-latest } + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + clean: true + + - uses: actions/download-artifact@v4 + with: + name: ${{inputs.ARTIFACT_NAME}} + path: "${{ github.workspace }}/${{inputs.ARTIFACT_NAME}}" + + - name: "Display downloaded content" + run: ls -R + working-directory: "${{ github.workspace }}/${{inputs.ARTIFACT_NAME}}" + + - name: Azure login + uses: azure/login@v2 + with: + client-id: ${{vars.AZURE_GITHUB_ACCESS_APP_ID}} + tenant-id: ${{vars.AZURE_GITHUB_ACCESS_TENANT_ID}} + subscription-id: ${{vars.AZURE_GITHUB_ACCESS_SUB_ID}} + enable-AzPSSession: false + + - name: Get app name + run: | + WEB_APP_NAME=$(az deployment group show --name ${{inputs.DEPLOYMENT_NAME}} --resource-group ${{vars.CC_DEPLOYMENT_GROUP_NAME}} --output json | jq -r '.properties.outputs.memoryPipelineName.value') + echo "AZURE_WEBAPP_NAME=$WEB_APP_NAME" >> $GITHUB_ENV + + - name: Enable Run From Package + uses: azure/CLI@v2 + with: + azcliversion: 2.30.0 + inlineScript: | + az webapp config appsettings set --resource-group ${{vars.CC_DEPLOYMENT_GROUP_NAME}} --name ${{ env.AZURE_WEBAPP_NAME }} --settings WEBSITE_RUN_FROM_PACKAGE="1" -o none + + - name: "Deploy" + uses: azure/webapps-deploy@v3 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} + package: "${{ github.workspace }}/${{inputs.ARTIFACT_NAME}}/memorypipeline.zip" diff --git a/.github/workflows/copilot-deploy-pipeline.yml b/.github/workflows/copilot-deploy-pipeline.yml new file mode 100644 index 0000000..8c43f12 --- /dev/null +++ b/.github/workflows/copilot-deploy-pipeline.yml @@ -0,0 +1,49 @@ +name: copilot-deploy-pipeline + +on: + workflow_dispatch: + push: + branches: ["main"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + id-token: write + +jobs: + build-webapi: + uses: ./.github/workflows/copilot-build-backend.yml + + build-memorypipeline: + uses: ./.github/workflows/copilot-build-memorypipeline.yml + + build-plugins: + uses: ./.github/workflows/copilot-build-plugins.yml + + int: + needs: [build-webapi, build-memorypipeline, build-plugins] + uses: ./.github/workflows/copilot-deploy-environment.yml + with: + ENVIRONMENT: int + WEBAPI_ARTIFACT_NAME: ${{needs.build-webapi.outputs.artifact}} + MEMORYPIPELINE_ARTIFACT_NAME: ${{needs.build-memorypipeline.outputs.artifact}} + PLUGINS_ARTIFACT_NAME: ${{needs.build-plugins.outputs.artifact}} + + int-tests: + uses: ./.github/workflows/copilot-run-integration-tests.yml + needs: int + with: + BACKEND_HOST: ${{needs.int.outputs.backend-host}} + ENVIRONMENT: int + + stable: + needs: [int-tests, build-webapi, build-memorypipeline, build-plugins] + uses: ./.github/workflows/copilot-deploy-environment.yml + with: + ENVIRONMENT: stable + WEBAPI_ARTIFACT_NAME: ${{needs.build-webapi.outputs.artifact}} + MEMORYPIPELINE_ARTIFACT_NAME: ${{needs.build-memorypipeline.outputs.artifact}} + PLUGINS_ARTIFACT_NAME: ${{needs.build-plugins.outputs.artifact}} diff --git a/.github/workflows/copilot-deploy-plugins.yml b/.github/workflows/copilot-deploy-plugins.yml new file mode 100644 index 0000000..a5283bf --- /dev/null +++ b/.github/workflows/copilot-deploy-plugins.yml @@ -0,0 +1,63 @@ +name: copilot-deploy-plugins + +on: + workflow_call: + inputs: + ARTIFACT_NAME: + required: true + type: string + ENVIRONMENT: + required: true + type: string + DEPLOYMENT_NAME: + required: true + type: string + +permissions: + contents: read + id-token: write + +jobs: + plugins: + environment: ${{inputs.ENVIRONMENT}} + strategy: + fail-fast: false + matrix: + include: + - { dotnet: "6.0", configuration: Release, os: ubuntu-latest } + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + clean: true + + - name: Install Azure CLI + run: | + sudo apt update && sudo apt-get install curl -y + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + + - uses: actions/download-artifact@v4 + with: + name: ${{inputs.ARTIFACT_NAME}} + path: "${{ github.workspace }}/${{inputs.ARTIFACT_NAME}}" + + - name: "Display downloaded content" + run: ls -R + working-directory: "${{ github.workspace }}/${{inputs.ARTIFACT_NAME}}" + + - name: "Azure login" + uses: azure/login@v2 + with: + client-id: ${{vars.AZURE_GITHUB_ACCESS_APP_ID}} + tenant-id: ${{vars.AZURE_GITHUB_ACCESS_TENANT_ID}} + subscription-id: ${{vars.AZURE_GITHUB_ACCESS_SUB_ID}} + enable-AzPSSession: false + + - name: "Deploy" + run: | + scripts/deploy/deploy-plugins.sh \ + --deployment-name ${{inputs.DEPLOYMENT_NAME}} \ + --subscription ${{vars.AZURE_SUBSCRIPTION_ID}} \ + --resource-group ${{vars.CC_DEPLOYMENT_GROUP_NAME}} \ + --packages "${{ github.workspace }}/${{inputs.ARTIFACT_NAME}}" diff --git a/.github/workflows/copilot-run-integration-tests.yml b/.github/workflows/copilot-run-integration-tests.yml new file mode 100644 index 0000000..9b00c4f --- /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-test-e2e.yml b/.github/workflows/copilot-test-e2e.yml new file mode 100644 index 0000000..197e5c0 --- /dev/null +++ b/.github/workflows/copilot-test-e2e.yml @@ -0,0 +1,112 @@ +name: Copilot Chat Tests +on: + workflow_dispatch: + inputs: + ENVIRONMENT: + required: true + type: string + merge_group: + types: [checks_requested] + +permissions: + contents: read + +jobs: + e2e: + environment: ${{inputs.ENVIRONMENT}} + defaults: + run: + working-directory: webapp + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 16 + cache-dependency-path: webapp/yarn.lock + cache: "yarn" + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 6.0.x + + - name: Install dependencies + run: yarn install + + - name: Install Playwright Browsers + run: yarn playwright install --with-deps + + - name: Update AIService configuration + working-directory: webapi + env: + AzureOpenAI__ApiKey: ${{ secrets.AZUREOPENAI__APIKEY }} + run: | + dotnet dev-certs https + 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_TEST_USER_ACCOUNT1: ${{ secrets.COPILOT_CHAT_TEST_USER_ACCOUNT1 }} + REACT_APP_TEST_USER_ACCOUNT2: ${{ secrets.COPILOT_CHAT_TEST_USER_ACCOUNT2 }} + 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 }} + REACT_APP_TEST_JIRA_SERVER_URL: ${{ secrets.COPILOT_CHAT_TEST_JIRA_SERVER_URL }} + + REACT_APP_TEST_GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REACT_APP_TEST_GITHUB_ACCOUNT_OWNER: ${{ secrets.COPILOT_CHAT_TEST_GITHUB_ACCOUNT_OWNER }} + REACT_APP_TEST_GITHUB_REPOSITORY_NAME: ${{ secrets.COPILOT_CHAT_TEST_GITHUB_REPOSITORY_NAME }} + run: yarn playwright test + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: webapp/playwright-report/ + retention-days: 30 + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: service-log + path: webapi/service-log.txt + retention-days: 30 diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml new file mode 100644 index 0000000..39f33aa --- /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: 7.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 0000000..dd5f87a --- /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 new file mode 100644 index 0000000..1044cc6 --- /dev/null +++ b/.github/workflows/markdown-link-check-config.json @@ -0,0 +1,39 @@ +{ + "ignorePatterns": [ + { + "pattern": "/github/" + }, + { + "pattern": "./actions" + }, + { + "pattern": "./blob" + }, + { + "pattern": "./issues" + }, + { + "pattern": "./discussions" + }, + { + "pattern": "./pulls" + }, + { + "pattern": "^http://localhost" + }, + { + "pattern": "^https://localhost" + }, + { + "pattern": "^https://platform.openai.com" + }, + { + "pattern": "^https://stackoverflow.com" + } + ], + "timeout": "20s", + "retryOn429": true, + "retryCount": 3, + "fallbackRetryDelay": "30s", + "aliveStatusCodes": [200, 206, 429, 500, 503] +} diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml new file mode 100644 index 0000000..4e02c72 --- /dev/null +++ b/.github/workflows/markdown-link-check.yml @@ -0,0 +1,23 @@ +name: Check .md links + +on: + workflow_dispatch: + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + markdown-link-check: + runs-on: ubuntu-latest + # check out the latest version of the code + steps: + - uses: actions/checkout@v4 + + # Checks the status of hyperlinks in .md files in verbose mode + - name: Check links + uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + use-verbose-mode: 'yes' + config-file: ".github/workflows/markdown-link-check-config.json" diff --git a/.github/workflows/merge-gatekeeper.yml b/.github/workflows/merge-gatekeeper.yml new file mode 100644 index 0000000..372ea5f --- /dev/null +++ b/.github/workflows/merge-gatekeeper.yml @@ -0,0 +1,30 @@ +name: Merge Gatekeeper + +on: + pull_request: + branches: [ "main", "feature*" ] + merge_group: + branches: ["main"] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + merge-gatekeeper: + runs-on: ubuntu-latest + # Restrict permissions of the GITHUB_TOKEN. + # Docs: https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs + permissions: + checks: read + statuses: read + steps: + - name: Run Merge Gatekeeper + # NOTE: v1 is updated to reflect the latest v1.x.y. Please use any tag/branch that suits your needs: + # https://github.com/upsidr/merge-gatekeeper/tags + # https://github.com/upsidr/merge-gatekeeper/branches + uses: upsidr/merge-gatekeeper@v1 + if: github.event_name == 'pull_request' + with: + token: ${{ secrets.GITHUB_TOKEN }} + timeout: 1800 diff --git a/.github/workflows/typos.yaml b/.github/workflows/typos.yaml new file mode 100644 index 0000000..94931d4 --- /dev/null +++ b/.github/workflows/typos.yaml @@ -0,0 +1,29 @@ +# Check pull requests for typos. +# +# Configuration: .github/_typos.toml +# +# Info: https://github.com/marketplace/actions/typos-action +# Local install: brew install typos-cli +# Local install: conda install typos +# Local run: typos -c .github/_typos.toml + +name: Spell Check + +on: + workflow_dispatch: + pull_request: + branches: [ "main", "feature*" ] + +jobs: + run: + name: Spell Check with Typos + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Use custom config file + uses: crate-ci/typos@master + with: + config: .github/_typos.toml + write_changes: false diff --git a/.gitignore b/.gitignore index 8a30d25..a06144d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +dotnet/.config + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## @@ -396,3 +398,103 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +*.tmp +*.log +*.bck +*.tgz +*.tar +*.zip +*.cer +*.crt +*.key +*.pem + +.env +.env.production +certs/ +launchSettings.json +config.development.yaml +*.development.config +*.development.json +.DS_Store +.idea/ +node_modules/ +obj/ +bin/ +_dev/ +.dev/ +*.devis.* +.vs/ +*.user +**/.vscode/chrome +**/.vscode/.ropeproject/objectdb +*.pyc +.ipynb_checkpoints +.jython_cache/ +__pycache__/ +.mypy_cache/ +__pypackages__/ +.pdm.toml +global.json + +# doxfx +**/DROP/ +**/TEMP/ +**/packages/ +**/bin/ +**/obj/ +_site + +# Yarn +.yarn +.yarnrc.yml + +# Python Environments +.env +.venv +.myenv +env/ +venv/ +myvenv/ +ENV/ + +# Python dist +dist/ + +# Peristant storage +data/qdrant +data/chatstore* + +# Java build +java/**/target +java/.mvn/wrapper/maven-wrapper.jar + +# Java settings +conf.properties + +# Playwright +playwright-report/ + +# Static Web App deployment config +swa-cli.config.json +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 0000000..abd8d87 --- /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 0000000..8495ea1 --- /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 0000000..a714f66 --- /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": "explicit" + } + }, + "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": "explicit", + "source.fixAll": "explicit" + } + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit", + "source.fixAll": "explicit" + } + }, + "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 new file mode 100644 index 0000000..67e6a3b --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,134 @@ +{ + "version": "2.0.0", + "tasks": [ + // Copilot Chat + { + "label": "run (Copilot Chat)", + "detail": "Run all copilot chat components", + "group": "test", + "dependsOn": ["run (CopilotChatWebApi)", "run (CopilotChatApp)"], + "dependsOrder": "parallel" + }, + // Copilot Setup + { + "label": "install (CopilotChatApp)", + "detail": "Install all copilot chat app dependencies", + "type": "shell", + "group": "build", + "command": "yarn", + "presentation": { + "echo": true, + "reveal": "always", + "showReuseMessage": false, + "panel": "shared", + "group": "buildTasks" + }, + "options": { + "cwd": "${workspaceFolder}/webapp" + }, + "problemMatcher": [] + }, + { + "label": "setup (Copilot Chat)", + "detail": "Setup (like setting secrets) for copilot chat app and api", + "group": "test", + "dependsOn": ["GetSecret (AIService:Key)"], + "dependsOrder": "sequence" + // TODO -- add tasks for configuring environment variables + }, + { + "label": "GetSecret (AIService:Key)", + "command": "dotnet", + "type": "process", + "args": ["user-secrets", "set", "AIService:Key", "${input:aiServiceSecret}"], + "options": { + "cwd": "${workspaceFolder}/webapi" + } + }, + // Copilot Chat App + { + "label": "build (CopilotChatApp)", + "type": "shell", + "group": "build", + "command": "yarn build", + "presentation": { + "echo": true, + "reveal": "always", + "panel": "shared", + "group": "buildTasks" + }, + "options": { + "cwd": "${workspaceFolder}/webapp" + }, + "problemMatcher": [] + }, + { + "label": "run (CopilotChatApp)", + "type": "shell", + "group": "test", + "command": "yarn start", + "presentation": { + "reveal": "always", + "panel": "shared", + "group": "copilot" + }, + "options": { + "cwd": "${workspaceFolder}/webapp" + } + }, + // Copilot Chat Api + { + "label": "build (CopilotChatWebApi)", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/webapi/CopilotChatWebApi.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary", + "/property:DebugType=portable" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "run (CopilotChatWebApi)", + "command": "dotnet", + "type": "process", + "args": [ + "run", + "--project", + "${workspaceFolder}/webapi/CopilotChatWebApi.csproj" + ], + "problemMatcher": "$msCompile", + "group": "test", + "presentation": { + "reveal": "always", + "panel": "shared", + "group": "copilot" + } + }, + { + "label": "watch (CopilotChatWebApi)", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/webapi/CopilotChatWebApi.csproj" + ], + "problemMatcher": "$msCompile", + "group": "build" + } + ], + "inputs": [ + { + "id": "aiServiceSecret", + "type": "promptString", + "default": "", + "description": "Enter a secret for Copilot Chat AIService:Key", + "password": true + } + ] +} diff --git a/CopilotChat.sln b/CopilotChat.sln new file mode 100644 index 0000000..e79eb97 --- /dev/null +++ b/CopilotChat.sln @@ -0,0 +1,60 @@ +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 + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5252E68F-B653-44CE-9A32-360A75C54E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5252E68F-B653-44CE-9A32-360A75C54E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5252E68F-B653-44CE-9A32-360A75C54E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5252E68F-B653-44CE-9A32-360A75C54E0E}.Release|Any CPU.Build.0 = Release|Any CPU + {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 + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {971570D3-60EA-4EE4-980C-0BDA3E66E741} + EndGlobalSection +EndGlobal diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000..3f8d10c --- /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__ContentStorageType=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__ContentStorageType=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 0000000..6d37f08 --- /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 0000000..85e5756 --- /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:7.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:7.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 0000000..d4fd113 --- /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 0000000..6e59012 --- /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:6.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-isolated6.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 0000000..65e9115 --- /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 0000000..b0bb229 --- /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:7.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:7.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 0000000..3fc4671 --- /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 0000000..505a342 --- /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/integration-tests/ChatCopilotIntegrationTest.cs b/integration-tests/ChatCopilotIntegrationTest.cs new file mode 100644 index 0000000..84613fb --- /dev/null +++ b/integration-tests/ChatCopilotIntegrationTest.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Identity.Client; +using Xunit; + +namespace ChatCopilotIntegrationTests; + +/// +/// Base class for Chat Copilot integration tests +/// +[Trait("Category", "Integration Tests")] +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 SetUpAuth() + { + string accesstoken = await this.GetUserTokenByPassword(); + Assert.True(!string.IsNullOrEmpty(accesstoken)); + + this._httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accesstoken); + } + + protected async Task GetUserTokenByPassword() + { + IPublicClientApplication app = PublicClientApplicationBuilder.Create(this.configuration[ClientIdSettingName]) + .WithAuthority(this.configuration[AuthoritySettingName]) + .Build(); + + string? scopeString = this.configuration[ScopesSettingName]; + Assert.NotNull(scopeString); + + string[] scopes = scopeString.Split(new char[] { ',', ' ' }, 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 0000000..7856460 --- /dev/null +++ b/integration-tests/ChatCopilotIntegrationTests.csproj @@ -0,0 +1,39 @@ + + + + net6.0 + enable + false + 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 0000000..6e05d5b --- /dev/null +++ b/integration-tests/ChatTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Net.Http; +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 +{ + [Fact] + public async void ChatMessagePostSucceedsWithValidInput() + { + await this.SetUpAuth(); + + // Create chat session + var createChatParams = new CreateChatParameters() { Title = nameof(ChatMessagePostSucceedsWithValidInput) }; + HttpResponseMessage response = await this._httpClient.PostAsJsonAsync("chats", createChatParams); + response.EnsureSuccessStatusCode(); + + var contentStream = await response.Content.ReadAsStreamAsync(); + var createChatResponse = await JsonSerializer.DeserializeAsync(contentStream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + 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, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + Assert.NotNull(askResult); + Assert.False(string.IsNullOrEmpty(askResult.Value)); + + + // Clean up + response = await this._httpClient.DeleteAsync($"chats/{createChatResponse.ChatSession.Id}"); + response.EnsureSuccessStatusCode(); + } +} + diff --git a/integration-tests/HealthzTests.cs b/integration-tests/HealthzTests.cs new file mode 100644 index 0000000..f14b006 --- /dev/null +++ b/integration-tests/HealthzTests.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +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 0000000..6674822 --- /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 0000000..4cf8039 --- /dev/null +++ b/integration-tests/ServiceInfoTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +using System.Text.Json; +using CopilotChat.WebApi.Models.Response; +using CopilotChat.WebApi.Options; +using Xunit; + +namespace ChatCopilotIntegrationTests; + +public class ServiceInfoTests : ChatCopilotIntegrationTest +{ + [Fact] + public async void GetServiceInfo() + { + await this.SetUpAuth(); + + HttpResponseMessage response = await this._httpClient.GetAsync("info/"); + response.EnsureSuccessStatusCode(); + + var contentStream = await response.Content.ReadAsStreamAsync(); + var objectFromResponse = await JsonSerializer.DeserializeAsync(contentStream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + 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, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + 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 0000000..7873aa0 --- /dev/null +++ b/integration-tests/SpeechTokenTests.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +using System.Text.Json; +using CopilotChat.WebApi.Models.Response; +using Xunit; + +namespace ChatCopilotIntegrationTests; + +public class SpeechTokenTests : ChatCopilotIntegrationTest +{ + [Fact] + public async void GetSpeechToken() + { + await this.SetUpAuth(); + + HttpResponseMessage response = await this._httpClient.GetAsync("speechToken/"); + response.EnsureSuccessStatusCode(); + + var contentStream = await response.Content.ReadAsStreamAsync(); + var speechTokenResponse = await JsonSerializer.DeserializeAsync(contentStream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + 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 0000000..3926d28 --- /dev/null +++ b/integration-tests/StaticFiles.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net.Http; +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 0000000..2ec088a --- /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 0000000..716e43d --- /dev/null +++ b/memorypipeline/CopilotChatMemoryPipeline.csproj @@ -0,0 +1,22 @@ + + + + CopilotChat.MemoryPipeline + net6.0 + LatestMajor + disable + enable + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + + + + + + + + + + + + + diff --git a/memorypipeline/Program.cs b/memorypipeline/Program.cs new file mode 100644 index 0000000..93157e3 --- /dev/null +++ b/memorypipeline/Program.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using CopilotChat.Shared; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.KernelMemory; +using Microsoft.KernelMemory.Diagnostics; + +// ******************************************************** +// ************** SETUP *********************************** +// ******************************************************** + +var builder = WebApplication.CreateBuilder(); + +IKernelMemory memory = + new KernelMemoryBuilder(builder.Services) + .FromAppSettings() + .WithCustomOcr(builder.Configuration) + .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 0000000..2700474 --- /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 `SemanticMemory: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 `SemanticMemory: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 SemanticMemory:Services:AzureBlobs:Auth ConnectionString + dotnet user-secrets set SemanticMemory:Services:AzureBlobs:ConnectionString [your secret] + dotnet user-secrets set SemanticMemory:Services:AzureQueue:Auth ConnectionString + dotnet user-secrets set SemanticMemory: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 SemanticMemory:Services:AzureAISearch:Endpoint [your secret] + dotnet user-secrets set SemanticMemory: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 0000000..932afd2 --- /dev/null +++ b/memorypipeline/appsettings.json @@ -0,0 +1,227 @@ +{ + // + // Kernel Memory configuration - https://github.com/microsoft/kernel-memory + // - ContentStorageType 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 "AzureFormRecognizer" 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": { + "ContentStorageType": "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 SemanticMemory 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 "SemanticMemory:Services:AzureBlobs:ConnectionString" "MY_AZUREBLOB_CONNECTIONSTRING" + //"Account": "", + "Container": "memorypipeline" + //"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 "SemanticMemory: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 "SemanticMemory:Services:RabbitMq:Username" "MY_RABBITMQ_USER" + //"Password": "", // dotnet user-secrets set "SemanticMemory: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 "SemanticMemory: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 "SemanticMemory: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-35-turbo, gpt-4). + // - 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 "SemanticMemory:Services:AzureOpenAIText:APIKey" "MY_AZUREOPENAI_KEY" + "Endpoint": "", + "Deployment": "gpt-35-turbo", + "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 a embedding model (e.g., gpt-35-turbo, gpt-4). + // + "AzureOpenAIEmbedding": { + "Auth": "ApiKey", + // "APIKey": "", // dotnet user-secrets set "SemanticMemory: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-35-turbo, gpt-4). + // - 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 "SemanticMemory: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. + // + "AzureFormRecognizer": { + "Auth": "APIKey", + //"APIKey": "", // dotnet user-secrets set "SemanticMemory:Services:AzureFormRecognizer:APIKey" "MY_AZUREFORMRECOGNIZER_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/README.md b/plugins/README.md new file mode 100644 index 0000000..28ce816 --- /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 0000000..5335aa7 --- /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 0000000..445038e --- /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 0000000..0223233 --- /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 0000000..cd1e84c --- /dev/null +++ b/plugins/shared/PluginShared.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + 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 0000000000000000000000000000000000000000..3da002c0cd5ecf9ba5c83990330129c107b8eaf6 GIT binary patch literal 9653 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 +/// 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 0000000..2d46115 --- /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 0000000..ba8b825 --- /dev/null +++ b/plugins/web-searcher/PluginEndpoint.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +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.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 WellKnownAIPluginManifest( + [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 Icon( + [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: new[] { "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 WebSearch([HttpTrigger(AuthorizationLevel.Function, "get", Route = "search")] HttpRequestData req) + { + var queries = QueryHelpers.ParseQuery(req.Url.Query); + var query = queries.ContainsKey("Query") ? queries["Query"].ToString() : string.Empty; + if (string.IsNullOrWhiteSpace(query)) + { + return await this.CreateBadRequestResponseAsync(req, "Empty query."); + } + + var numResults = queries.ContainsKey("NumResults") ? int.Parse(queries["NumResults"]) : 0; + if (numResults <= 0) + { + return await this.CreateBadRequestResponseAsync(req, "Invalid number of results."); + } + + int offset = 0; + if (queries.TryGetValue("Offset", out var offsetValue)) + { + int.TryParse(offsetValue, out offset); + } + + var site = queries.ContainsKey("Site") ? queries["Site"].ToString() : string.Empty; + 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 0000000..edf0bc3 --- /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 0000000..e9709a6 --- /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 0000000..8b35b8c --- /dev/null +++ b/plugins/web-searcher/WebSearcher.csproj @@ -0,0 +1,36 @@ + + + net6.0 + v4 + Exe + enable + 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 0000000..3ca80eb --- /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 0000000..0e06eb2 --- /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/Configure.ps1 b/scripts/Configure.ps1 new file mode 100644 index 0000000..9481952 --- /dev/null +++ b/scripts/Configure.ps1 @@ -0,0 +1,212 @@ +<# +.SYNOPSIS +Configure user secrets, appsettings.Development.json, and webapp/.env for Chat Copilot. + +.PARAMETER AIService +The service type used: OpenAI or AzureOpenAI. + +.PARAMETER APIKey +The API key for the AI service. + +.PARAMETER Endpoint +Set when using Azure OpenAI. + +.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 FrontendClientId +The client (application) ID associated with your frontend's AAD app registration. + +.PARAMETER BackendClientId +The client (application) ID associated with your backend's AAD app registration. + +.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(Mandatory = $true)] + [string]$AIService, + + [Parameter(Mandatory = $true)] + [string]$APIKey, + + [Parameter(Mandatory = $false)] + [string]$Endpoint, + + [Parameter(Mandatory = $false)] + [string]$CompletionModel, + + [Parameter(Mandatory = $false)] + [string]$EmbeddingModel, + + [Parameter(Mandatory = $false)] + [string] $FrontendClientId, + + [Parameter(Mandatory = $false)] + [string] $BackendClientId, + + [Parameter(Mandatory = $false)] + [string] $TenantId, + + [Parameter(Mandatory = $false)] + [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) { + dotnet dev-certs https + if ($LASTEXITCODE -ne 0) { exit(1) } +} +else { + # Windows/MacOS + dotnet dev-certs https --trust + if ($LASTEXITCODE -ne 0) { exit(1) } +} + +$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 { + 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 = @{ + 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 '$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 "========" +Get-Content $appsettingsOverridesFilePath | Write-Host +Write-Host "========" + +Write-Host "" +Write-Host "##########################" +Write-Host "# Frontend configuration #" +Write-Host "##########################" + +$webappProjectPath = Join-Path "$PSScriptRoot" '../webapp' +$webappEnvFilePath = Join-Path "$webappProjectPath" '/.env' + +Write-Host "Setting up '.env'..." +Set-Content -Path $webappEnvFilePath -Value "REACT_APP_BACKEND_URI=https://localhost:40443/" + +Write-Host "($webappEnvFilePath)" +Write-Host "========" +Get-Content $webappEnvFilePath | Write-Host +Write-Host "========" + +Write-Host "Done!" diff --git a/scripts/Install.ps1 b/scripts/Install.ps1 new file mode 100644 index 0000000..f6d95ce --- /dev/null +++ b/scripts/Install.ps1 @@ -0,0 +1,33 @@ +<# +.SYNOPSIS +Installs the requirements for running Chat Copilot. +#> + +if ($IsLinux) +{ + Write-Host "ERROR: This script is not supported for your operating system." + exit 1; +} + +Set-ExecutionPolicy Bypass -Scope Process -Force +[System.Net.ServicePointManager]::SecurityProtocol = 3072 + +# Install chocolatey if not already installed +$ChocoInstalled = $false +if (Get-Command choco.exe -ErrorAction SilentlyContinue) { + $ChocoInstalled = $true +} +if (!$ChocoInstalled) +{ + Write-Host "Installing Chocolatey..." + Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1') + $env:PATH += ";%ALLUSERSPROFILE%\chocolatey\bin" + refreshenv +} + +# Ensure required packages are installed +$Packages = 'dotnet-7.0-sdk', 'nodejs', 'yarn' +foreach ($PackageName in $Packages) +{ + choco install $PackageName -y +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..5f5e3eb --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,3 @@ +# Chat Copilot setup scripts - Local deployment + +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 new file mode 100644 index 0000000..2e9a5f4 --- /dev/null +++ b/scripts/Start-Backend.ps1 @@ -0,0 +1,19 @@ +<# +.SYNOPSIS +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-Frontend.ps1 b/scripts/Start-Frontend.ps1 new file mode 100644 index 0000000..c8f059f --- /dev/null +++ b/scripts/Start-Frontend.ps1 @@ -0,0 +1,8 @@ +<# +.SYNOPSIS +Builds and runs the Chat Copilot frontend. +#> + +Join-Path "$PSScriptRoot" '../webapp' | Set-Location +yarn install +yarn start diff --git a/scripts/Start.ps1 b/scripts/Start.ps1 new file mode 100644 index 0000000..66cb99e --- /dev/null +++ b/scripts/Start.ps1 @@ -0,0 +1,52 @@ +<# +.SYNOPSIS +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 "-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 +} + +# 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 "*************************************************" +} diff --git a/scripts/Variables.ps1 b/scripts/Variables.ps1 new file mode 100644 index 0000000..3d75b5c --- /dev/null +++ b/scripts/Variables.ps1 @@ -0,0 +1,15 @@ +# Default environment file to be read by scripts + +# Default values +$varCompletionModelOpenAI = "gpt-3.5-turbo" +$varCompletionModelAzureOpenAI = "gpt-35-turbo" +$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 100644 index 0000000..7cf2a59 --- /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 0000000..5b93a17 --- /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/scripts/deploy/deploy-azure.ps1 b/scripts/deploy/deploy-azure.ps1 new file mode 100644 index 0000000..7dd9127 --- /dev/null +++ b/scripts/deploy/deploy-azure.ps1 @@ -0,0 +1,169 @@ +<# +.SYNOPSIS +Deploy Chat Copilot Azure resources +#> + +param( + [Parameter(Mandatory)] + [string] + # Name for the deployment + $DeploymentName, + + [Parameter(Mandatory)] + [string] + # Subscription to which to make the deployment + $Subscription, + + [Parameter(Mandatory)] + [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 = "AzureOpenAI", + + [string] + # API key for existing Azure OpenAI resource or OpenAI account + $AIApiKey, + + # Endpoint for existing Azure OpenAI resource + [string] + $AIEndpoint, + + [string] + # Resource group to which to make the deployment + $ResourceGroup, + + [string] + # Region to which to make the deployment + $Region = "southcentralus", + + [string] + # SKU for the Azure App Service plan + $WebAppServiceSku = "B1", + + [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 + $NoCosmosDb, + + [switch] + # 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, + + [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, 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." + } + else { + $DeployAzureOpenAI = $false + } +} + +# if AIService is OpenAI then $AIApiKey is mandatory. +if ($AIService -eq "OpenAI" -and !$AIApiKey) { + Write-Error "When AIService is OpenAI, AIApiKey must be set." + exit 1 +} + +$jsonConfig = " +{ + `\`"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"}) }, + `\`"memoryStore`\`": { `\`"value`\`": `\`"$MemoryStore`\`" }, + `\`"deployCosmosDB`\`": { `\`"value`\`": $(If (!($NoCosmosDb)) {"true"} Else {"false"}) }, + `\`"deploySpeechServices`\`": { `\`"value`\`": $(If (!($NoSpeechServices)) {"true"} Else {"false"}) }, + `\`"deployWebSearcherPlugin`\`": { `\`"value`\`": $(If ($DeployWebSearcherPlugin) {"true"} Else {"false"}) } +} +" + +$jsonConfig = $jsonConfig -replace '\s', '' + +$ErrorActionPreference = "Stop" + +$templateFile = "$($PSScriptRoot)/main.bicep" + +if (!$ResourceGroup) { + $ResourceGroup = "rg-" + $DeploymentName +} + +az account show --output none +if ($LASTEXITCODE -ne 0) { + Write-Host "Log into your Azure account" + az login --output none +} + +az account set -s $Subscription +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +Write-Host "Ensuring resource group '$ResourceGroup' exists..." +az group create --location $Region --name $ResourceGroup --tags Creator=$env:UserName +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +Write-Host "Validating template file..." +az deployment group validate --name $DeploymentName --resource-group $ResourceGroup --template-file $templateFile --parameters $jsonConfig +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +Write-Host "Deploying Azure resources ($DeploymentName)..." +if ($DebugDeployment) { + az deployment group create --name $DeploymentName --resource-group $ResourceGroup --template-file $templateFile --debug --parameters $jsonConfig +} +else { + az deployment group create --name $DeploymentName --resource-group $ResourceGroup --template-file $templateFile --parameters $jsonConfig +} diff --git a/scripts/deploy/deploy-azure.sh b/scripts/deploy/deploy-azure.sh new file mode 100644 index 0000000..e6df71d --- /dev/null +++ b/scripts/deploy/deploy-azure.sh @@ -0,0 +1,225 @@ +#!/usr/bin/env bash + +# Deploy Chat Copilot Azure resources. + +set -e + +usage() { + 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 " -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 " -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) + DEPLOYMENT_NAME="$2" + shift + shift + ;; + -s | --subscription) + SUBSCRIPTION="$2" + shift + shift + ;; + -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) + AI_SERVICE_KEY="$2" + shift + shift + ;; + -aiend | --ai-endpoint) + AI_ENDPOINT="$2" + shift + shift + ;; + -rg | --resource-group) + RESOURCE_GROUP="$2" + shift + shift + ;; + -r | --region) + REGION="$2" + shift + shift + ;; + -a | --app-service-sku) + WEB_APP_SVC_SKU="$2" + shift + shift + ;; + -i | --instance) + AZURE_AD_INSTANCE="$2" + shift + shift + ;; + -ms | --memory-store) + MEMORY_STORE=="$2" + shift + ;; + -nc | --no-cosmos-db) + NO_COSMOS_DB=true + shift + ;; + -ns | --no-speech-services) + NO_SPEECH_SERVICES=true + shift + ;; + -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 + ;; + esac +done + +# Check mandatory arguments +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 + +# Check if AI_SERVICE_TYPE is either OpenAI or AzureOpenAI +if [[ "${AI_SERVICE_TYPE,,}" != "openai" ]] && [[ "${AI_SERVICE_TYPE,,}" != "azureopenai" ]]; then + echo "--ai-service must be either OpenAI or AzureOpenAI" + usage + exit 1 +fi + +# if AI_SERVICE_TYPE is AzureOpenAI +if [[ "${AI_SERVICE_TYPE,,}" = "azureopenai" ]]; then + # Both AI_ENDPOINT and AI_SERVICE_KEY must be set or neither of them. + if [[ (-z "$AI_ENDPOINT" && -n "$AI_SERVICE_KEY") || (-n "$AI_ENDPOINT" && -z "$AI_SERVICE_KEY") ]]; then + echo "When --ai is 'AzureOpenAI', if either --ai-endpoint or --ai-service-key is set, then both must be set." + usage + exit 1 + fi + + # if AI_ENDPOINT and AI_SERVICE_KEY are not set, set NO_NEW_AZURE_OPENAI to false and tell the user, else set NO_NEW_AZURE_OPENAI to true + if [[ -z "$AI_ENDPOINT" ]] && [[ -z "$AI_SERVICE_KEY" ]]; then + NO_NEW_AZURE_OPENAI=false + echo "When --ai is 'AzureOpenAI', if neither --ai-endpoint nor --ai-service-key are set, then a new Azure OpenAI resource will be created." + else + NO_NEW_AZURE_OPENAI=true + echo "When --ai is 'AzureOpenAI', if both --ai-endpoint and --ai-service-key are set, then an existing Azure OpenAI resource will be used." + fi +fi + +# if AI_SERVICE_TYPE is OpenAI then AI_SERVICE_KEY is mandatory +if [[ "${AI_SERVICE_TYPE,,}" = "openai" ]] && [[ -z "$AI_SERVICE_KEY" ]]; then + echo "When --ai is 'OpenAI', --ai-service-key must be set." + usage + exit 1 +fi + +# If resource group is not set, then set it to rg-DEPLOYMENT_NAME +if [ -z "$RESOURCE_GROUP" ]; then + RESOURCE_GROUP="rg-${DEPLOYMENT_NAME}" +fi + +TEMPLATE_FILE="$(dirname "$0")/main.bicep" + +az account show --output none +if [ $? -ne 0 ]; then + echo "Log into your Azure account" + az login --use-device-code +fi + +az account set -s "$SUBSCRIPTION" + +# Set defaults +: "${REGION:="southcentralus"}" +: "${WEB_APP_SVC_SKU:="B1"}" +: "${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 < + +param( + [Parameter(Mandatory)] + [string] + # Subscription to which to make the deployment + $Subscription, + + [Parameter(Mandatory)] + [string] + # Resource group to which to make the deployment + $ResourceGroupName, + + [Parameter(Mandatory)] + [string] + # Name of the previously deployed Azure deployment + $DeploymentName, + + [string] + # CopilotChat 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-memorypipeline.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..." +$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: $memoryPipelineName" + +Write-Host "Configuring Azure WebApp to run from package..." +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 '$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/scripts/deploy/deploy-memorypipeline.sh b/scripts/deploy/deploy-memorypipeline.sh new file mode 100644 index 0000000..a2b79ba --- /dev/null +++ b/scripts/deploy/deploy-memorypipeline.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# Deploy CopilotChat's MemoryPipeline to Azure. + +set -e + +usage() { + echo "Usage: $0 -d DEPLOYMENT_NAME -s SUBSCRIPTION -rg RESOURCE_GROUP [OPTIONS]" + echo "" + echo "Arguments:" + echo " -d, --deployment-name DEPLOYMENT_NAME Name of the deployment from a 'deploy-azure.sh' deployment (mandatory)" + echo " -s, --subscription SUBSCRIPTION Subscription to which to make the deployment (mandatory)" + echo " -rg, --resource-group RESOURCE_GROUP Resource group name from a 'deploy-azure.sh' deployment (mandatory)" + echo " -p, --package PACKAGE_FILE_PATH Path to the MemoryPipeline package file from a 'package-memorypipeline.sh' run" +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + key="$1" + case $key in + -d|--deployment-name) + DEPLOYMENT_NAME="$2" + shift + shift + ;; + -s|--subscription) + SUBSCRIPTION="$2" + shift + shift + ;; + -rg|--resource-group) + RESOURCE_GROUP="$2" + shift + shift + ;; + -p|--package) + PACKAGE_FILE_PATH="$2" + shift + shift + ;; + *) + echo "Unknown option $1" + usage + exit 1 + ;; + esac +done + +# Check mandatory arguments +if [[ -z "$DEPLOYMENT_NAME" ]] || [[ -z "$SUBSCRIPTION" ]] || [[ -z "$RESOURCE_GROUP" ]]; then + usage + exit 1 +fi + +# Set defaults +: "${PACKAGE_FILE_PATH:="$(dirname "$0")/out/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-memorypipeline.sh' yet?" + exit 1 +fi + +az account show --output none +if [ $? -ne 0 ]; then + echo "Log into your Azure account" + az login --use-device-code +fi + +az account set -s "$SUBSCRIPTION" + +echo "Getting Azure WebApp resource name..." +eval 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: $MEMORY_PIPELINE_NAME" + +echo "Configuring Azure WebApp to run from package..." +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 '$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 '$PACKAGE_FILE_PATH' to Azure WebApp '$MEMORY_PIPELINE_NAME'." + exit 1 +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 0000000..ffb2946 --- /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 100644 index 0000000..f681ce3 --- /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 0000000..cc5fa50 --- /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 100644 index 0000000..764fe75 --- /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 0000000..756affb --- /dev/null +++ b/scripts/deploy/main.bicep @@ -0,0 +1,1148 @@ +/* +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-35-turbo' + +@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:ContentStorageType' + 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: { + 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:ContentStorageType' + 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 0000000..27b231f --- /dev/null +++ b/scripts/deploy/main.json @@ -0,0 +1,1159 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.25.53.49325", + "templateHash": "1335279082624698663" + } + }, + "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-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" + } + }, + "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:ContentStorageType', '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": { + "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:ContentStorageType", + "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/scripts/deploy/package-memorypipeline.ps1 b/scripts/deploy/package-memorypipeline.ps1 new file mode 100644 index 0000000..5b5c4d1 --- /dev/null +++ b/scripts/deploy/package-memorypipeline.ps1 @@ -0,0 +1,56 @@ +<# +.SYNOPSIS +Package CopilotChat's MemoryPipeline for deployment to Azure +#> + +param( + [string] + # Build configuration to publish. + $BuildConfiguration = "Release", + + [string] + # .NET framework to publish. + $DotNetFramework = "net6.0", + + [string] + # Target runtime to publish. + $TargetRuntime = "win-x64", + + [string] + # Output directory for published assets. + $OutputDirectory = "$PSScriptRoot", + + [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" +$publishedZipFilePath = "$publishedZipDirectory/memorypipeline.zip" +if (!(Test-Path $publishedZipDirectory)) { + New-Item -ItemType Directory -Force -Path $publishedZipDirectory | Out-Null +} +if (!(Test-Path $publishOutputDirectory)) { + New-Item -ItemType Directory -Force -Path $publishOutputDirectory | Out-Null +} + +Write-Host "Build configuration: $BuildConfiguration" +dotnet publish "$PSScriptRoot/../../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 +} + +Write-Host "Compressing to $publishedZipFilePath" +Compress-Archive -Path $publishOutputDirectory\* -DestinationPath $publishedZipFilePath -Force + +Write-Host "Published memorypipeline package to '$publishedZipFilePath'" \ No newline at end of file diff --git a/scripts/deploy/package-memorypipeline.sh b/scripts/deploy/package-memorypipeline.sh new file mode 100644 index 0000000..6495e3c --- /dev/null +++ b/scripts/deploy/package-memorypipeline.sh @@ -0,0 +1,109 @@ +#!/bin/bash + +# Package CiopilotChat's MemoryPipeline 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: net6.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)" +} + +# 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 + ;; + *) + echo "Unknown option $1" + usage + exit 1 + ;; + esac +done + +# Set defaults +: "${CONFIGURATION:="Release"}" +: "${DOTNET:="net6.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/memorypipeline.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/../../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 + +# 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/deploy/package-plugins.ps1 b/scripts/deploy/package-plugins.ps1 new file mode 100644 index 0000000..3489b72 --- /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 = "net6.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 0000000..86bb28a --- /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: net6.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:="net6.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 100644 index 0000000..47569e0 --- /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 = "net6.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 0000000..f5f12e5 --- /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: net6.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:="net6.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-apt.sh b/scripts/install-apt.sh new file mode 100644 index 0000000..ab1850f --- /dev/null +++ b/scripts/install-apt.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Installs the requirements for running Chat Copilot. + +set -e + +# Add Yarn's package repository to the system +curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list + +# Add .NET's package repository to the system +wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb +sudo dpkg -i packages-microsoft-prod.deb +rm packages-microsoft-prod.deb + +# Add NodeJS's package repository to the system +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + +# Install the requirements +sudo apt update; +sudo apt install yarn -y; +sudo apt install dotnet-sdk-7.0 -y; +sudo apt install nodejs -y; + +echo "" +echo "YARN $(yarn --version) installed." +echo "NODEJS $(node --version) installed." +echo "DOTNET $(dotnet --version) installed." diff --git a/scripts/install-brew.sh b/scripts/install-brew.sh new file mode 100644 index 0000000..4b4be44 --- /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 100644 index 0000000..c5bc2b8 --- /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 100644 index 0000000..0421a97 --- /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 100644 index 0000000..9496ca1 --- /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 0000000..e3c5a0b --- /dev/null +++ b/shared/ConfigurationBuilderExtensions.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.KernelMemory.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 ConfigurationException($"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 0000000..c5d38a8 --- /dev/null +++ b/shared/CopilotChatShared.csproj @@ -0,0 +1,19 @@ + + + + CopilotChat.Shared + net6.0 + LatestMajor + disable + enable + + + + + + + + + + + diff --git a/shared/KernelMemoryBuilderExtensions.cs b/shared/KernelMemoryBuilderExtensions.cs new file mode 100644 index 0000000..1b82ff7 --- /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/MemoryClientBuilderExtensions.cs b/shared/MemoryClientBuilderExtensions.cs new file mode 100644 index 0000000..e9d3f21 --- /dev/null +++ b/shared/MemoryClientBuilderExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +using CopilotChat.Shared.Ocr; +using Microsoft.Extensions.Configuration; +using Microsoft.KernelMemory; + +namespace CopilotChat.Shared; + +/// +/// Dependency injection for kernel memory using custom OCR configuration defined in appsettings.json +/// +public static class MemoryClientBuilderExtensions +{ + public static IKernelMemoryBuilder WithCustomOcr(this IKernelMemoryBuilder builder, IConfiguration configuration) + { + var ocrEngine = configuration.CreateCustomOcr(); + + if (ocrEngine is not null) + { + builder.WithCustomImageOcr(ocrEngine); + } + + return builder; + } +} diff --git a/shared/MemoryConfiguration.cs b/shared/MemoryConfiguration.cs new file mode 100644 index 0000000..1af9750 --- /dev/null +++ b/shared/MemoryConfiguration.cs @@ -0,0 +1,13 @@ +// 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/shared/Ocr/ConfigurationExtensions.cs b/shared/Ocr/ConfigurationExtensions.cs new file mode 100644 index 0000000..3d477e2 --- /dev/null +++ b/shared/Ocr/ConfigurationExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using CopilotChat.Shared.Ocr.Tesseract; +using Microsoft.Extensions.Configuration; +using Microsoft.KernelMemory.Configuration; +using Microsoft.KernelMemory.DataFormats; + +namespace CopilotChat.Shared.Ocr; + +/// +/// Dependency injection for kernel memory using configuration defined in appsettings.json +/// +public static class ConfigurationExtensions +{ + private const string ConfigOcrType = "ImageOcrType"; + + public static IOcrEngine? CreateCustomOcr(this IConfiguration configuration) + { + var ocrType = configuration.GetSection($"{MemoryConfiguration.KernelMemorySection}:{ConfigOcrType}").Value ?? string.Empty; + switch (ocrType) + { + case string x when x.Equals(TesseractOptions.SectionName, StringComparison.OrdinalIgnoreCase): + var tesseractOptions = + configuration + .GetSection($"{MemoryConfiguration.KernelMemorySection}:{MemoryConfiguration.ServicesSection}:{TesseractOptions.SectionName}") + .Get(); + + if (tesseractOptions == null) + { + throw new ConfigurationException($"Missing configuration for {ConfigOcrType}: {ocrType}"); + } + + return new TesseractOcrEngine(tesseractOptions); + + default: // Allow for fall-through for standard OCR settings + break; + } + + return null; + } +} diff --git a/shared/Ocr/Tesseract/TesseractOcrEngine.cs b/shared/Ocr/Tesseract/TesseractOcrEngine.cs new file mode 100644 index 0000000..75881c9 --- /dev/null +++ b/shared/Ocr/Tesseract/TesseractOcrEngine.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.KernelMemory.DataFormats; +using Tesseract; + +namespace CopilotChat.Shared.Ocr.Tesseract; + +/// +/// Wrapper for the TesseractEngine within the Tesseract OCR library. +/// +public class TesseractOcrEngine : IOcrEngine +{ + private readonly TesseractEngine _engine; + + /// + /// Creates a new instance of the TesseractEngineWrapper passing in a valid TesseractEngine. + /// + public TesseractOcrEngine(TesseractOptions tesseractOptions) + { + this._engine = new TesseractEngine(tesseractOptions.FilePath, tesseractOptions.Language); + } + + /// + public async Task ExtractTextFromImageAsync(Stream imageContent, CancellationToken cancellationToken = default) + { + await using (var imgStream = new MemoryStream()) + { + await imageContent.CopyToAsync(imgStream); + imgStream.Position = 0; + + using var img = Pix.LoadFromMemory(imgStream.ToArray()); + + using var page = this._engine.Process(img); + return page.GetText(); + } + } +} diff --git a/shared/Ocr/Tesseract/TesseractOptions.cs b/shared/Ocr/Tesseract/TesseractOptions.cs new file mode 100644 index 0000000..87c3182 --- /dev/null +++ b/shared/Ocr/Tesseract/TesseractOptions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel.DataAnnotations; + +namespace CopilotChat.Shared.Ocr.Tesseract; + +/// +/// Configuration options for Tesseract OCR support. +/// +public sealed class TesseractOptions +{ + public const string SectionName = "Tesseract"; + + /// + /// The file path where the Tesseract language file is stored (e.g. "./data") + /// + [Required] + public string? FilePath { get; set; } = string.Empty; + + /// + /// The language file prefix name (e.g. "eng") + /// + [Required] + public string? Language { get; set; } = string.Empty; +} diff --git a/shared/ServiceConfiguration.cs b/shared/ServiceConfiguration.cs new file mode 100644 index 0000000..7fd116d --- /dev/null +++ b/shared/ServiceConfiguration.cs @@ -0,0 +1,513 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.KernelMemory; +using Microsoft.KernelMemory.AI; +using Microsoft.KernelMemory.Configuration; +using Microsoft.KernelMemory.ContentStorage.DevTools; +using Microsoft.KernelMemory.MemoryStorage; +using Microsoft.KernelMemory.MemoryStorage.DevTools; +using Microsoft.KernelMemory.Pipeline.Queue.DevTools; +using Microsoft.KernelMemory.Postgres; + +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"; + + // ASP.NET env var + private const string AspnetEnvVar = "ASPNETCORE_ENVIRONMENT"; + + // 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 y1 when y1.Equals("AzureQueue", StringComparison.OrdinalIgnoreCase): + case string y2 when y2.Equals("AzureQueues", StringComparison.OrdinalIgnoreCase): + // Check 2 keys for backward compatibility + builder.Services.AddAzureQueuesOrchestration(this.GetServiceConfig("AzureQueue")); + break; + + case string y when y.Equals("RabbitMQ", StringComparison.OrdinalIgnoreCase): + // Check 2 keys for backward compatibility + 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.ContentStorageType) + { + case string x1 when x1.Equals("AzureBlob", StringComparison.OrdinalIgnoreCase): + case string x2 when x2.Equals("AzureBlobs", StringComparison.OrdinalIgnoreCase): + // Check 2 keys for backward compatibility + builder.Services.AddAzureBlobsAsContentStorage(this.GetServiceConfig("AzureBlobs") + ?? this.GetServiceConfig("AzureBlob")); + break; + + case string x when x.Equals("SimpleFileStorage", StringComparison.OrdinalIgnoreCase): + builder.Services.AddSimpleFileStorageAsContentStorage(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(this.GetServiceConfig("AzureOpenAIEmbedding"))); + builder.AddIngestionEmbeddingGenerator(instance); + break; + } + + case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): + { + var instance = this.GetServiceInstance(builder, + s => s.AddOpenAITextEmbeddingGeneration(this.GetServiceConfig("OpenAI"))); + 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("Qdrant", StringComparison.OrdinalIgnoreCase): + { + var instance = this.GetServiceInstance(builder, + s => s.AddQdrantAsMemoryDb(this.GetServiceConfig("Qdrant")) + ); + 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("Redis", StringComparison.OrdinalIgnoreCase): + { + var instance = this.GetServiceInstance(builder, + s => s.AddRedisAsMemoryDb(this.GetServiceConfig("Redis")) + ); + 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("SimpleTextDb", StringComparison.OrdinalIgnoreCase): + { + var instance = this.GetServiceInstance(builder, + s => s.AddSimpleTextDbAsMemoryDb(this.GetServiceConfig("SimpleTextDb")) + ); + 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(this.GetServiceConfig("AzureOpenAIEmbedding")); + break; + + case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): + builder.Services.AddOpenAITextEmbeddingGeneration(this.GetServiceConfig("OpenAI")); + 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("Qdrant", StringComparison.OrdinalIgnoreCase): + builder.Services.AddQdrantAsMemoryDb(this.GetServiceConfig("Qdrant")); + break; + + case string x when x.Equals("Postgres", StringComparison.OrdinalIgnoreCase): + builder.Services.AddPostgresAsMemoryDb(this.GetServiceConfig("Postgres")); + break; + + case string x when x.Equals("Redis", StringComparison.OrdinalIgnoreCase): + builder.Services.AddRedisAsMemoryDb(this.GetServiceConfig("Redis")); + break; + + case string x when x.Equals("SimpleVectorDb", StringComparison.OrdinalIgnoreCase): + builder.Services.AddSimpleVectorDbAsMemoryDb(this.GetServiceConfig("SimpleVectorDb")); + break; + + case string x when x.Equals("SimpleTextDb", StringComparison.OrdinalIgnoreCase): + builder.Services.AddSimpleTextDbAsMemoryDb(this.GetServiceConfig("SimpleTextDb")); + 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(this.GetServiceConfig("AzureOpenAIText")); + break; + + case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): + builder.Services.AddOpenAITextGeneration(this.GetServiceConfig("OpenAI")); + 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; + + 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/tools/importdocument/Config.cs b/tools/importdocument/Config.cs new file mode 100644 index 0000000..7c75652 --- /dev/null +++ b/tools/importdocument/Config.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.Configuration; + +namespace ImportDocument; + +/// +/// Configuration for the app. +/// +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. + /// + public string RedirectUri { get; set; } = string.Empty; + + /// + /// Uri for the service that is running the chat. + /// + public string ServiceUri { get; set; } = string.Empty; + + /// + /// Gets configuration from appsettings.json. + /// + /// An Config instance + public static Config? GetConfig() + { + var config = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + + return config.GetRequiredSection("Config").Get(); + } + + /// + /// Validates a Config object. + /// + /// + /// True is the config object is not null. + public static bool Validate(Config? config) + { + return config != null; + } +} diff --git a/tools/importdocument/ImportDocument.csproj b/tools/importdocument/ImportDocument.csproj new file mode 100644 index 0000000..243fc41 --- /dev/null +++ b/tools/importdocument/ImportDocument.csproj @@ -0,0 +1,30 @@ + + + Exe + net6.0 + LatestMajor + disable + enable + 10 + false + + + + + Always + + + + + + + + + + + + + <_Parameter1>false + + + diff --git a/tools/importdocument/Program.cs b/tools/importdocument/Program.cs new file mode 100644 index 0000000..8b05704 --- /dev/null +++ b/tools/importdocument/Program.cs @@ -0,0 +1,191 @@ +// 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 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 0000000..0952db9 --- /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 0000000..8409065 --- /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/tools/importdocument/sample-docs/Lorem_ipsum.pdf b/tools/importdocument/sample-docs/Lorem_ipsum.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e25081e0325c8bfd62d09786c49e64c283f49da0 GIT binary patch literal 70011 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

`kx>?2G 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 literal 0 HcmV?d00001 diff --git a/tools/importdocument/sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf b/tools/importdocument/sample-docs/Microsoft-Responsible-AI-Standard-v2-General-Requirements.pdf new file mode 100644 index 0000000000000000000000000000000000000000..31669fb07c3c808bac2720b3600d615555f82192 GIT binary patch literal 712896 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%0A779lPuvQp=vXGA#?OS4?h0)`22FeZzlT&DO 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 z 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

TB=fLuZ<2&#z(2VtBmnAey|DJ? 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%0Aa> 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

D(w|-g2Z@ra-mx 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 +/// 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 0000000..2d58aec --- /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 0000000..333503c --- /dev/null +++ b/webapi/Auth/ChatParticipantAuthorizationHandler.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using CopilotChat.WebApi.Storage; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +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 (Azure.Identity.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 0000000..a286716 --- /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 0000000..6769144 --- /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 new file mode 100644 index 0000000..7124496 --- /dev/null +++ b/webapi/Auth/PassThroughAuthenticationHandler.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Web; + +namespace CopilotChat.WebApi.Auth; + +/// +/// Class implementing "authentication" that lets all requests pass through. +/// +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 + /// + public PassThroughAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory loggerFactory, + UrlEncoder encoder, + ISystemClock clock) : base(options, loggerFactory, encoder, clock) + { + } + + protected override Task HandleAuthenticateAsync() + { + this.Logger.LogInformation("Allowing request to pass through"); + + 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 0000000..a7e48f4 --- /dev/null +++ b/webapi/Controllers/ChatArchiveController.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +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.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +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 0000000..56fb282 --- /dev/null +++ b/webapi/Controllers/ChatController.cs @@ -0,0 +1,484 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +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 CopilotChat.WebApi.Utilities; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +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 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, + IDictionary plugins) + { + this._logger = logger; + this._httpClientFactory = httpClientFactory; + this._telemetryService = telemetryService; + this._disposables = new List(); + this._serviceOptions = serviceOptions.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.RegisterGithubPlugin(kernel, GithubAuthHeader)); + } + + // Jira + if (authHeaders.TryGetValue("JIRA", out string? JiraAuthHeader)) + { + tasks.Add(this.RegisterJiraPlugin(kernel, JiraAuthHeader, variables)); + } + + // Microsoft Graph + if (authHeaders.TryGetValue("GRAPH", out string? GraphAuthHeader)) + { + tasks.Add(this.RegisterMicrosoftGraphPlugins(kernel, GraphAuthHeader)); + } + + if (variables.TryGetValue("customPlugins", out object? customPluginsString)) + { + tasks.AddRange(this.RegisterCustomPlugins(kernel, customPluginsString, authHeaders)); + } + + await Task.WhenAll(tasks); + } + + private async Task RegisterGithubPlugin(Kernel kernel, string GithubAuthHeader) + { + this._logger.LogInformation("Enabling GitHub plugin."); + BearerAuthenticationProvider authenticationProvider = new(() => Task.FromResult(GithubAuthHeader)); + await kernel.ImportPluginFromOpenApiAsync( + pluginName: "GitHubPlugin", + filePath: GetPluginFullPath("GitHubPlugin/openapi.json"), + new OpenApiFunctionExecutionParameters + { + AuthCallback = authenticationProvider.AuthenticateRequestAsync, + }); + } + + private async Task RegisterJiraPlugin(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 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)) + { + // 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); + } + } + + /// + /// 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 async Task RegisterHostedFunctionsAsync(Kernel kernel, HashSet enabledPlugins) + { + foreach (string enabledPlugin in enabledPlugins) + { + if (this._plugins.TryGetValue(enabledPlugin, out Plugin? plugin)) + { + 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; + } + + 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); + } + + /// + /// Applies the token to the provided HTTP request message. + /// + /// The HTTP request message. + public async Task OpenAIAuthenticateRequestAsync(HttpRequestMessage request, string pluginName, OpenAIAuthenticationConfig openAIAuthConfig, CancellationToken cancellationToken = default) + { + await this.AuthenticateRequestAsync(request, cancellationToken); + } +} diff --git a/webapi/Controllers/ChatHistoryController.cs b/webapi/Controllers/ChatHistoryController.cs new file mode 100644 index 0000000..68353c4 --- /dev/null +++ b/webapi/Controllers/ChatHistoryController.cs @@ -0,0 +1,352 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +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.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +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 0000000..cda7de4 --- /dev/null +++ b/webapi/Controllers/ChatMemoryController.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +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.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +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 semantic text memory instance. + /// 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/Controllers/ChatParticipantController.cs b/webapi/Controllers/ChatParticipantController.cs new file mode 100644 index 0000000..cecc4ae --- /dev/null +++ b/webapi/Controllers/ChatParticipantController.cs @@ -0,0 +1,109 @@ +// 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; + +namespace CopilotChat.WebApi.Controllers; + +/// +/// Controller for managing invitations and participants in a chat session. +/// This controller is responsible for: +/// 1. Creating invitation links. +/// 2. Accepting/rejecting invitation links. +/// 3. Managing participants in a chat session. +/// +[ApiController] +public class ChatParticipantController : ControllerBase +{ + private const string UserJoinedClientCall = "UserJoined"; + private readonly ILogger _logger; + private readonly ChatParticipantRepository _chatParticipantRepository; + private readonly ChatSessionRepository _chatSessionRepository; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The chat participant repository. + /// The chat session repository. + public ChatParticipantController( + ILogger logger, + ChatParticipantRepository chatParticipantRepository, + ChatSessionRepository chatSessionRepository) + { + this._logger = logger; + this._chatParticipantRepository = chatParticipantRepository; + this._chatSessionRepository = chatSessionRepository; + } + + /// + /// Join the logged in user to a chat session given a chat ID. + /// + /// Message Hub that performs the real time relay service. + /// The auth info for the current request. + /// The ID of the chat to join. + [HttpPost] + [Route("chats/{chatId:guid}/participants")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task JoinChatAsync( + [FromServices] IHubContext messageRelayHubContext, + [FromServices] IAuthInfo authInfo, + [FromRoute] Guid chatId) + { + string userId = authInfo.UserId; + + // Make sure the chat session exists. + 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.ToString())) + { + return this.Conflict("User is already in the chat session."); + } + + 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.ToString()).SendAsync(UserJoinedClientCall, chatId, userId); + + return this.Ok(chatParticipant); + } + + /// + /// Get a list of chat participants that have the same chat id. + /// + /// The ID of the chat to get all the participants from. + [HttpGet] + [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())) + { + 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 0000000..d2279fb --- /dev/null +++ b/webapi/Controllers/DocumentController.cs @@ -0,0 +1,509 @@ +// 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 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.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +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.TryCreateDocumentUploadMessage(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 TryCreateDocumentUploadMessage( + 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 0000000..62b0f2e --- /dev/null +++ b/webapi/Controllers/MaintenanceController.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; +using CopilotChat.WebApi.Models.Response; +using CopilotChat.WebApi.Options; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +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 0000000..6a795fc --- /dev/null +++ b/webapi/Controllers/PluginController.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +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.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; + +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")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetPluginManifest([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 0000000..0f2ee8f --- /dev/null +++ b/webapi/Controllers/ServiceInfoController.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using CopilotChat.WebApi.Models.Response; +using CopilotChat.WebApi.Options; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +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/Controllers/SpeechTokenController.cs b/webapi/Controllers/SpeechTokenController.cs new file mode 100644 index 0000000..787f8a5 --- /dev/null +++ b/webapi/Controllers/SpeechTokenController.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using CopilotChat.WebApi.Models.Response; +using CopilotChat.WebApi.Options; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace CopilotChat.WebApi.Controllers; + +[ApiController] +public class SpeechTokenController : ControllerBase +{ + private sealed class TokenResult + { + public string? Token { get; set; } + public HttpStatusCode? ResponseCode { get; set; } + } + + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly AzureSpeechOptions _options; + + public SpeechTokenController(IOptions options, + ILogger logger, + IHttpClientFactory httpClientFactory) + { + this._logger = logger; + this._httpClientFactory = httpClientFactory; + this._options = options.Value; + } + + /// + /// Get an authorization token and region + /// + [Route("speechToken")] + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetAsync() + { + // Azure Speech token support is optional. If the configuration is missing or incomplete, return an unsuccessful token response. + if (string.IsNullOrWhiteSpace(this._options.Region) || + string.IsNullOrWhiteSpace(this._options.Key)) + { + return new SpeechTokenResponse { IsSuccess = false }; + } + + string fetchTokenUri = "https://" + this._options.Region + ".api.cognitive.microsoft.com/sts/v1.0/issueToken"; + + TokenResult tokenResult = await this.FetchTokenAsync(fetchTokenUri, this._options.Key); + var isSuccess = tokenResult.ResponseCode != HttpStatusCode.NotFound; + return new SpeechTokenResponse { Token = tokenResult.Token, Region = this._options.Region, IsSuccess = isSuccess }; + } + + private async Task FetchTokenAsync(string fetchUri, string subscriptionKey) + { + using var client = this._httpClientFactory.CreateClient(); + + 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}", fetchUri); + string token = await result.Content.ReadAsStringAsync(); + return new TokenResult { Token = token, ResponseCode = response.StatusCode }; + } + + return new TokenResult { ResponseCode = HttpStatusCode.NotFound }; + } +} diff --git a/webapi/CopilotChatWebApi.csproj b/webapi/CopilotChatWebApi.csproj new file mode 100644 index 0000000..9b17022 --- /dev/null +++ b/webapi/CopilotChatWebApi.csproj @@ -0,0 +1,99 @@ + + + CopilotChat.WebApi + net6.0 + LatestMajor + 10 + enable + disable + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + SKEXP0003,SKEXP0011,SKEXP0021,SKEXP0026,SKEXP0042,SKEXP0050,SKEXP0052,SKEXP0053,SKEXP0060 + + + + true + true + AllEnabledByDefault + latest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + <_Parameter1>false + + + + + + PreserveNewest + + + + + + + + \ No newline at end of file diff --git a/webapi/Extensions/ConfigurationExtensions.cs b/webapi/Extensions/ConfigurationExtensions.cs new file mode 100644 index 0000000..e89c1d7 --- /dev/null +++ b/webapi/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Reflection; +using Azure.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace CopilotChat.WebApi.Extensions; + +internal static class ConfigExtensions +{ + /// + /// Build the configuration for the service. + /// + public static IHostBuilder AddConfiguration(this IHostBuilder host) + { + string? environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + + host.ConfigureAppConfiguration((builderContext, configBuilder) => + { + configBuilder.AddJsonFile( + path: "appsettings.json", + optional: false, + reloadOnChange: true); + + configBuilder.AddJsonFile( + path: $"appsettings.{environment}.json", + optional: true, + reloadOnChange: true); + + configBuilder.AddEnvironmentVariables(); + + configBuilder.AddUserSecrets( + assembly: Assembly.GetExecutingAssembly(), + optional: true, + reloadOnChange: true); + + // For settings from Key Vault, see https://learn.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-8.0 + string? keyVaultUri = builderContext.Configuration["Service:KeyVault"]; + if (!string.IsNullOrWhiteSpace(keyVaultUri)) + { + configBuilder.AddAzureKeyVault( + new Uri(keyVaultUri), + new DefaultAzureCredential()); + + // for more information on how to use DefaultAzureCredential, see https://learn.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet + } + }); + + return host; + } +} diff --git a/webapi/Extensions/ExceptionExtensions.cs b/webapi/Extensions/ExceptionExtensions.cs new file mode 100644 index 0000000..4581dc7 --- /dev/null +++ b/webapi/Extensions/ExceptionExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading; + +#pragma warning disable IDE0130 +// ReSharper disable once CheckNamespace - Using NS of Exception +namespace System; +#pragma warning restore IDE0130 + +/// +/// Exception extension methods. +/// +internal static class ExceptionExtensions +{ + /// + /// Check if an exception is of a type that should not be caught by the kernel. + /// + /// Exception. + /// True if is a critical exception and should not be caught. + internal static bool IsCriticalException(this Exception ex) + => ex is OutOfMemoryException + or ThreadAbortException + or AccessViolationException + or AppDomainUnloadedException + or BadImageFormatException + or CannotUnloadAppDomainException + or InvalidProgramException + or StackOverflowException; +} diff --git a/webapi/Extensions/IAsyncEnumerableExtensions.cs b/webapi/Extensions/IAsyncEnumerableExtensions.cs new file mode 100644 index 0000000..af09b19 --- /dev/null +++ b/webapi/Extensions/IAsyncEnumerableExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace CopilotChat.WebApi.Extensions; + +/// +/// Extension methods for enabling async LINQ operations on IAsyncEnumerable sequence. +/// +public static class IAsyncEnumerableExtensions +{ + /// + /// Creates a List from an IAsyncEnumerable by enumerating it asynchronously. + /// + internal static async Task> ToListAsync(this IAsyncEnumerable source) + { + var result = new List(); + await foreach (var item in source) + { + result.Add(item); + } + + return result; + } +} diff --git a/webapi/Extensions/ISemanticMemoryClientExtensions.cs b/webapi/Extensions/ISemanticMemoryClientExtensions.cs new file mode 100644 index 0000000..b6782fa --- /dev/null +++ b/webapi/Extensions/ISemanticMemoryClientExtensions.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CopilotChat.Shared; +using CopilotChat.WebApi.Models.Storage; +using CopilotChat.WebApi.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.KernelMemory; + +namespace CopilotChat.WebApi.Extensions; + +/// +/// Extension methods for and service registration. +/// +internal static class ISemanticMemoryClientExtensions +{ + private static readonly List pipelineSteps = new() { "extract", "partition", "gen_embeddings", "save_records" }; + + /// + /// Inject . + /// + public static void AddSemanticMemoryServices(this WebApplicationBuilder appBuilder) + { + var serviceProvider = appBuilder.Services.BuildServiceProvider(); + + var memoryConfig = serviceProvider.GetRequiredService>().Value; + + var ocrType = memoryConfig.DataIngestion.ImageOcrType; + var hasOcr = !string.IsNullOrWhiteSpace(ocrType) && !ocrType.Equals(MemoryConfiguration.NoneType, StringComparison.OrdinalIgnoreCase); + + var pipelineType = memoryConfig.DataIngestion.OrchestrationType; + var isDistributed = pipelineType.Equals(MemoryConfiguration.OrchestrationTypeDistributed, StringComparison.OrdinalIgnoreCase); + + appBuilder.Services.AddSingleton(sp => new DocumentTypeProvider(hasOcr)); + + var memoryBuilder = new KernelMemoryBuilder(appBuilder.Services); + + if (isDistributed) + { + memoryBuilder.WithoutDefaultHandlers(); + } + else + { + if (hasOcr) + { + memoryBuilder.WithCustomOcr(appBuilder.Configuration); + } + } + + IKernelMemory memory = memoryBuilder.FromMemoryConfiguration( + memoryConfig, + appBuilder.Configuration + ).Build(); + + appBuilder.Services.AddSingleton(memory); + } + + public static Task SearchMemoryAsync( + this IKernelMemory memoryClient, + string indexName, + string query, + float relevanceThreshold, + string chatId, + string? memoryName = null, + CancellationToken cancellationToken = default) + { + return memoryClient.SearchMemoryAsync(indexName, query, relevanceThreshold, resultCount: -1, chatId, memoryName, cancellationToken); + } + + public static async Task SearchMemoryAsync( + this IKernelMemory memoryClient, + string indexName, + string query, + float relevanceThreshold, + int resultCount, + string chatId, + string? memoryName = null, + CancellationToken cancellationToken = default) + { + var filter = new MemoryFilter(); + + filter.ByTag(MemoryTags.TagChatId, chatId); + + if (!string.IsNullOrWhiteSpace(memoryName)) + { + filter.ByTag(MemoryTags.TagMemory, memoryName); + } + + var searchResult = + await memoryClient.SearchAsync( + query, + indexName, + filter, + null, + relevanceThreshold, // minRelevance param + resultCount, + cancellationToken); + + return searchResult; + } + + public static async Task StoreDocumentAsync( + this IKernelMemory memoryClient, + string indexName, + string documentId, + string chatId, + string memoryName, + string fileName, + Stream fileContent, + CancellationToken cancellationToken = default) + { + var uploadRequest = + new DocumentUploadRequest + { + DocumentId = documentId, + Files = new List { new(fileName, fileContent) }, + Index = indexName, + Steps = pipelineSteps, + }; + + uploadRequest.Tags.Add(MemoryTags.TagChatId, chatId); + uploadRequest.Tags.Add(MemoryTags.TagMemory, memoryName); + + await memoryClient.ImportDocumentAsync(uploadRequest, cancellationToken); + } + + public static Task StoreMemoryAsync( + this IKernelMemory memoryClient, + string indexName, + string chatId, + string memoryName, + string memory, + CancellationToken cancellationToken = default) + { + return memoryClient.StoreMemoryAsync(indexName, chatId, memoryName, memoryId: Guid.NewGuid().ToString(), memory, cancellationToken); + } + + public static async Task StoreMemoryAsync( + this IKernelMemory memoryClient, + string indexName, + string chatId, + string memoryName, + string memoryId, + string memory, + CancellationToken cancellationToken = default) + { + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream); + await writer.WriteAsync(memory); + await writer.FlushAsync(); + stream.Position = 0; + + var uploadRequest = new DocumentUploadRequest + { + DocumentId = memoryId, + Index = indexName, + Files = + new() + { + // Document file name not relevant, but required. + new DocumentUploadRequest.UploadedFile("memory.txt", stream) + }, + Steps = pipelineSteps, + }; + + uploadRequest.Tags.Add(MemoryTags.TagChatId, chatId); + uploadRequest.Tags.Add(MemoryTags.TagMemory, memoryName); + + await memoryClient.ImportDocumentAsync(uploadRequest, cancellationToken); + } + + public static async Task RemoveChatMemoriesAsync( + this IKernelMemory memoryClient, + string indexName, + string chatId, + CancellationToken cancellationToken = default) + { + var memories = await memoryClient.SearchMemoryAsync(indexName, "*", 0.0F, chatId, cancellationToken: cancellationToken); + var documentIds = memories.Results.Select(memory => memory.Link.Split('/').First()).Distinct().ToArray(); + var tasks = documentIds.Select(documentId => memoryClient.DeleteDocumentAsync(documentId, indexName, cancellationToken)).ToArray(); + + Task.WaitAll(tasks, cancellationToken); + } +} diff --git a/webapi/Extensions/SemanticKernelExtensions.cs b/webapi/Extensions/SemanticKernelExtensions.cs new file mode 100644 index 0000000..0bafb44 --- /dev/null +++ b/webapi/Extensions/SemanticKernelExtensions.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Threading.Tasks; +using CopilotChat.WebApi.Hubs; +using CopilotChat.WebApi.Models.Response; +using CopilotChat.WebApi.Options; +using CopilotChat.WebApi.Plugins.Chat; +using CopilotChat.WebApi.Services; +using CopilotChat.WebApi.Storage; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.KernelMemory; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Plugins.Core; + +namespace CopilotChat.WebApi.Extensions; + +/// +/// Extension methods for registering Semantic Kernel related services. +/// +internal static class SemanticKernelExtensions +{ + /// + /// Delegate to register functions with a Semantic Kernel + /// + public delegate Task RegisterFunctionsWithKernel(IServiceProvider sp, Kernel kernel); + + /// + /// Delegate for any complimentary setup of the kernel, i.e., registering custom plugins, etc. + /// See webapi/README.md#Add-Custom-Setup-to-Chat-Copilot's-Kernel for more details. + /// + public delegate Task KernelSetupHook(IServiceProvider sp, Kernel kernel); + + /// + /// Add Semantic Kernel services + /// + public static WebApplicationBuilder AddSemanticKernelServices(this WebApplicationBuilder builder) + { + builder.InitializeKernelProvider(); + + // Semantic Kernel + builder.Services.AddScoped( + sp => + { + var provider = sp.GetRequiredService(); + var kernel = provider.GetCompletionKernel(); + + sp.GetRequiredService()(sp, kernel); + + // If KernelSetupHook is not null, invoke custom kernel setup. + sp.GetService()?.Invoke(sp, kernel); + return kernel; + }); + + // Azure Content Safety + builder.Services.AddContentSafety(); + + // Register plugins + builder.Services.AddScoped(sp => RegisterChatCopilotFunctionsAsync); + + // Add any additional setup needed for the kernel. + // Uncomment the following line and pass in a custom hook for any complimentary setup of the kernel. + // builder.Services.AddKernelSetupHook(customHook); + + return builder; + } + + /// + /// Add embedding model + /// + public static WebApplicationBuilder AddBotConfig(this WebApplicationBuilder builder) + { + builder.Services.AddScoped(sp => sp.WithBotConfig(builder.Configuration)); + + return builder; + } + + /// + /// Register custom hook for any complimentary setup of the kernel. + /// + /// The delegate to perform any additional setup of the kernel. + public static IServiceCollection AddKernelSetupHook(this IServiceCollection services, KernelSetupHook hook) + { + // Add the hook to the service collection + services.AddScoped(sp => hook); + return services; + } + + /// + /// Register the chat plugin with the kernel. + /// + public static Kernel RegisterChatPlugin(this Kernel kernel, IServiceProvider sp) + { + // Chat plugin + kernel.ImportPluginFromObject( + new ChatPlugin( + kernel, + memoryClient: sp.GetRequiredService(), + chatMessageRepository: sp.GetRequiredService(), + chatSessionRepository: sp.GetRequiredService(), + messageRelayHubContext: sp.GetRequiredService>(), + promptOptions: sp.GetRequiredService>(), + documentImportOptions: sp.GetRequiredService>(), + contentSafety: sp.GetService(), + logger: sp.GetRequiredService>()), + nameof(ChatPlugin)); + + return kernel; + } + + private static void InitializeKernelProvider(this WebApplicationBuilder builder) + { + builder.Services.AddSingleton(sp => new SemanticKernelProvider(sp, builder.Configuration, sp.GetRequiredService())); + } + + /// + /// Register functions with the main kernel responsible for handling Chat Copilot requests. + /// + private static Task RegisterChatCopilotFunctionsAsync(IServiceProvider sp, Kernel kernel) + { + // Chat Copilot functions + kernel.RegisterChatPlugin(sp); + + // Time plugin + kernel.ImportPluginFromObject(new TimePlugin(), nameof(TimePlugin)); + + return Task.CompletedTask; + } + + /// + /// Register plugins with a given kernel. + /// + private static Task RegisterPluginsAsync(IServiceProvider sp, Kernel kernel) + { + var logger = kernel.LoggerFactory.CreateLogger(nameof(Kernel)); + + // Semantic plugins + ServiceOptions options = sp.GetRequiredService>().Value; + if (!string.IsNullOrWhiteSpace(options.SemanticPluginsDirectory)) + { + foreach (string subDir in Directory.GetDirectories(options.SemanticPluginsDirectory)) + { + try + { + kernel.ImportPluginFromPromptDirectory(options.SemanticPluginsDirectory, Path.GetFileName(subDir)!); + } + catch (KernelException ex) + { + logger.LogError("Could not load plugin from {Directory}: {Message}", subDir, ex.Message); + } + } + } + + // Native plugins + if (!string.IsNullOrWhiteSpace(options.NativePluginsDirectory)) + { + // Loop through all the files in the directory that have the .cs extension + var pluginFiles = Directory.GetFiles(options.NativePluginsDirectory, "*.cs"); + foreach (var file in pluginFiles) + { + // Parse the name of the class from the file name (assuming it matches) + var className = Path.GetFileNameWithoutExtension(file); + + // Get the type of the class from the current assembly + var assembly = Assembly.GetExecutingAssembly(); + var classType = assembly.GetTypes().FirstOrDefault(t => t.Name.Contains(className, StringComparison.CurrentCultureIgnoreCase)); + + // If the type is found, create an instance of the class using the default constructor + if (classType != null) + { + try + { + var plugin = Activator.CreateInstance(classType); + kernel.ImportPluginFromObject(plugin!, classType.Name!); + } + catch (KernelException ex) + { + logger.LogError("Could not load plugin from file {File}: {Details}", file, ex.Message); + } + } + else + { + logger.LogError("Class type not found. Make sure the class type matches exactly with the file name {FileName}", className); + } + } + } + + return Task.CompletedTask; + } + + /// + /// Adds Azure Content Safety + /// + internal static void AddContentSafety(this IServiceCollection services) + { + IConfiguration configuration = services.BuildServiceProvider().GetRequiredService(); + var options = configuration.GetSection(ContentSafetyOptions.PropertyName).Get() ?? new ContentSafetyOptions { Enabled = false }; + services.AddSingleton(sp => new AzureContentSafety(options.Endpoint, options.Key)); + } + + /// + /// Get the embedding model from the configuration. + /// + private static ChatArchiveEmbeddingConfig WithBotConfig(this IServiceProvider provider, IConfiguration configuration) + { + var memoryOptions = provider.GetRequiredService>().Value; + + switch (memoryOptions.Retrieval.EmbeddingGeneratorType) + { + case string x when x.Equals("AzureOpenAI", StringComparison.OrdinalIgnoreCase): + case string y when y.Equals("AzureOpenAIEmbedding", StringComparison.OrdinalIgnoreCase): + var azureAIOptions = memoryOptions.GetServiceConfig(configuration, "AzureOpenAIEmbedding"); + return + new ChatArchiveEmbeddingConfig + { + AIService = ChatArchiveEmbeddingConfig.AIServiceType.AzureOpenAIEmbedding, + DeploymentOrModelId = azureAIOptions.Deployment, + }; + + case string x when x.Equals("OpenAI", StringComparison.OrdinalIgnoreCase): + var openAIOptions = memoryOptions.GetServiceConfig(configuration, "OpenAI"); + return + new ChatArchiveEmbeddingConfig + { + AIService = ChatArchiveEmbeddingConfig.AIServiceType.OpenAI, + DeploymentOrModelId = openAIOptions.EmbeddingModel, + }; + + default: + throw new ArgumentException($"Invalid {nameof(memoryOptions.Retrieval.EmbeddingGeneratorType)} value in 'SemanticMemory' settings."); + } + } +} diff --git a/webapi/Extensions/ServiceExtensions.cs b/webapi/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000..182a770 --- /dev/null +++ b/webapi/Extensions/ServiceExtensions.cs @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Reflection; +using CopilotChat.Shared; +using CopilotChat.WebApi.Auth; +using CopilotChat.WebApi.Models.Storage; +using CopilotChat.WebApi.Options; +using CopilotChat.WebApi.Services; +using CopilotChat.WebApi.Storage; +using CopilotChat.WebApi.Utilities; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Web; +using Microsoft.KernelMemory; +using Microsoft.KernelMemory.Diagnostics; + +namespace CopilotChat.WebApi.Extensions; + +/// +/// Extension methods for . +/// Add options and services for Chat Copilot. +/// +public static class CopilotChatServiceExtensions +{ + /// + /// Parse configuration into options. + /// + public static IServiceCollection AddOptions(this IServiceCollection services, ConfigurationManager configuration) + { + // General configuration + AddOptions(ServiceOptions.PropertyName); + + // Authentication configuration + AddOptions(ChatAuthenticationOptions.PropertyName); + + // Chat storage configuration + AddOptions(ChatStoreOptions.PropertyName); + + // Azure speech token configuration + AddOptions(AzureSpeechOptions.PropertyName); + + AddOptions(DocumentMemoryOptions.PropertyName); + + // Chat prompt options + AddOptions(PromptsOptions.PropertyName); + + AddOptions(ContentSafetyOptions.PropertyName); + + AddOptions(MemoryConfiguration.KernelMemorySection); + + AddOptions(FrontendOptions.PropertyName); + + return services; + + void AddOptions(string propertyName) + where TOptions : class + { + services.AddOptions(configuration.GetSection(propertyName)); + } + } + + internal static void AddOptions(this IServiceCollection services, IConfigurationSection section) + where TOptions : class + { + services.AddOptions() + .Bind(section) + .ValidateDataAnnotations() + .ValidateOnStart() + .PostConfigure(TrimStringProperties); + } + + internal static IServiceCollection AddPlugins(this IServiceCollection services, IConfiguration configuration) + { + var plugins = configuration.GetSection("Plugins").Get>() ?? new List(); + var logger = services.BuildServiceProvider().GetRequiredService>(); + logger.LogDebug("Found {0} plugins.", plugins.Count); + + // Validate the plugins + Dictionary validatedPlugins = new(); + foreach (Plugin plugin in plugins) + { + if (validatedPlugins.ContainsKey(plugin.Name)) + { + logger.LogWarning("Plugin '{0}' is defined more than once. Skipping...", plugin.Name); + continue; + } + + var pluginManifestUrl = PluginUtils.GetPluginManifestUri(plugin.ManifestDomain); + using var request = new HttpRequestMessage(HttpMethod.Get, pluginManifestUrl); + // Need to set the user agent to avoid 403s from some sites. + request.Headers.Add("User-Agent", Telemetry.HttpUserAgent); + try + { + logger.LogInformation("Adding plugin: {0}.", plugin.Name); + using var httpClient = new HttpClient(); + var response = httpClient.SendAsync(request).Result; + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException($"Plugin '{plugin.Name}' at '{pluginManifestUrl}' returned status code '{response.StatusCode}'."); + } + validatedPlugins.Add(plugin.Name, plugin); + logger.LogInformation("Added plugin: {0}.", plugin.Name); + } + catch (Exception ex) when (ex is InvalidOperationException || ex is AggregateException) + { + logger.LogWarning(ex, "Plugin '{0}' at {1} responded with error. Skipping...", plugin.Name, pluginManifestUrl); + } + catch (Exception ex) when (ex is UriFormatException) + { + logger.LogWarning("Plugin '{0}' at {1} is not a valid URL. Skipping...", plugin.Name, pluginManifestUrl); + } + } + + // Add the plugins + services.AddSingleton>(validatedPlugins); + + return services; + } + + internal static IServiceCollection AddMaintenanceServices(this IServiceCollection services) + { + // Inject action stub + services.AddSingleton>( + sp => (IReadOnlyList)Array.Empty()); + + return services; + } + + /// + /// Add CORS settings. + /// + internal static IServiceCollection AddCorsPolicy(this IServiceCollection services, IConfiguration configuration) + { + string[] allowedOrigins = configuration.GetSection("AllowedOrigins").Get() ?? Array.Empty(); + if (allowedOrigins.Length > 0) + { + services.AddCors(options => + { + options.AddDefaultPolicy( + policy => + { + policy.WithOrigins(allowedOrigins) + .WithMethods("POST", "GET", "PUT", "DELETE", "PATCH") + .AllowAnyHeader(); + }); + }); + } + + return services; + } + + /// + /// Add persistent chat store services. + /// + public static IServiceCollection AddPersistentChatStore(this IServiceCollection services) + { + IStorageContext chatSessionStorageContext; + ICopilotChatMessageStorageContext chatMessageStorageContext; + IStorageContext chatMemorySourceStorageContext; + IStorageContext chatParticipantStorageContext; + + ChatStoreOptions chatStoreConfig = services.BuildServiceProvider().GetRequiredService>().Value; + + switch (chatStoreConfig.Type) + { + case ChatStoreOptions.ChatStoreType.Volatile: + { + chatSessionStorageContext = new VolatileContext(); + chatMessageStorageContext = new VolatileCopilotChatMessageContext(); + chatMemorySourceStorageContext = new VolatileContext(); + chatParticipantStorageContext = new VolatileContext(); + break; + } + + case ChatStoreOptions.ChatStoreType.Filesystem: + { + if (chatStoreConfig.Filesystem == null) + { + throw new InvalidOperationException("ChatStore:Filesystem is required when ChatStore:Type is 'Filesystem'"); + } + + string fullPath = Path.GetFullPath(chatStoreConfig.Filesystem.FilePath); + string directory = Path.GetDirectoryName(fullPath) ?? string.Empty; + chatSessionStorageContext = new FileSystemContext( + new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_sessions{Path.GetExtension(fullPath)}"))); + chatMessageStorageContext = new FileSystemCopilotChatMessageContext( + new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_messages{Path.GetExtension(fullPath)}"))); + chatMemorySourceStorageContext = new FileSystemContext( + new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_memorysources{Path.GetExtension(fullPath)}"))); + chatParticipantStorageContext = new FileSystemContext( + new FileInfo(Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(fullPath)}_participants{Path.GetExtension(fullPath)}"))); + break; + } + + case ChatStoreOptions.ChatStoreType.Cosmos: + { + if (chatStoreConfig.Cosmos == null) + { + throw new InvalidOperationException("ChatStore:Cosmos is required when ChatStore:Type is 'Cosmos'"); + } +#pragma warning disable CA2000 // Dispose objects before losing scope - objects are singletons for the duration of the process and disposed when the process exits. + chatSessionStorageContext = new CosmosDbContext( + chatStoreConfig.Cosmos.ConnectionString, chatStoreConfig.Cosmos.Database, chatStoreConfig.Cosmos.ChatSessionsContainer); + chatMessageStorageContext = new CosmosDbCopilotChatMessageContext( + chatStoreConfig.Cosmos.ConnectionString, chatStoreConfig.Cosmos.Database, chatStoreConfig.Cosmos.ChatMessagesContainer); + chatMemorySourceStorageContext = new CosmosDbContext( + chatStoreConfig.Cosmos.ConnectionString, chatStoreConfig.Cosmos.Database, chatStoreConfig.Cosmos.ChatMemorySourcesContainer); + chatParticipantStorageContext = new CosmosDbContext( + chatStoreConfig.Cosmos.ConnectionString, chatStoreConfig.Cosmos.Database, chatStoreConfig.Cosmos.ChatParticipantsContainer); +#pragma warning restore CA2000 // Dispose objects before losing scope + break; + } + + default: + { + throw new InvalidOperationException( + "Invalid 'ChatStore' setting 'chatStoreConfig.Type'."); + } + } + + services.AddSingleton(new ChatSessionRepository(chatSessionStorageContext)); + services.AddSingleton(new ChatMessageRepository(chatMessageStorageContext)); + services.AddSingleton(new ChatMemorySourceRepository(chatMemorySourceStorageContext)); + services.AddSingleton(new ChatParticipantRepository(chatParticipantStorageContext)); + + return services; + } + + /// + /// Add authorization services + /// + public static IServiceCollection AddChatCopilotAuthorization(this IServiceCollection services) + { + return services.AddScoped() + .AddAuthorizationCore(options => + { + options.DefaultPolicy = new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); + options.AddPolicy(AuthPolicyName.RequireChatParticipant, builder => + { + builder.RequireAuthenticatedUser() + .AddRequirements(new ChatParticipantRequirement()); + }); + }); + } + + /// + /// Add authentication services + /// + public static IServiceCollection AddChatCopilotAuthentication(this IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + var config = services.BuildServiceProvider().GetRequiredService>().Value; + switch (config.Type) + { + case ChatAuthenticationOptions.AuthenticationType.AzureAd: + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(configuration.GetSection($"{ChatAuthenticationOptions.PropertyName}:AzureAd")); + break; + + case ChatAuthenticationOptions.AuthenticationType.None: + services.AddAuthentication(PassThroughAuthenticationHandler.AuthenticationScheme) + .AddScheme( + authenticationScheme: PassThroughAuthenticationHandler.AuthenticationScheme, + configureOptions: null); + break; + + default: + throw new InvalidOperationException($"Invalid authentication type '{config.Type}'."); + } + + return services; + } + + /// + /// Trim all string properties, recursively. + /// + private static void TrimStringProperties(T options) where T : class + { + Queue targets = new(); + targets.Enqueue(options); + + while (targets.Count > 0) + { + object target = targets.Dequeue(); + Type targetType = target.GetType(); + foreach (PropertyInfo property in targetType.GetProperties()) + { + // Skip enumerations + if (property.PropertyType.IsEnum) + { + continue; + } + + // Skip index properties + if (property.GetIndexParameters().Length == 0) + { + continue; + } + + // Property is a built-in type, readable, and writable. + if (property.PropertyType.Namespace == "System" && + property.CanRead && + property.CanWrite) + { + // Property is a non-null string. + if (property.PropertyType == typeof(string) && + property.GetValue(target) != null) + { + property.SetValue(target, property.GetValue(target)!.ToString()!.Trim()); + } + } + else + { + // Property is a non-built-in and non-enum type - queue it for processing. + if (property.GetValue(target) != null) + { + targets.Enqueue(property.GetValue(target)!); + } + } + } + } + } +} diff --git a/webapi/Hubs/MessageRelayHub.cs b/webapi/Hubs/MessageRelayHub.cs new file mode 100644 index 0000000..f01233d --- /dev/null +++ b/webapi/Hubs/MessageRelayHub.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; + +namespace CopilotChat.WebApi.Hubs; + +/// +/// Represents a chat hub for real-time communication. +/// +public class MessageRelayHub : Hub +{ + private const string ReceiveMessageClientCall = "ReceiveMessage"; + private const string ReceiveUserTypingStateClientCall = "ReceiveUserTypingState"; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public MessageRelayHub(ILogger logger) + { + this._logger = logger; + } + + /// + /// Adds the user to the groups that they are a member of. + /// Groups are identified by the chat ID. + /// TODO: [Issue #50] Retrieve the user ID from the claims and call this method + /// from the OnConnectedAsync method instead of the frontend. + /// + /// The chat ID used as group id for SignalR. + public async Task AddClientToGroupAsync(string chatId) + { + await this.Groups.AddToGroupAsync(this.Context.ConnectionId, chatId); + } + + /// + /// Sends a message to all users except the sender. + /// + /// The chat ID used as group id for SignalR. + /// The user ID of the user that sent the message. + /// The message to send. + public async Task SendMessageAsync(string chatId, string senderId, object message) + { + await this.Clients.OthersInGroup(chatId).SendAsync(ReceiveMessageClientCall, chatId, senderId, message); + } + + /// + /// Sends the typing state to all users except the sender. + /// + /// The chat ID used as group id for SignalR. + /// The user ID of the user who is typing. + /// Whether the user is typing. + /// A task that represents the asynchronous operation. + public async Task SendUserTypingStateAsync(string chatId, string userId, bool isTyping) + { + await this.Clients.OthersInGroup(chatId).SendAsync(ReceiveUserTypingStateClientCall, chatId, userId, isTyping); + } +} diff --git a/webapi/Models/Request/Ask.cs b/webapi/Models/Request/Ask.cs new file mode 100644 index 0000000..3f7521b --- /dev/null +++ b/webapi/Models/Request/Ask.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using CopilotChat.WebApi.Options; + +namespace CopilotChat.WebApi.Models.Request; + +public class Ask +{ + [Required, NotEmptyOrWhitespace] + public string Input { get; set; } = string.Empty; + + public IEnumerable> Variables { get; set; } = Enumerable.Empty>(); +} diff --git a/webapi/Models/Request/CreateChatParameters.cs b/webapi/Models/Request/CreateChatParameters.cs new file mode 100644 index 0000000..0d8a324 --- /dev/null +++ b/webapi/Models/Request/CreateChatParameters.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json.Serialization; + +namespace CopilotChat.WebApi.Models.Request; + +/// +/// Parameters for creating a new chat session. +/// +public class CreateChatParameters +{ + /// + /// Title of the chat. + /// + [JsonPropertyName("title")] + public string? Title { get; set; } +} diff --git a/webapi/Models/Request/CustomPlugin.cs b/webapi/Models/Request/CustomPlugin.cs new file mode 100644 index 0000000..b0872e4 --- /dev/null +++ b/webapi/Models/Request/CustomPlugin.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json.Serialization; + +namespace CopilotChat.WebApi.Models.Request; + +/// +/// Custom plugin imported from ChatGPT Manifest file. +/// Docs: https://platform.openai.com/docs/plugins/introduction. +/// +public class CustomPlugin +{ + /// + /// Human-readable name, such as the full company name. + /// + [JsonPropertyName("nameForHuman")] + public string NameForHuman { get; set; } = string.Empty; + + /// + /// Name the model will use to target the plugin. + /// + [JsonPropertyName("nameForModel")] + public string NameForModel { get; set; } = string.Empty; + + /// + /// Expected request header tag containing auth information. + /// + [JsonPropertyName("authHeaderTag")] + public string AuthHeaderTag { get; set; } = string.Empty; + + /// + /// Auth type. Currently limited to either 'none' + /// or user PAT (https://platform.openai.com/docs/plugins/authentication/user-level) + /// + [JsonPropertyName("authType")] + public string AuthType { get; set; } = string.Empty; + + /// + /// Website domain hosting the plugin files. + /// + [JsonPropertyName("manifestDomain")] + public string ManifestDomain { get; set; } = string.Empty; +} diff --git a/webapi/Models/Request/DocumentData.cs b/webapi/Models/Request/DocumentData.cs new file mode 100644 index 0000000..5eedba8 --- /dev/null +++ b/webapi/Models/Request/DocumentData.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace CopilotChat.WebApi.Models.Request; + +public sealed class DocumentData +{ + /// + /// Name of the uploaded document. + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// Size of the uploaded document in bytes. + /// + [JsonPropertyName("size")] + public string Size { get; set; } = string.Empty; + + /// + /// Status of the uploaded document. + /// If true, the document is successfully uploaded. False otherwise. + /// + [JsonPropertyName("isUploaded")] + public bool IsUploaded { get; set; } = false; +} diff --git a/webapi/Models/Request/DocumentImportForm.cs b/webapi/Models/Request/DocumentImportForm.cs new file mode 100644 index 0000000..65623b3 --- /dev/null +++ b/webapi/Models/Request/DocumentImportForm.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; + +namespace CopilotChat.WebApi.Models.Request; + +/// +/// Form for importing a document from a POST Http request. +/// +public class DocumentImportForm +{ + /// + /// The file to import. + /// + public IEnumerable FormFiles { get; set; } = Enumerable.Empty(); + + /// + /// Flag indicating whether user has content safety enabled from the client. + /// + public bool UseContentSafety { get; set; } = false; +} diff --git a/webapi/Models/Request/DocumentScopes.cs b/webapi/Models/Request/DocumentScopes.cs new file mode 100644 index 0000000..9e4c9a2 --- /dev/null +++ b/webapi/Models/Request/DocumentScopes.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Models.Request; + +/// +/// Scope of the document. This determines the collection name in the document memory. +/// +public enum DocumentScopes +{ + Global, + Chat, +} diff --git a/webapi/Models/Request/DocumentStatusForm.cs b/webapi/Models/Request/DocumentStatusForm.cs new file mode 100644 index 0000000..8415a79 --- /dev/null +++ b/webapi/Models/Request/DocumentStatusForm.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CopilotChat.WebApi.Models.Request; + +/// +/// Form for importing a document from a POST Http request. +/// +public class DocumentStatusForm +{ + /// + /// The file to import. + /// + public IEnumerable FileReferences { get; set; } = Enumerable.Empty(); + + /// + /// Scope of the document. This determines the collection name in the document memory. + /// + public DocumentScopes DocumentScope { get; set; } = DocumentScopes.Chat; + + /// + /// The ID of the chat that owns the document. + /// This is used to create a unique collection name for the chat. + /// If the chat ID is not specified or empty, the documents will be stored in a global collection. + /// If the document scope is set to global, this value is ignored. + /// + public Guid ChatId { get; set; } = Guid.Empty; + + /// + /// The ID of the user who is importing the document to a chat session. + /// Will be use to validate if the user has access to the chat session. + /// + public string UserId { get; set; } = string.Empty; + + /// + /// Name of the user who sent this message. + /// Will be used to create the chat message representing the document upload. + /// + public string UserName { get; set; } = string.Empty; +} diff --git a/webapi/Models/Request/EditChatParameters.cs b/webapi/Models/Request/EditChatParameters.cs new file mode 100644 index 0000000..97985c9 --- /dev/null +++ b/webapi/Models/Request/EditChatParameters.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Models.Request; + +/// +/// Parameters for editing chat session. +/// +public class EditChatParameters +{ + /// + /// Title of the chat. + /// + public string? Title { get; set; } + + /// + /// System description of the chat that is used to generate responses. + /// + public string? SystemDescription { get; set; } + + /// + /// The balance between long term memory and working term memory. + /// The higher this value, the more the system will rely on long term memory by lowering + /// the relevance threshold of long term memory and increasing the threshold score of working memory. + /// + public float? MemoryBalance { get; set; } +} diff --git a/webapi/Models/Request/SemanticMemoryType.cs b/webapi/Models/Request/SemanticMemoryType.cs new file mode 100644 index 0000000..a9d5b7a --- /dev/null +++ b/webapi/Models/Request/SemanticMemoryType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Models.Request; + +/// +/// Types of semantic memories supported by chat-copilot. +/// +public enum SemanticMemoryType +{ + LongTermMemory, + WorkingMemory +} diff --git a/webapi/Models/Response/AskResult.cs b/webapi/Models/Response/AskResult.cs new file mode 100644 index 0000000..e7a1a9d --- /dev/null +++ b/webapi/Models/Response/AskResult.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; + +namespace CopilotChat.WebApi.Models.Response; + +public class AskResult +{ + public string Value { get; set; } = string.Empty; + + public IEnumerable>? Variables { get; set; } = Enumerable.Empty>(); +} diff --git a/webapi/Models/Response/BotResponsePrompt.cs b/webapi/Models/Response/BotResponsePrompt.cs new file mode 100644 index 0000000..4f7cb46 --- /dev/null +++ b/webapi/Models/Response/BotResponsePrompt.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// The final prompt sent to generate bot response. +/// +public class BotResponsePrompt +{ + /// + /// The system persona of the chat, includes SystemDescription and SystemResponse components from PromptsOptions.cs. + /// + [JsonPropertyName("systemPersona")] + public string SystemPersona { get; set; } = string.Empty; + + /// + /// Audience extracted from conversation history. + /// + [JsonPropertyName("audience")] + public string Audience { get; set; } = string.Empty; + + /// + /// User intent extracted from input and conversation history. + /// + [JsonPropertyName("userIntent")] + public string UserIntent { get; set; } = string.Empty; + + /// + /// Chat memories queried from the chat memory store if any, includes long term and working memory. + /// + [JsonPropertyName("chatMemories")] + public string PastMemories { get; set; } = string.Empty; + + /// + /// Most recent messages from chat history. + /// + [JsonPropertyName("chatHistory")] + public string ChatHistory { get; set; } = string.Empty; + + /// + /// The collection of context messages associated with this chat completions request. + /// See https://learn.microsoft.com/en-us/dotnet/api/azure.ai.openai.chatcompletionsoptions.messages?view=azure-dotnet-preview#azure-ai-openai-chatcompletionsoptions-messages. + /// + [JsonPropertyName("metaPromptTemplate")] + public ChatHistory MetaPromptTemplate { get; set; } = new(); + + public BotResponsePrompt( + string systemInstructions, + string audience, + string userIntent, + string chatMemories, + string chatHistory, + ChatHistory metaPromptTemplate + ) + { + this.SystemPersona = systemInstructions; + this.Audience = audience; + this.UserIntent = userIntent; + this.PastMemories = chatMemories; + this.ChatHistory = chatHistory; + this.MetaPromptTemplate = metaPromptTemplate; + } +} diff --git a/webapi/Models/Response/ChatArchive.cs b/webapi/Models/Response/ChatArchive.cs new file mode 100644 index 0000000..f371933 --- /dev/null +++ b/webapi/Models/Response/ChatArchive.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using CopilotChat.WebApi.Models.Storage; +using CopilotChat.WebApi.Options; +using Microsoft.KernelMemory; + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// The data model of a chat archive. +/// +public class ChatArchive +{ + /// + /// Schema information for the chat archive. + /// + public ChatArchiveSchemaInfo Schema { get; set; } = new ChatArchiveSchemaInfo(); + + /// + /// The embedding configurations. + /// + public ChatArchiveEmbeddingConfig EmbeddingConfigurations { get; set; } = new ChatArchiveEmbeddingConfig(); + + /// + /// Chat title. + /// + public string ChatTitle { get; set; } = string.Empty; + + /// + /// The system description of the chat that is used to generate responses. + /// + public string SystemDescription { get; set; } = string.Empty; + + /// + /// The chat history. It contains all the messages in the conversation with the bot. + /// + public List ChatHistory { get; set; } = new List(); + + /// + /// Chat archive's embeddings. + /// + public Dictionary> Embeddings { get; set; } = new Dictionary>(); + + /// + /// The embeddings of uploaded documents in Chat Copilot. It represents the document memory which is accessible to all chat sessions of a given user. + /// + public Dictionary> DocumentEmbeddings { get; set; } = new Dictionary>(); +} diff --git a/webapi/Models/Response/ChatArchiveEmbeddingConfig.cs b/webapi/Models/Response/ChatArchiveEmbeddingConfig.cs new file mode 100644 index 0000000..e2aa012 --- /dev/null +++ b/webapi/Models/Response/ChatArchiveEmbeddingConfig.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// Chat archive embedding configuration. +/// +public class ChatArchiveEmbeddingConfig +{ + /// + /// Supported types of AI services. + /// + public enum AIServiceType + { + /// + /// Azure OpenAI https://learn.microsoft.com/en-us/azure/cognitive-services/openai/ + /// + AzureOpenAIEmbedding, + + /// + /// OpenAI https://openai.com/ + /// + OpenAI + } + + /// + /// The AI service. + /// + [Required] + [JsonConverter(typeof(JsonStringEnumConverter))] + public AIServiceType AIService { get; set; } = AIServiceType.AzureOpenAIEmbedding; + + /// + /// The deployment or the model id. + /// + public string DeploymentOrModelId { get; set; } = string.Empty; +} diff --git a/webapi/Models/Response/CreateChatResponse.cs b/webapi/Models/Response/CreateChatResponse.cs new file mode 100644 index 0000000..1210ffa --- /dev/null +++ b/webapi/Models/Response/CreateChatResponse.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; +using CopilotChat.WebApi.Models.Storage; + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// Response object definition to the 'chats' POST request. +/// This groups the initial bot message with the chat session +/// to avoid making two requests. +/// +public class CreateChatResponse +{ + /// + /// The chat session that was created. + /// + [JsonPropertyName("chatSession")] + public ChatSession ChatSession { get; set; } + + /// + /// Initial bot message. + /// + [JsonPropertyName("initialBotMessage")] + public CopilotChatMessage InitialBotMessage { get; set; } + + public CreateChatResponse(ChatSession chatSession, CopilotChatMessage initialBotMessage) + { + this.ChatSession = chatSession; + this.InitialBotMessage = initialBotMessage; + } +} diff --git a/webapi/Models/Response/DocumentMessageContent.cs b/webapi/Models/Response/DocumentMessageContent.cs new file mode 100644 index 0000000..1379551 --- /dev/null +++ b/webapi/Models/Response/DocumentMessageContent.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using CopilotChat.WebApi.Models.Request; + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// Value of `Content` for a `ChatMessage` of type `ChatMessageType.Document`. +/// +public class DocumentMessageContent +{ + /// + /// List of documents contained in the message. + /// + [JsonPropertyName("documents")] + public IEnumerable Documents { get; set; } = Enumerable.Empty(); + + /// + /// Add a document to the list of documents. + /// + /// Name of the uploaded document + /// Size of the uploaded document in bytes + /// Status of the uploaded document + public void AddDocument(string name, string size, bool isUploaded) + { + this.Documents = this.Documents.Append(new DocumentData + { + Name = name, + Size = size, + IsUploaded = isUploaded, + }); + } + + /// + /// Serialize the object to a JSON string. + /// + /// A serialized JSON string + public override string ToString() + { + return JsonSerializer.Serialize(this); + } + + /// + /// Serialize the object to a formatted string. + /// Only successful uploads will be included in the formatted string. + /// + /// A formatted string + public string ToFormattedString() + { + if (!this.Documents.Any()) + { + return string.Empty; + } + + var formattedStrings = this.Documents + .Where(document => document.IsUploaded) + .Select(document => document.Name).ToList(); + + if (formattedStrings.Count == 1) + { + return formattedStrings.First(); + } + + return string.Join(", ", formattedStrings); + } + + /// + /// Serialize the object to a formatted string that only + /// contains document names separated by comma. + /// + /// A formatted string + public string ToFormattedStringNamesOnly() + { + if (!this.Documents.Any()) + { + return string.Empty; + } + + var formattedStrings = this.Documents + .Where(document => document.IsUploaded) + .Select(document => document.Name).ToList(); + + if (formattedStrings.Count == 1) + { + return formattedStrings.First(); + } + + return string.Join(", ", formattedStrings); + } + + /// + /// Deserialize a JSON string to a DocumentMessageContent object. + /// + /// A JSON string + /// A DocumentMessageContent object + public static DocumentMessageContent? FromString(string json) + { + return JsonSerializer.Deserialize(json); + } +} diff --git a/webapi/Models/Response/FrontendAuthConfig.cs b/webapi/Models/Response/FrontendAuthConfig.cs new file mode 100644 index 0000000..2f3758b --- /dev/null +++ b/webapi/Models/Response/FrontendAuthConfig.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; +using CopilotChat.WebApi.Options; + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// Configuration to be used by the frontend client to this service. +/// +public class FrontendAuthConfig +{ + /// + /// Type of auth to use. + /// + [JsonPropertyName("authType")] + public string AuthType { get; set; } = ChatAuthenticationOptions.AuthenticationType.None.ToString(); + + /// + /// Azure Active Directory authority to use. + /// + [JsonPropertyName("aadAuthority")] + public string AadAuthority { get; set; } = string.Empty; + + /// + /// Azure Active Directory client ID the frontend is to use. + /// + [JsonPropertyName("aadClientId")] + public string AadClientId { get; set; } = string.Empty; + + /// + /// Azure Active Directory scope the frontend should request. + /// + [JsonPropertyName("aadApiScope")] + public string AadApiScope { get; set; } = string.Empty; +} diff --git a/webapi/Models/Response/ImageAnalysisResponse.cs b/webapi/Models/Response/ImageAnalysisResponse.cs new file mode 100644 index 0000000..e75bd10 --- /dev/null +++ b/webapi/Models/Response/ImageAnalysisResponse.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; +using CopilotChat.WebApi.Services; + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// Response definition to image content safety analysis requests. +/// endpoint made by the AzureContentSafety. +/// +public class ImageAnalysisResponse +{ + /// + /// Gets or sets the AnalysisResult related to hate. + /// + [JsonPropertyName("hateResult")] + public AnalysisResult? HateResult { get; set; } + + /// + /// Gets or sets the AnalysisResult related to self-harm. + /// + [JsonPropertyName("selfHarmResult")] + public AnalysisResult? SelfHarmResult { get; set; } + + /// + /// Gets or sets the AnalysisResult related to sexual content. + /// + [JsonPropertyName("sexualResult")] + public AnalysisResult? SexualResult { get; set; } + + /// + /// Gets or sets the AnalysisResult related to violence. + /// + [JsonPropertyName("violenceResult")] + public AnalysisResult? ViolenceResult { get; set; } +} diff --git a/webapi/Models/Response/MaintenanceResult.cs b/webapi/Models/Response/MaintenanceResult.cs new file mode 100644 index 0000000..688d164 --- /dev/null +++ b/webapi/Models/Response/MaintenanceResult.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// Defines optional messaging for maintenance mode. +/// +public class MaintenanceResult +{ + /// + /// The maintenance notification title. + /// + /// + /// Will utilize default if not defined. + /// + public string Title { get; set; } = string.Empty; + + /// + /// The maintenance notification message. + /// + /// + /// Will utilize default if not defined. + /// + public string Message { get; set; } = string.Empty; + + /// + /// The maintenance notification note. + /// + /// + /// Will utilize default if not defined. + /// + public string? Note { get; set; } +} diff --git a/webapi/Models/Response/ServiceInfoResponse.cs b/webapi/Models/Response/ServiceInfoResponse.cs new file mode 100644 index 0000000..fb8f8fc --- /dev/null +++ b/webapi/Models/Response/ServiceInfoResponse.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using CopilotChat.WebApi.Options; + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// Information on running service. +/// +public class ServiceInfoResponse +{ + /// + /// Configured memory store. + /// + [JsonPropertyName("memoryStore")] + public MemoryStoreInfoResponse MemoryStore { get; set; } = new MemoryStoreInfoResponse(); + + /// + /// All the available plugins. + /// + [JsonPropertyName("availablePlugins")] + public IEnumerable AvailablePlugins { get; set; } = Enumerable.Empty(); + + /// + /// Version of this application. + /// + [JsonPropertyName("version")] + public string Version { get; set; } = string.Empty; + + /// + /// True if content safety if enabled, false otherwise. + /// + [JsonPropertyName("isContentSafetyEnabled")] + public bool IsContentSafetyEnabled { get; set; } = false; +} + +/// +/// Response to memoryStoreType request. +/// +public class MemoryStoreInfoResponse +{ + /// + /// All the available memory store types. + /// + [JsonPropertyName("types")] + public IEnumerable Types { get; set; } = Enumerable.Empty(); + + /// + /// The selected memory store type. + /// + [JsonPropertyName("selectedType")] + public string SelectedType { get; set; } = string.Empty; +} diff --git a/webapi/Models/Response/SpeechTokenResponse.cs b/webapi/Models/Response/SpeechTokenResponse.cs new file mode 100644 index 0000000..b71b91b --- /dev/null +++ b/webapi/Models/Response/SpeechTokenResponse.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Models.Response; + +/// +/// Token Response is a simple wrapper around the token and region +/// +public class SpeechTokenResponse +{ + public string? Token { get; set; } + public string? Region { get; set; } + public bool? IsSuccess { get; set; } +} diff --git a/webapi/Models/Storage/ChatParticipant.cs b/webapi/Models/Storage/ChatParticipant.cs new file mode 100644 index 0000000..11bd86e --- /dev/null +++ b/webapi/Models/Storage/ChatParticipant.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json.Serialization; +using CopilotChat.WebApi.Storage; + +namespace CopilotChat.WebApi.Models.Storage; + +/// +/// A chat participant is a user that is part of a chat. +/// A user can be part of multiple chats, thus a user can have multiple chat participants. +/// +public class ChatParticipant : IStorageEntity +{ + /// + /// Participant ID that is persistent and unique. + /// + public string Id { get; set; } + + /// + /// User ID that is persistent and unique. + /// + public string UserId { get; set; } + + /// + /// Chat ID that this participant belongs to. + /// + public string ChatId { get; set; } + + /// + /// The partition key for the source. + /// + [JsonIgnore] + public string Partition => this.UserId; + + public ChatParticipant(string userId, string chatId) + { + this.Id = Guid.NewGuid().ToString(); + this.UserId = userId; + this.ChatId = chatId; + } +} diff --git a/webapi/Models/Storage/ChatSession.cs b/webapi/Models/Storage/ChatSession.cs new file mode 100644 index 0000000..c7a1cd6 --- /dev/null +++ b/webapi/Models/Storage/ChatSession.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using CopilotChat.WebApi.Storage; + +namespace CopilotChat.WebApi.Models.Storage; + +/// +/// A chat session +/// +public class ChatSession : IStorageEntity +{ + private const string CurrentVersion = "2.0"; + + /// + /// Chat ID that is persistent and unique. + /// + public string Id { get; set; } + + /// + /// Title of the chat. + /// + public string Title { get; set; } + + /// + /// Timestamp of the chat creation. + /// + public DateTimeOffset CreatedOn { get; set; } + + /// + /// System description of the chat that is used to generate responses. + /// + public string SystemDescription { get; set; } + + /// + /// Fixed system description with "TimeSkill" replaced by "TimePlugin" + /// + public string SafeSystemDescription => this.SystemDescription.Replace("TimeSkill", "TimePlugin", StringComparison.OrdinalIgnoreCase); + + /// + /// The balance between long term memory and working term memory. + /// The higher this value, the more the system will rely on long term memory by lowering + /// the relevance threshold of long term memory and increasing the threshold score of working memory. + /// + public float MemoryBalance { get; set; } = 0.5F; + + /// + /// A list of enabled plugins. + /// + public HashSet EnabledPlugins { get; set; } = new(); + + /// + /// Used to determine if the current chat requires upgrade. + /// + public string? Version { get; set; } + + /// + /// The partition key for the session. + /// + [JsonIgnore] + public string Partition => this.Id; + + /// + /// Initializes a new instance of the class. + /// + /// The title of the chat. + /// The system description of the chat. + public ChatSession(string title, string systemDescription) + { + this.Id = Guid.NewGuid().ToString(); + this.Title = title; + this.CreatedOn = DateTimeOffset.Now; + this.SystemDescription = systemDescription; + this.Version = CurrentVersion; + } +} diff --git a/webapi/Models/Storage/CitationSource.cs b/webapi/Models/Storage/CitationSource.cs new file mode 100644 index 0000000..466687b --- /dev/null +++ b/webapi/Models/Storage/CitationSource.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.KernelMemory; + +namespace CopilotChat.WebApi.Models.Storage; + +/// +/// Information about a citation source. +/// This is a replica of the class in Kernel Memory. +/// Creating a replica here is to avoid taking a direct dependency on Kernel Memory in our data model. +/// +public class CitationSource +{ + /// + /// Link of the citation. + /// + public string Link { get; set; } = string.Empty; + + /// + /// Type of source, e.g. PDF, Word, Chat, etc. + /// + public string SourceContentType { get; set; } = string.Empty; + + /// + /// Name of the source, e.g. file name. + /// + public string SourceName { get; set; } = string.Empty; + + /// + /// The snippet of the citation. + /// + public string Snippet { get; set; } = string.Empty; + + /// + /// Relevance score of the citation against the query. + /// + public double RelevanceScore { get; set; } = 0.0; + + /// + /// Converts a to a . + /// + public static CitationSource FromSemanticMemoryCitation(Citation citation, string snippet, double relevanceScore) + { + var citationSource = new CitationSource + { + Link = citation.Link, + SourceContentType = citation.SourceContentType, + SourceName = citation.SourceName, + Snippet = snippet, + RelevanceScore = relevanceScore + }; + + return citationSource; + } +} diff --git a/webapi/Models/Storage/CopilotChatMessage.cs b/webapi/Models/Storage/CopilotChatMessage.cs new file mode 100644 index 0000000..c0aa13a --- /dev/null +++ b/webapi/Models/Storage/CopilotChatMessage.cs @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using CopilotChat.WebApi.Models.Response; +using CopilotChat.WebApi.Storage; + +namespace CopilotChat.WebApi.Models.Storage; + +/// +/// Information about a single chat message. +/// +public class CopilotChatMessage : IStorageEntity +{ + private static readonly JsonSerializerOptions SerializerSettings = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + /// + /// Role of the author of a chat message. + /// + public enum AuthorRoles + { + /// + /// The current user of the chat. + /// + User = 0, + + /// + /// The bot. + /// + Bot + } + + /// + /// Type of the chat message. + /// + public enum ChatMessageType + { + /// + /// A standard message + /// + Message, + + /// + /// A message for a Plan + /// + Plan, + + /// + /// An uploaded document notification + /// + Document, + } + + /// + /// Timestamp of the message. + /// + public DateTimeOffset Timestamp { get; set; } + + /// + /// Id of the user who sent this message. + /// + public string UserId { get; set; } + + /// + /// Name of the user who sent this message. + /// + public string UserName { get; set; } + + /// + /// Id of the chat this message belongs to. + /// + public string ChatId { get; set; } + + /// + /// Content of the message. + /// + public string Content { get; set; } + + /// + /// Id of the message. + /// + public string Id { get; set; } + + /// + /// Role of the author of the message. + /// + public AuthorRoles AuthorRole { get; set; } + + /// + /// Prompt used to generate the message. + /// Will be empty if the message is not generated by a prompt. + /// + public string Prompt { get; set; } = string.Empty; + + /// + /// Citations of the message. + /// + public IEnumerable? Citations { get; set; } + + /// + /// Type of the message. + /// + public ChatMessageType Type { get; set; } + + /// + /// Counts of total token usage used to generate bot response. + /// + public IDictionary? TokenUsage { get; set; } + + /// + /// The partition key for the source. + /// + [JsonIgnore] + public string Partition => this.ChatId; + + /// + /// Create a new chat message. Timestamp is automatically generated. + /// + /// Id of the user who sent this message + /// Name of the user who sent this message + /// The chat ID that this message belongs to + /// The message + /// The prompt used to generate the message + /// Role of the author + /// Type of the message + /// Total token usages used to generate bot response + public CopilotChatMessage( + string userId, + string userName, + string chatId, + string content, + string? prompt = null, + IEnumerable? citations = null, + AuthorRoles authorRole = AuthorRoles.User, + ChatMessageType type = ChatMessageType.Message, + IDictionary? tokenUsage = null) + { + this.Timestamp = DateTimeOffset.Now; + this.UserId = userId; + this.UserName = userName; + this.ChatId = chatId; + this.Content = content; + this.Id = Guid.NewGuid().ToString(); + this.Prompt = prompt ?? string.Empty; + this.Citations = citations; + this.AuthorRole = authorRole; + this.Type = type; + this.TokenUsage = tokenUsage; + } + + /// + /// Create a new chat message for the bot response. + /// + /// The chat ID that this message belongs to + /// The message + /// The prompt used to generate the message + /// Total token usage of response completion + public static CopilotChatMessage CreateBotResponseMessage(string chatId, string content, string prompt, IEnumerable? citations, IDictionary? tokenUsage = null) + { + return new CopilotChatMessage("Bot", "Bot", chatId, content, prompt, citations, AuthorRoles.Bot, ChatMessageType.Message, tokenUsage); + } + + /// + /// Create a new chat message for a document upload. + /// + /// The user ID that uploaded the document + /// The user name that uploaded the document + /// The chat ID that this message belongs to + /// The document message content + public static CopilotChatMessage CreateDocumentMessage(string userId, string userName, string chatId, DocumentMessageContent documentMessageContent) + { + return new CopilotChatMessage(userId, userName, chatId, documentMessageContent.ToString(), string.Empty, null, AuthorRoles.User, ChatMessageType.Document); + } + + /// + /// Serialize the object to a formatted string. + /// + /// A formatted string + public string ToFormattedString() + { + var messagePrefix = $"[{this.Timestamp.ToString("G", CultureInfo.CurrentCulture)}]"; + switch (this.Type) + { + case ChatMessageType.Document: + var documentMessage = DocumentMessageContent.FromString(this.Content); + var documentMessageContent = (documentMessage != null) ? documentMessage.ToFormattedString() : "documents"; + + return $"{messagePrefix} {this.UserName} uploaded: {documentMessageContent}"; + + case ChatMessageType.Plan: // Fall through + case ChatMessageType.Message: + return $"{messagePrefix} {this.UserName} said: {this.Content}"; + + default: + // This should never happen. + throw new InvalidOperationException($"Unknown message type: {this.Type}"); + } + } + + /// + /// Serialize the object to a JSON string. + /// + /// A serialized json string + public override string ToString() + { + return JsonSerializer.Serialize(this, SerializerSettings); + } + + /// + /// Deserialize a JSON string to a ChatMessage object. + /// + /// A json string + /// A ChatMessage object + public static CopilotChatMessage? FromString(string json) + { + return JsonSerializer.Deserialize(json, SerializerSettings); + } +} diff --git a/webapi/Models/Storage/MemorySource.cs b/webapi/Models/Storage/MemorySource.cs new file mode 100644 index 0000000..a9488ba --- /dev/null +++ b/webapi/Models/Storage/MemorySource.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json.Serialization; +using CopilotChat.WebApi.Storage; + +namespace CopilotChat.WebApi.Models.Storage; + +/// +/// Type of the memory source. +/// +public enum MemorySourceType +{ + // A file source. + File, +} + +/// +/// The external memory source. +/// +public class MemorySource : IStorageEntity +{ + /// + /// Source ID that is persistent and unique. + /// + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// + /// The Chat ID. + /// + [JsonPropertyName("chatId")] + public string ChatId { get; set; } = string.Empty; + + /// + /// The type of the source. + /// + [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonPropertyName("sourceType")] + public MemorySourceType SourceType { get; set; } = MemorySourceType.File; + + /// + /// The name of the source. + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// The external link to the source. + /// + [JsonPropertyName("hyperlink")] + public Uri? HyperLink { get; set; } = null; + + /// + /// The user ID of who shared the source. + /// + [JsonPropertyName("sharedBy")] + public string SharedBy { get; set; } = string.Empty; + + /// + /// When the source is created in the bot. + /// + [JsonPropertyName("createdOn")] + public DateTimeOffset CreatedOn { get; set; } + + /// + /// The size of the source in bytes. + /// + [JsonPropertyName("size")] + public long Size { get; set; } + + /// + /// The number of tokens in the source. + /// + [JsonPropertyName("tokens")] + public long Tokens { get; set; } = 0; + + /// + /// The partition key for the source. + /// + [JsonIgnore] + public string Partition => this.ChatId; + + /// + /// Empty constructor for serialization. + /// + public MemorySource() + { + } + + public MemorySource(string chatId, string name, string sharedBy, MemorySourceType type, long size, Uri? hyperlink) + { + this.Id = Guid.NewGuid().ToString(); + this.ChatId = chatId; + this.Name = name; + this.SourceType = type; + this.HyperLink = hyperlink; + this.SharedBy = sharedBy; + this.CreatedOn = DateTimeOffset.Now; + this.Size = size; + } +} diff --git a/webapi/Models/Storage/MemoryTags.cs b/webapi/Models/Storage/MemoryTags.cs new file mode 100644 index 0000000..25ecfbe --- /dev/null +++ b/webapi/Models/Storage/MemoryTags.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Models.Storage; + +/// +/// Tag names for kernel memory. +/// +internal static class MemoryTags +{ + /// + /// Associates memory with a specific chat + /// + public const string TagChatId = "chatid"; + + /// + /// Associates memory with specific type. + /// + public const string TagMemory = "memory"; +} diff --git a/webapi/Options/AzureSpeechOptions.cs b/webapi/Options/AzureSpeechOptions.cs new file mode 100644 index 0000000..81d5bdd --- /dev/null +++ b/webapi/Options/AzureSpeechOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Options; + +/// +/// Configuration options for Azure speech recognition. +/// +public sealed class AzureSpeechOptions +{ + public const string PropertyName = "AzureSpeech"; + + /// + /// Location of the Azure speech service to use (e.g. "South Central US") + /// + public string? Region { get; set; } = string.Empty; + + /// + /// Key to access the Azure speech service. + /// + public string? Key { get; set; } = string.Empty; +} diff --git a/webapi/Options/ChatArchiveSchemaInfo.cs b/webapi/Options/ChatArchiveSchemaInfo.cs new file mode 100644 index 0000000..a9a1df5 --- /dev/null +++ b/webapi/Options/ChatArchiveSchemaInfo.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel.DataAnnotations; + +namespace CopilotChat.WebApi.Options; + +/// +/// Information on schema used to serialize chat archives. +/// +public record ChatArchiveSchemaInfo +{ + /// + /// The name of the schema. + /// + [Required, NotEmptyOrWhitespace] + public string Name { get; init; } = "CopilotChat"; + + /// + /// The version of the schema. + /// + [Range(0, int.MaxValue)] + public int Version { get; init; } = 1; +} diff --git a/webapi/Options/ChatAuthenticationOptions.cs b/webapi/Options/ChatAuthenticationOptions.cs new file mode 100644 index 0000000..27de39b --- /dev/null +++ b/webapi/Options/ChatAuthenticationOptions.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel.DataAnnotations; + +namespace CopilotChat.WebApi.Options; + +/// +/// Configuration options for authenticating to the service. +/// +public class ChatAuthenticationOptions +{ + public const string PropertyName = "Authentication"; + + public enum AuthenticationType + { + None, + AzureAd + } + + /// + /// Type of authentication. + /// + [Required] + public AuthenticationType Type { get; set; } = AuthenticationType.None; + + /// + /// When is , these are the Azure AD options to use. + /// + [RequiredOnPropertyValue(nameof(Type), AuthenticationType.AzureAd)] + public AzureAdOptions? AzureAd { get; set; } + + /// + /// Configuration options for Azure Active Directory (AAD) authorization. + /// + public class AzureAdOptions + { + /// + /// AAD instance url, i.e., https://login.microsoftonline.com + /// + [Required, NotEmptyOrWhitespace] + public string Instance { get; set; } = string.Empty; + + /// + /// Tenant (directory) ID + /// + [Required, NotEmptyOrWhitespace] + public string TenantId { get; set; } = string.Empty; + + /// + /// Application (client) ID + /// + [Required, NotEmptyOrWhitespace] + public string ClientId { get; set; } = string.Empty; + + /// + /// Required scopes. + /// + [Required] + public string? Scopes { get; set; } = string.Empty; + } +} diff --git a/webapi/Options/ChatStoreOptions.cs b/webapi/Options/ChatStoreOptions.cs new file mode 100644 index 0000000..27f25a2 --- /dev/null +++ b/webapi/Options/ChatStoreOptions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Options; + +/// +/// Configuration settings for the chat store. +/// +public class ChatStoreOptions +{ + public const string PropertyName = "ChatStore"; + + /// + /// The type of chat store to use. + /// + public enum ChatStoreType + { + /// + /// Non-persistent chat store + /// + Volatile, + + /// + /// File-system based persistent chat store. + /// + Filesystem, + + /// + /// Azure CosmosDB based persistent chat store. + /// + Cosmos + } + + /// + /// Gets or sets the type of chat store to use. + /// + public ChatStoreType Type { get; set; } = ChatStoreType.Volatile; + + /// + /// Gets or sets the configuration for the file system chat store. + /// + [RequiredOnPropertyValue(nameof(Type), ChatStoreType.Filesystem)] + public FileSystemOptions? Filesystem { get; set; } + + /// + /// Gets or sets the configuration for the Azure CosmosDB chat store. + /// + [RequiredOnPropertyValue(nameof(Type), ChatStoreType.Cosmos)] + public CosmosOptions? Cosmos { get; set; } +} diff --git a/webapi/Options/ContentSafetyOptions.cs b/webapi/Options/ContentSafetyOptions.cs new file mode 100644 index 0000000..8374367 --- /dev/null +++ b/webapi/Options/ContentSafetyOptions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace CopilotChat.WebApi.Options; + +/// +/// Configuration options for content safety. +/// +public class ContentSafetyOptions +{ + public const string PropertyName = "ContentSafety"; + + /// + /// Whether to enable content safety. + /// + [Required] + public bool Enabled { get; set; } = false; + + /// + /// Azure Content Safety endpoints + /// + [RequiredOnPropertyValue(nameof(Enabled), true)] + public string Endpoint { get; set; } = string.Empty; + + /// + /// Key to access the content safety service. + /// + [RequiredOnPropertyValue(nameof(Enabled), true)] + public string Key { get; set; } = string.Empty; + + /// + /// Set the violation threshold. See https://learn.microsoft.com/en-us/azure/ai-services/content-safety/quickstart-image for details. + /// + [Range(0, 6)] + public short ViolationThreshold { get; set; } = 4; +} diff --git a/webapi/Options/CosmosOptions.cs b/webapi/Options/CosmosOptions.cs new file mode 100644 index 0000000..c0e20bb --- /dev/null +++ b/webapi/Options/CosmosOptions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel.DataAnnotations; + +namespace CopilotChat.WebApi.Options; + +/// +/// Configuration settings for connecting to Azure CosmosDB. +/// +public class CosmosOptions +{ + /// + /// Gets or sets the Cosmos database name. + /// + [Required, NotEmptyOrWhitespace] + public string Database { get; set; } = string.Empty; + + /// + /// Gets or sets the Cosmos connection string. + /// + [Required, NotEmptyOrWhitespace] + public string ConnectionString { get; set; } = string.Empty; + + /// + /// Gets or sets the Cosmos container for chat sessions. + /// + [Required, NotEmptyOrWhitespace] + public string ChatSessionsContainer { get; set; } = string.Empty; + + /// + /// Gets or sets the Cosmos container for chat messages. + /// + [Required, NotEmptyOrWhitespace] + public string ChatMessagesContainer { get; set; } = string.Empty; + + /// + /// Gets or sets the Cosmos container for chat memory sources. + /// + [Required, NotEmptyOrWhitespace] + public string ChatMemorySourcesContainer { get; set; } = string.Empty; + + /// + /// Gets or sets the Cosmos container for chat participants. + /// + [Required, NotEmptyOrWhitespace] + public string ChatParticipantsContainer { get; set; } = string.Empty; +} diff --git a/webapi/Options/DocumentMemoryOptions.cs b/webapi/Options/DocumentMemoryOptions.cs new file mode 100644 index 0000000..5d50aa6 --- /dev/null +++ b/webapi/Options/DocumentMemoryOptions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace CopilotChat.WebApi.Options; + +/// +/// Configuration options for handling memorized documents. +/// +public class DocumentMemoryOptions +{ + public const string PropertyName = "DocumentMemory"; + + /// + /// Global documents will be tagged by an empty Guid as chat-id ("00000000-0000-0000-0000-000000000000"). + /// + internal static readonly Guid GlobalDocumentChatId = Guid.Empty; + + /// + /// Gets or sets the maximum number of tokens to use when splitting a document into "lines". + /// For more details on tokens and how to count them, see: + /// https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them + /// + [Range(0, int.MaxValue)] + public int DocumentLineSplitMaxTokens { get; set; } = 30; + + /// + /// Gets or sets the maximum number of tokens to use when splitting documents for embeddings. + /// For more details on tokens and how to count them, see: + /// https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them + /// + [Range(0, int.MaxValue)] + public int DocumentChunkMaxTokens { get; set; } = 100; + + /// + /// Maximum size in bytes of a document to be allowed for importing. + /// Prevent large uploads by setting a file size limit (in bytes) as suggested here: + /// https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-6.0 + /// + [Range(0, int.MaxValue)] + public int FileSizeLimit { get; set; } = 1000000; + + /// + /// Maximum number of files to be allowed for importing in a single request. + /// + [Range(0, int.MaxValue)] + public int FileCountLimit { get; set; } = 10; +} diff --git a/webapi/Options/FileSystemOptions.cs b/webapi/Options/FileSystemOptions.cs new file mode 100644 index 0000000..c6c62ca --- /dev/null +++ b/webapi/Options/FileSystemOptions.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel.DataAnnotations; + +namespace CopilotChat.WebApi.Options; + +/// +/// File system storage configuration. +/// +public class FileSystemOptions +{ + /// + /// Gets or sets the file path for persistent file system storage. + /// + [Required, NotEmptyOrWhitespace] + public string FilePath { get; set; } = string.Empty; +} diff --git a/webapi/Options/FrontendOptions.cs b/webapi/Options/FrontendOptions.cs new file mode 100644 index 0000000..ce91bce --- /dev/null +++ b/webapi/Options/FrontendOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace CopilotChat.WebApi.Options; + +/// +/// Configuration options to be relayed to the frontend. +/// +public sealed class FrontendOptions +{ + public const string PropertyName = "Frontend"; + + /// + /// Client ID for the frontend + /// + public string AadClientId { get; set; } = string.Empty; +} diff --git a/webapi/Options/MemoryStoreType.cs b/webapi/Options/MemoryStoreType.cs new file mode 100644 index 0000000..d681523 --- /dev/null +++ b/webapi/Options/MemoryStoreType.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.KernelMemory; +using Microsoft.KernelMemory.MemoryStorage.DevTools; + +namespace CopilotChat.WebApi.Options; + +/// +/// The type of memory store to use. +/// +public enum MemoryStoreType +{ + /// + /// In-memory volatile memory store. + /// + Volatile, + + /// + /// File system based persistent memory store. + /// + TextFile, + + /// + /// Qdrant based persistent memory store. + /// + Qdrant, + + /// + /// Azure AI Search persistent memory store. + /// + AzureAISearch, +} + +public static class MemoryStoreTypeExtensions +{ + /// + /// Gets the memory store type from the configuration. + /// Volatile and TextFile are storage solutions in SimpleVectorDb. + /// If SimpleVectorDb is configured, then the storage type is determined by the SimpleVectorDb configuration. + /// + /// The configuration. + /// The memory store type. + public static MemoryStoreType GetMemoryStoreType(this KernelMemoryConfig memoryOptions, IConfiguration configuration) + { + var type = memoryOptions.Retrieval.MemoryDbType; + if (type.Equals("AzureAISearch", StringComparison.OrdinalIgnoreCase)) + { + return MemoryStoreType.AzureAISearch; + } + else if (type.Equals("Qdrant", StringComparison.OrdinalIgnoreCase)) + { + return MemoryStoreType.Qdrant; + } + else if (type.Equals("SimpleVectorDb", StringComparison.OrdinalIgnoreCase)) + { + var simpleVectorDbConfig = memoryOptions.GetServiceConfig(configuration, "SimpleVectorDb"); + if (simpleVectorDbConfig != null) + { + type = simpleVectorDbConfig.StorageType.ToString(); + if (type.Equals("Volatile", StringComparison.OrdinalIgnoreCase)) + { + return MemoryStoreType.Volatile; + } + else if (type.Equals("Disk", StringComparison.OrdinalIgnoreCase)) + { + return MemoryStoreType.TextFile; + } + } + } + + throw new ArgumentException($"Invalid memory store type: {type}"); + } +} diff --git a/webapi/Options/NotEmptyOrWhitespaceAttribute.cs b/webapi/Options/NotEmptyOrWhitespaceAttribute.cs new file mode 100644 index 0000000..a38dd28 --- /dev/null +++ b/webapi/Options/NotEmptyOrWhitespaceAttribute.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel.DataAnnotations; + +namespace CopilotChat.WebApi.Options; + +/// +/// If the string is set, it must not be empty or whitespace. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +internal sealed class NotEmptyOrWhitespaceAttribute : ValidationAttribute +{ + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + if (value == null) + { + return ValidationResult.Success; + } + + if (value is string s) + { + if (!string.IsNullOrWhiteSpace(s)) + { + return ValidationResult.Success; + } + + return new ValidationResult($"'{validationContext.MemberName}' cannot be empty or whitespace."); + } + + return new ValidationResult($"'{validationContext.MemberName}' must be a string."); + } +} diff --git a/webapi/Options/PluginOptions.cs b/webapi/Options/PluginOptions.cs new file mode 100644 index 0000000..cad8524 --- /dev/null +++ b/webapi/Options/PluginOptions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; + +namespace CopilotChat.WebApi.Options; + +/// +/// Option for a single plugin. +/// +public class Plugin +{ + /// + /// The name of the plugin. + /// + public string Name { get; set; } = string.Empty; + + /// + /// The url of the plugin. + /// + public Uri ManifestDomain { get; set; } = new Uri("http://localhost"); + + /// + /// The key of the plugin. + /// + public string Key { get; set; } = string.Empty; +} diff --git a/webapi/Options/PromptsOptions.cs b/webapi/Options/PromptsOptions.cs new file mode 100644 index 0000000..5dd1edc --- /dev/null +++ b/webapi/Options/PromptsOptions.cs @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using CopilotChat.WebApi.Models.Request; + +namespace CopilotChat.WebApi.Options; + +/// +/// Configuration options for the chat +/// +public class PromptsOptions +{ + public const string PropertyName = "Prompts"; + + /// + /// Token limit of the chat model. + /// + /// https://platform.openai.com/docs/models/overview for token limits. + [Required, Range(0, int.MaxValue)] public int CompletionTokenLimit { get; set; } + + /// + /// The token count left for the model to generate text after the prompt. + /// + [Required, Range(0, int.MaxValue)] public int ResponseTokenLimit { get; set; } + + /// + /// Weight of memories in the contextual part of the final prompt. + /// Contextual prompt excludes all the system commands and user intent. + /// + internal double MemoriesResponseContextWeight { get; } = 0.6; + + /// + /// Upper bound of the relevancy score of a kernel memory to be included in the final prompt. + /// The actual relevancy score is determined by the memory balance. + /// + internal float SemanticMemoryRelevanceUpper { get; } = 0.9F; + + /// + /// Lower bound of the relevancy score of a kernel memory to be included in the final prompt. + /// The actual relevancy score is determined by the memory balance. + /// + internal float SemanticMemoryRelevanceLower { get; } = 0.6F; + + /// + /// Minimum relevance of a document memory to be included in the final prompt. + /// The higher the value, the answer will be more relevant to the user intent. + /// + internal float DocumentMemoryMinRelevance { get; } = 0.8F; + + // System + [Required, NotEmptyOrWhitespace] public string KnowledgeCutoffDate { get; set; } = string.Empty; + [Required, NotEmptyOrWhitespace] public string InitialBotMessage { get; set; } = string.Empty; + [Required, NotEmptyOrWhitespace] public string SystemDescription { get; set; } = string.Empty; + [Required, NotEmptyOrWhitespace] public string SystemResponse { get; set; } = string.Empty; + + internal string[] SystemAudiencePromptComponents => new string[] + { + this.SystemAudience, + "{{ChatPlugin.ExtractChatHistory}}", + this.SystemAudienceContinuation + }; + + internal string SystemAudienceExtraction => string.Join("\n", this.SystemAudiencePromptComponents); + + internal string[] SystemIntentPromptComponents => new string[] + { + this.SystemDescription, + this.SystemIntent, + "{{ChatPlugin.ExtractChatHistory}}", + this.SystemIntentContinuation + }; + + internal string SystemIntentExtraction => string.Join("\n", this.SystemIntentPromptComponents); + + // Intent extraction + [Required, NotEmptyOrWhitespace] public string SystemIntent { get; set; } = string.Empty; + [Required, NotEmptyOrWhitespace] public string SystemIntentContinuation { get; set; } = string.Empty; + + // Audience extraction + [Required, NotEmptyOrWhitespace] public string SystemAudience { get; set; } = string.Empty; + [Required, NotEmptyOrWhitespace] public string SystemAudienceContinuation { get; set; } = string.Empty; + + // Memory storage + [Required, NotEmptyOrWhitespace] public string MemoryIndexName { get; set; } = string.Empty; + + // Document memory + [Required, NotEmptyOrWhitespace] public string DocumentMemoryName { get; set; } = string.Empty; + + // Memory extraction + [Required, NotEmptyOrWhitespace] public string SystemCognitive { get; set; } = string.Empty; + [Required, NotEmptyOrWhitespace] public string MemoryFormat { get; set; } = string.Empty; + [Required, NotEmptyOrWhitespace] public string MemoryAntiHallucination { get; set; } = string.Empty; + [Required, NotEmptyOrWhitespace] public string MemoryContinuation { get; set; } = string.Empty; + + // Long-term memory + [Required, NotEmptyOrWhitespace] public string LongTermMemoryName { get; set; } = string.Empty; + [Required, NotEmptyOrWhitespace] public string LongTermMemoryExtraction { get; set; } = string.Empty; + + internal string[] LongTermMemoryPromptComponents => new string[] + { + this.SystemCognitive, + $"{this.LongTermMemoryName} Description:\n{this.LongTermMemoryExtraction}", + this.MemoryAntiHallucination, + $"Chat Description:\n{this.SystemDescription}", + "{{ChatPlugin.ExtractChatHistory}}", + this.MemoryContinuation + }; + + internal string LongTermMemory => string.Join("\n", this.LongTermMemoryPromptComponents); + + // Working memory + [Required, NotEmptyOrWhitespace] public string WorkingMemoryName { get; set; } = string.Empty; + [Required, NotEmptyOrWhitespace] public string WorkingMemoryExtraction { get; set; } = string.Empty; + + internal string[] WorkingMemoryPromptComponents => new string[] + { + this.SystemCognitive, + $"{this.WorkingMemoryName} Description:\n{this.WorkingMemoryExtraction}", + this.MemoryAntiHallucination, + $"Chat Description:\n{this.SystemDescription}", + "{{ChatPlugin.ExtractChatHistory}}", + this.MemoryContinuation + }; + + internal string WorkingMemory => string.Join("\n", this.WorkingMemoryPromptComponents); + + // Memory map + internal IDictionary MemoryMap => new Dictionary() + { + { this.LongTermMemoryName, this.LongTermMemory }, + { this.WorkingMemoryName, this.WorkingMemory } + }; + + // Chat commands + internal string[] SystemPersonaComponents => new string[] + { + this.SystemDescription, + this.SystemResponse, + }; + + internal string SystemPersona => string.Join("\n\n", this.SystemPersonaComponents); + + internal double ResponseTemperature { get; } = 0.7; + internal double ResponseTopP { get; } = 1; + internal double ResponsePresencePenalty { get; } = 0.5; + internal double ResponseFrequencyPenalty { get; } = 0.5; + + internal double IntentTemperature { get; } = 0.7; + internal double IntentTopP { get; } = 1; + internal double IntentPresencePenalty { get; } = 0.5; + internal double IntentFrequencyPenalty { get; } = 0.5; + + /// + /// Copy the options in case they need to be modified per chat. + /// + /// A shallow copy of the options. + internal PromptsOptions Copy() => (PromptsOptions)this.MemberwiseClone(); + + /// + /// Tries to retrieve the memoryContainerName associated with the specified memory type. + /// + internal bool TryGetMemoryContainerName(string memoryType, out string memoryContainerName) + { + memoryContainerName = ""; + if (!Enum.TryParse(memoryType, true, out SemanticMemoryType semanticMemoryType)) + { + return false; + } + + switch (semanticMemoryType) + { + case SemanticMemoryType.LongTermMemory: + memoryContainerName = this.LongTermMemoryName; + return true; + + case SemanticMemoryType.WorkingMemory: + memoryContainerName = this.WorkingMemoryName; + return true; + + default: return false; + } + } +} diff --git a/webapi/Options/RequiredOnPropertyValueAttribute.cs b/webapi/Options/RequiredOnPropertyValueAttribute.cs new file mode 100644 index 0000000..a46a395 --- /dev/null +++ b/webapi/Options/RequiredOnPropertyValueAttribute.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace CopilotChat.WebApi.Options; + +/// +/// If the other property is set to the expected value, then this property is required. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +internal sealed class RequiredOnPropertyValueAttribute : ValidationAttribute +{ + /// + /// Name of the other property. + /// + public string OtherPropertyName { get; } + + /// + /// Value of the other property when this property is required. + /// + public object? OtherPropertyValue { get; } + + /// + /// True to make sure that the value is not empty or whitespace when required. + /// + public bool NotEmptyOrWhitespace { get; } + + /// + /// If the other property is set to the expected value, then this property is required. + /// + /// Name of the other property. + /// Value of the other property when this property is required. + /// True to make sure that the value is not empty or whitespace when required. + public RequiredOnPropertyValueAttribute(string otherPropertyName, object? otherPropertyValue, bool notEmptyOrWhitespace = true) + { + this.OtherPropertyName = otherPropertyName; + this.OtherPropertyValue = otherPropertyValue; + this.NotEmptyOrWhitespace = notEmptyOrWhitespace; + } + + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) + { + PropertyInfo? otherPropertyInfo = validationContext.ObjectType.GetRuntimeProperty(this.OtherPropertyName); + + // If the other property is not found, return an error. + if (otherPropertyInfo == null) + { + return new ValidationResult($"Unknown other property name '{this.OtherPropertyName}'."); + } + + // If the other property is an indexer, return an error. + if (otherPropertyInfo.GetIndexParameters().Length > 0) + { + throw new ArgumentException($"Other property not found ('{validationContext.MemberName}, '{this.OtherPropertyName}')."); + } + + object? otherPropertyValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance, null); + + // If the other property is set to the expected value, then this property is required. + if (Equals(this.OtherPropertyValue, otherPropertyValue)) + { + if (value == null) + { + return new ValidationResult($"Property '{validationContext.DisplayName}' is required when '{this.OtherPropertyName}' is {this.OtherPropertyValue}."); + } + else if (this.NotEmptyOrWhitespace && string.IsNullOrWhiteSpace(value.ToString())) + { + return new ValidationResult($"Property '{validationContext.DisplayName}' cannot be empty or whitespace when '{this.OtherPropertyName}' is {this.OtherPropertyValue}."); + } + else + { + return ValidationResult.Success; + } + } + + return ValidationResult.Success; + } +} diff --git a/webapi/Options/ServiceOptions.cs b/webapi/Options/ServiceOptions.cs new file mode 100644 index 0000000..dfbdf72 --- /dev/null +++ b/webapi/Options/ServiceOptions.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel.DataAnnotations; + +namespace CopilotChat.WebApi.Options; + +/// +/// Configuration options for the Chat Copilot service. +/// +public class ServiceOptions +{ + public const string PropertyName = "Service"; + + /// + /// Timeout limit on requests to the service in seconds. + /// + [Range(0, int.MaxValue)] + public double? TimeoutLimitInS { get; set; } + + /// + /// Configuration Key Vault URI + /// + [Url] + public string? KeyVault { get; set; } + + /// + /// Local directory from which to load semantic plugins. + /// + public string? SemanticPluginsDirectory { get; set; } + + /// + /// Local directory from which to load native plugins. + /// + public string? NativePluginsDirectory { get; set; } + + /// + /// Setting indicating if the site is undergoing maintenance. + /// + public bool InMaintenance { get; set; } +} diff --git a/webapi/Plugins/Chat/ChatPlugin.cs b/webapi/Plugins/Chat/ChatPlugin.cs new file mode 100644 index 0000000..010072c --- /dev/null +++ b/webapi/Plugins/Chat/ChatPlugin.cs @@ -0,0 +1,728 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using CopilotChat.WebApi.Auth; +using CopilotChat.WebApi.Hubs; +using CopilotChat.WebApi.Models.Response; +using CopilotChat.WebApi.Models.Storage; +using CopilotChat.WebApi.Options; +using CopilotChat.WebApi.Plugins.Utils; +using CopilotChat.WebApi.Services; +using CopilotChat.WebApi.Storage; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.KernelMemory; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using CopilotChatMessage = CopilotChat.WebApi.Models.Storage.CopilotChatMessage; + +namespace CopilotChat.WebApi.Plugins.Chat; + +/// +/// ChatPlugin offers a more coherent chat experience by using memories +/// to extract conversation history and user intentions. +/// +public class ChatPlugin +{ + /// + /// A kernel instance to create a completion function since each invocation + /// of the function will generate a new prompt dynamically. + /// + private readonly Kernel _kernel; + + /// + /// Client for the kernel memory service. + /// + private readonly IKernelMemory _memoryClient; + + /// + /// A logger instance to log events. + /// + private ILogger _logger; + + /// + /// A repository to save and retrieve chat messages. + /// + private readonly ChatMessageRepository _chatMessageRepository; + + /// + /// A repository to save and retrieve chat sessions. + /// + private readonly ChatSessionRepository _chatSessionRepository; + + /// + /// A SignalR hub context to broadcast updates of the execution. + /// + private readonly IHubContext _messageRelayHubContext; + + /// + /// Settings containing prompt texts. + /// + private readonly PromptsOptions _promptOptions; + + /// + /// A kernel memory retriever instance to query semantic memories. + /// + private readonly SemanticMemoryRetriever _semanticMemoryRetriever; + + /// + /// Azure content safety moderator. + /// + private readonly AzureContentSafety? _contentSafety = null; + + /// + /// Create a new instance of . + /// + public ChatPlugin( + Kernel kernel, + IKernelMemory memoryClient, + ChatMessageRepository chatMessageRepository, + ChatSessionRepository chatSessionRepository, + IHubContext messageRelayHubContext, + IOptions promptOptions, + IOptions documentImportOptions, + ILogger logger, + AzureContentSafety? contentSafety = null) + { + this._logger = logger; + this._kernel = kernel; + this._memoryClient = memoryClient; + this._chatMessageRepository = chatMessageRepository; + this._chatSessionRepository = chatSessionRepository; + this._messageRelayHubContext = messageRelayHubContext; + // Clone the prompt options to avoid modifying the original prompt options. + this._promptOptions = promptOptions.Value.Copy(); + + this._semanticMemoryRetriever = new SemanticMemoryRetriever(promptOptions, chatSessionRepository, memoryClient, logger); + + this._contentSafety = contentSafety; + } + + /// + /// Method that wraps GetAllowedChatHistoryAsync to get allotted history messages as one string. + /// GetAllowedChatHistoryAsync optionally updates a ChatHistory object with the allotted messages, + /// but the ChatHistory type is not supported when calling from a rendered prompt, so this wrapper bypasses the chatHistory parameter. + /// + /// The cancellation token. + [KernelFunction, Description("Extract chat history")] + public Task ExtractChatHistory( + [Description("Chat ID to extract history from")] string chatId, + [Description("Maximum number of tokens")] int tokenLimit, + CancellationToken cancellationToken = default) + { + return this.GetAllowedChatHistoryAsync(chatId, tokenLimit, cancellationToken: cancellationToken); + } + + /// + /// Extract chat history within token limit as a formatted string and optionally update the ChatHistory object with the allotted messages + /// + /// Chat ID to extract history from. + /// Maximum number of tokens. + /// Optional ChatHistory object tracking allotted messages. + /// The cancellation token. + /// Chat history as a string. + private async Task GetAllowedChatHistoryAsync( + string chatId, + int tokenLimit, + ChatHistory? chatHistory = null, + CancellationToken cancellationToken = default) + { + var sortedMessages = await this._chatMessageRepository.FindByChatIdAsync(chatId, 0, 100); + + ChatHistory allottedChatHistory = new(); + var remainingToken = tokenLimit; + string historyText = string.Empty; + + foreach (var chatMessage in sortedMessages) + { + var formattedMessage = chatMessage.ToFormattedString(); + + if (chatMessage.Type == CopilotChatMessage.ChatMessageType.Document) + { + continue; + } + + var promptRole = chatMessage.AuthorRole == CopilotChatMessage.AuthorRoles.Bot ? AuthorRole.System : AuthorRole.User; + int tokenCount = chatHistory is not null ? TokenUtils.GetContextMessageTokenCount(promptRole, formattedMessage) : TokenUtils.TokenCount(formattedMessage); + + if (remainingToken - tokenCount >= 0) + { + historyText = $"{formattedMessage}\n{historyText}"; + if (chatMessage.AuthorRole == CopilotChatMessage.AuthorRoles.Bot) + { + // Message doesn't have to be formatted for bot. This helps with asserting a natural language response from the LLM (no date or author preamble). + allottedChatHistory.AddAssistantMessage(chatMessage.Content.Trim()); + } + else + { + // Omit user name if Auth is disabled. + var userMessage = PassThroughAuthenticationHandler.IsDefaultUser(chatMessage.UserId) + ? $"[{chatMessage.Timestamp.ToString("G", CultureInfo.CurrentCulture)}] {chatMessage.Content}" + : formattedMessage; + allottedChatHistory.AddUserMessage(userMessage.Trim()); + } + + remainingToken -= tokenCount; + } + else + { + break; + } + } + + chatHistory?.AddRange(allottedChatHistory.Reverse()); + + return $"Chat history:\n{historyText.Trim()}"; + } + + /// + /// This is the entry point for getting a chat response. It manages the token limit, saves + /// messages to memory, and fills in the necessary context variables for completing the + /// prompt that will be rendered by the template engine. + /// + /// The cancellation token. + [KernelFunction, Description("Get chat response")] + public async Task ChatAsync( + [Description("The new message")] string message, + [Description("Unique and persistent identifier for the user")] string userId, + [Description("Name of the user")] string userName, + [Description("Unique and persistent identifier for the chat")] string chatId, + [Description("Type of the message")] string messageType, + KernelArguments context, + CancellationToken cancellationToken = default) + { + // Set the system description in the prompt options + await this.SetSystemDescriptionAsync(chatId, cancellationToken); + + // Save this new message to memory such that subsequent chat responses can use it + await this.UpdateBotResponseStatusOnClientAsync(chatId, "Saving user message to chat history", cancellationToken); + var newUserMessage = await this.SaveNewMessageAsync(message, userId, userName, chatId, messageType, cancellationToken); + + // Clone the context to avoid modifying the original context variables. + KernelArguments chatContext = new(context); + chatContext["knowledgeCutoff"] = this._promptOptions.KnowledgeCutoffDate; + + CopilotChatMessage chatMessage = await this.GetChatResponseAsync(chatId, userId, chatContext, newUserMessage, cancellationToken); + context["input"] = chatMessage.Content; + + if (chatMessage.TokenUsage != null) + { + context["tokenUsage"] = JsonSerializer.Serialize(chatMessage.TokenUsage); + } + else + { + this._logger.LogWarning("ChatPlugin.ChatAsync token usage unknown. Ensure token management has been implemented correctly."); + } + + return context; + } + + /// + /// Generate the necessary chat context to create a prompt then invoke the model to get a response. + /// + /// The chat ID + /// The user ID + /// The KernelArguments. + /// ChatMessage object representing new user message. + /// The cancellation token. + /// The created chat message containing the model-generated response. + private async Task GetChatResponseAsync(string chatId, string userId, KernelArguments chatContext, CopilotChatMessage userMessage, CancellationToken cancellationToken) + { + // Render system instruction components and create the meta-prompt template + var systemInstructions = await AsyncUtils.SafeInvokeAsync( + () => this.RenderSystemInstructions(chatId, chatContext, cancellationToken), nameof(RenderSystemInstructions)); + ChatHistory metaPrompt = new(systemInstructions); + + // Bypass audience extraction if Auth is disabled + var audience = string.Empty; + if (!PassThroughAuthenticationHandler.IsDefaultUser(userId)) + { + // Get the audience + await this.UpdateBotResponseStatusOnClientAsync(chatId, "Extracting audience", cancellationToken); + audience = await AsyncUtils.SafeInvokeAsync( + () => this.GetAudienceAsync(chatContext, cancellationToken), nameof(GetAudienceAsync)); + metaPrompt.AddSystemMessage(audience); + } + + // Extract user intent from the conversation history. + await this.UpdateBotResponseStatusOnClientAsync(chatId, "Extracting user intent", cancellationToken); + var userIntent = await AsyncUtils.SafeInvokeAsync( + () => this.GetUserIntentAsync(chatContext, cancellationToken), nameof(GetUserIntentAsync)); + metaPrompt.AddSystemMessage(userIntent); + + // Calculate max amount of tokens to use for memories + int maxRequestTokenBudget = this.GetMaxRequestTokenBudget(); + // Calculate tokens used so far: system instructions, audience extraction and user intent + int tokensUsed = TokenUtils.GetContextMessagesTokenCount(metaPrompt); + int chatMemoryTokenBudget = maxRequestTokenBudget + - tokensUsed + - TokenUtils.GetContextMessageTokenCount(AuthorRole.User, userMessage.ToFormattedString()); + chatMemoryTokenBudget = (int)(chatMemoryTokenBudget * this._promptOptions.MemoriesResponseContextWeight); + + // Query relevant semantic and document memories + await this.UpdateBotResponseStatusOnClientAsync(chatId, "Extracting semantic and document memories", cancellationToken); + (var memoryText, var citationMap) = await this._semanticMemoryRetriever.QueryMemoriesAsync(userIntent, chatId, chatMemoryTokenBudget); + if (!string.IsNullOrWhiteSpace(memoryText)) + { + metaPrompt.AddSystemMessage(memoryText); + tokensUsed += TokenUtils.GetContextMessageTokenCount(AuthorRole.System, memoryText); + } + + // Add as many chat history messages to meta-prompt as the token budget will allow + await this.UpdateBotResponseStatusOnClientAsync(chatId, "Extracting chat history", cancellationToken); + string allowedChatHistory = await this.GetAllowedChatHistoryAsync(chatId, maxRequestTokenBudget - tokensUsed, metaPrompt, cancellationToken); + + // Store token usage of prompt template + chatContext[TokenUtils.GetFunctionKey("SystemMetaPrompt")] = TokenUtils.GetContextMessagesTokenCount(metaPrompt).ToString(CultureInfo.CurrentCulture); + + // Stream the response to the client + var promptView = new BotResponsePrompt(systemInstructions, audience, userIntent, memoryText, allowedChatHistory, metaPrompt); + + return await this.HandleBotResponseAsync(chatId, userId, chatContext, promptView, citationMap.Values.AsEnumerable(), cancellationToken); + } + + /// + /// Helper function to render system instruction components. + /// + /// The chat ID + /// The KernelArguments. + /// The cancellation token. + private async Task RenderSystemInstructions(string chatId, KernelArguments context, CancellationToken cancellationToken) + { + // Render system instruction components + await this.UpdateBotResponseStatusOnClientAsync(chatId, "Initializing prompt", cancellationToken); + + var promptTemplateFactory = new KernelPromptTemplateFactory(); + var promptTemplate = promptTemplateFactory.Create(new PromptTemplateConfig(this._promptOptions.SystemPersona)); + return await promptTemplate.RenderAsync(this._kernel, context, cancellationToken); + } + + /// + /// Helper function to handle final steps of bot response generation, including streaming to client, + /// generating semantic text memory, calculating final token usages, and saving to chat history. + /// + /// The chat ID + /// The user ID + /// Chat context. + /// The prompt view. + /// Citation sources. + /// The cancellation token. + private async Task HandleBotResponseAsync( + string chatId, + string userId, + KernelArguments chatContext, + BotResponsePrompt promptView, + IEnumerable? citations, + CancellationToken cancellationToken) + { + // Get bot response and stream to client + await this.UpdateBotResponseStatusOnClientAsync(chatId, "Generating bot response", cancellationToken); + CopilotChatMessage chatMessage = await AsyncUtils.SafeInvokeAsync( + () => this.StreamResponseToClientAsync(chatId, userId, promptView, cancellationToken, citations), nameof(StreamResponseToClientAsync)); + + // Save the message into chat history + await this.UpdateBotResponseStatusOnClientAsync(chatId, "Saving message to chat history", cancellationToken); + await this._chatMessageRepository.UpsertAsync(chatMessage); + + // Extract semantic chat memory + await this.UpdateBotResponseStatusOnClientAsync(chatId, "Generating semantic chat memory", cancellationToken); + await AsyncUtils.SafeInvokeAsync( + () => SemanticChatMemoryExtractor.ExtractSemanticChatMemoryAsync( + chatId, + this._memoryClient, + this._kernel, + chatContext, + this._promptOptions, + this._logger, + cancellationToken), nameof(SemanticChatMemoryExtractor.ExtractSemanticChatMemoryAsync)); + + // Calculate total token usage for dependency functions and prompt template + await this.UpdateBotResponseStatusOnClientAsync(chatId, "Saving token usage", cancellationToken); + chatMessage.TokenUsage = this.GetTokenUsages(chatContext, chatMessage.Content); + + // Update the message on client and in chat history with final completion token usage + await this.UpdateMessageOnClient(chatMessage, cancellationToken); + await this._chatMessageRepository.UpsertAsync(chatMessage); + + return chatMessage; + } + + /// + /// Extract the list of participants from the conversation history. + /// Note that only those who have spoken will be included. + /// + /// Kernel context variables. + /// The cancellation token. + private async Task GetAudienceAsync(KernelArguments context, CancellationToken cancellationToken) + { + // Clone the context to avoid modifying the original context variables + KernelArguments audienceContext = new(context); + int historyTokenBudget = + this._promptOptions.CompletionTokenLimit - + this._promptOptions.ResponseTokenLimit - + TokenUtils.TokenCount(string.Join("\n\n", new string[] + { + this._promptOptions.SystemAudience, + this._promptOptions.SystemAudienceContinuation, + }) + ); + + audienceContext["tokenLimit"] = historyTokenBudget.ToString(new NumberFormatInfo()); + + var completionFunction = this._kernel.CreateFunctionFromPrompt( + this._promptOptions.SystemAudienceExtraction, + this.CreateIntentCompletionSettings(), + functionName: "SystemAudienceExtraction", + description: "Extract audience"); + + var result = await completionFunction.InvokeAsync(this._kernel, audienceContext, cancellationToken); + + // Get token usage from ChatCompletion result and add to original context + string? tokenUsage = TokenUtils.GetFunctionTokenUsage(result, this._logger); + if (tokenUsage is not null) + { + context[TokenUtils.GetFunctionKey("SystemAudienceExtraction")] = tokenUsage; + } + else + { + this._logger.LogError("Unable to determine token usage for audienceExtraction"); + } + + return $"List of participants: {result}"; + } + + /// + /// Extract user intent from the conversation history. + /// + /// Kernel context. + /// The cancellation token. + private async Task GetUserIntentAsync(KernelArguments context, CancellationToken cancellationToken) + { + // Clone the context to avoid modifying the original context variables + KernelArguments intentContext = new(context); + + int tokenBudget = + this._promptOptions.CompletionTokenLimit - + this._promptOptions.ResponseTokenLimit - + TokenUtils.TokenCount(string.Join("\n", new string[] + { + this._promptOptions.SystemPersona, + this._promptOptions.SystemIntent, + this._promptOptions.SystemIntentContinuation + }) + ); + + intentContext["tokenLimit"] = tokenBudget.ToString(new NumberFormatInfo()); + intentContext["knowledgeCutoff"] = this._promptOptions.KnowledgeCutoffDate; + + var completionFunction = this._kernel.CreateFunctionFromPrompt( + this._promptOptions.SystemIntentExtraction, + this.CreateIntentCompletionSettings(), + functionName: "UserIntentExtraction", + description: "Extract user intent"); + + var result = await completionFunction.InvokeAsync(this._kernel, intentContext, cancellationToken); + + // Get token usage from ChatCompletion result and add to original context + string? tokenUsage = TokenUtils.GetFunctionTokenUsage(result, this._logger); + if (tokenUsage is not null) + { + context[TokenUtils.GetFunctionKey("SystemIntentExtraction")] = tokenUsage; + } + else + { + this._logger.LogError("Unable to determine token usage for userIntentExtraction"); + } + + return $"User intent: {result}"; + } + + /// + /// Save a new message to the chat history. + /// + /// The message + /// The user ID + /// + /// The chat ID + /// Type of the message + /// The cancellation token. + private async Task SaveNewMessageAsync(string message, string userId, string userName, string chatId, string type, CancellationToken cancellationToken) + { + // Make sure the chat exists. + if (!await this._chatSessionRepository.TryFindByIdAsync(chatId)) + { + throw new ArgumentException("Chat session does not exist."); + } + + var chatMessage = new CopilotChatMessage( + userId, + userName, + chatId, + message, + string.Empty, + null, + CopilotChatMessage.AuthorRoles.User, + // Default to a standard message if the `type` is not recognized + Enum.TryParse(type, out CopilotChatMessage.ChatMessageType typeAsEnum) && Enum.IsDefined(typeof(CopilotChatMessage.ChatMessageType), typeAsEnum) + ? typeAsEnum + : CopilotChatMessage.ChatMessageType.Message); + + await this._chatMessageRepository.CreateAsync(chatMessage); + return chatMessage; + } + + /// + /// Save a new response to the chat history. + /// + /// Response from the chat. + /// Prompt used to generate the response. + /// The chat ID + /// The user ID + /// The cancellation token. + /// Total token usage of response completion + /// Citations for the message + /// The created chat message. + private async Task SaveNewResponseAsync( + string response, + string prompt, + string chatId, + string userId, + CancellationToken cancellationToken, + Dictionary? tokenUsage = null, + IEnumerable? citations = null + ) + { + // Make sure the chat exists. + if (!await this._chatSessionRepository.TryFindByIdAsync(chatId)) + { + throw new ArgumentException("Chat session does not exist."); + } + + // Save message to chat history + var chatMessage = await this.CreateBotMessageOnClient( + chatId, + userId, + prompt, + response, + cancellationToken, + citations, + tokenUsage + ); + await this._chatMessageRepository.UpsertAsync(chatMessage); + + return chatMessage; + } + + /// + /// Updates previously saved response in the chat history. + /// + /// Updated response from the chat. + /// The chat message ID. + /// The chat ID that's used as the partition Id. + /// The cancellation token. + private async Task UpdateChatMessageContentAsync(string updatedResponse, string messageId, string chatId, CancellationToken cancellationToken) + { + CopilotChatMessage? chatMessage = null; + if (!await this._chatMessageRepository.TryFindByIdAsync(messageId, chatId, callback: v => chatMessage = v)) + { + throw new ArgumentException($"Chat message {messageId} does not exist."); + } + + chatMessage!.Content = updatedResponse; + await this._chatMessageRepository.UpsertAsync(chatMessage); + } + + /// + /// Create `OpenAIPromptExecutionSettings` for chat response. Parameters are read from the PromptSettings class. + /// + private OpenAIPromptExecutionSettings CreateChatRequestSettings() + { + return new OpenAIPromptExecutionSettings + { + MaxTokens = this._promptOptions.ResponseTokenLimit, + Temperature = this._promptOptions.ResponseTemperature, + TopP = this._promptOptions.ResponseTopP, + FrequencyPenalty = this._promptOptions.ResponseFrequencyPenalty, + PresencePenalty = this._promptOptions.ResponsePresencePenalty, + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + }; + } + + /// + /// Create `OpenAIPromptExecutionSettings` for intent response. Parameters are read from the PromptSettings class. + /// + private OpenAIPromptExecutionSettings CreateIntentCompletionSettings() + { + return new OpenAIPromptExecutionSettings + { + MaxTokens = this._promptOptions.ResponseTokenLimit, + Temperature = this._promptOptions.IntentTemperature, + TopP = this._promptOptions.IntentTopP, + FrequencyPenalty = this._promptOptions.IntentFrequencyPenalty, + PresencePenalty = this._promptOptions.IntentPresencePenalty, + StopSequences = new string[] { "] bot:" } + }; + } + + /// + /// Calculate the maximum number of tokens that can be sent in a request + /// + private int GetMaxRequestTokenBudget() + { + // OpenAI inserts a message under the hood: + // "content": "Assistant is a large language model.","role": "system" + // This burns just under 20 tokens which need to be accounted for. + const int ExtraOpenAiMessageTokens = 20; + + return this._promptOptions.CompletionTokenLimit // Total token limit + - ExtraOpenAiMessageTokens + - this._promptOptions.ResponseTokenLimit; // Token count reserved for model to generate a response + } + + /// + /// Gets token usage totals for each semantic function if not undefined. + /// + /// Context maintained during response generation. + /// String representing bot response. If null, response is still being generated or was hardcoded. + /// Dictionary containing function to token usage mapping for each total that's defined. + private Dictionary GetTokenUsages(KernelArguments kernelArguments, string? content = null) + { + var tokenUsageDict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Total token usage of each semantic function + foreach (string function in TokenUtils.semanticFunctions.Values) + { + if (kernelArguments.TryGetValue($"{function}TokenUsage", out object? tokenUsage)) + { + if (tokenUsage is string tokenUsageString) + { + tokenUsageDict.Add(function, int.Parse(tokenUsageString, CultureInfo.InvariantCulture)); + } + } + } + + if (content != null) + { + tokenUsageDict.Add(TokenUtils.semanticFunctions["SystemCompletion"]!, TokenUtils.TokenCount(content)); + } + + return tokenUsageDict; + } + + /// + /// Stream the response to the client. + /// + /// The chat ID + /// The user ID + /// Prompt used to generate the response + /// The cancellation token. + /// Citations for the message + /// The created chat message + private async Task StreamResponseToClientAsync( + string chatId, + string userId, + BotResponsePrompt prompt, + CancellationToken cancellationToken, + IEnumerable? citations = null) + { + // Create the stream + var chatCompletion = this._kernel.GetRequiredService(); + var stream = + chatCompletion.GetStreamingChatMessageContentsAsync( + prompt.MetaPromptTemplate, + this.CreateChatRequestSettings(), + this._kernel, + cancellationToken); + + // Create message on client + var chatMessage = await this.CreateBotMessageOnClient( + chatId, + userId, + JsonSerializer.Serialize(prompt), + string.Empty, + cancellationToken, + citations + ); + + // Stream the message to the client + await foreach (var contentPiece in stream) + { + chatMessage.Content += contentPiece; + await this.UpdateMessageOnClient(chatMessage, cancellationToken); + } + + return chatMessage; + } + + /// + /// Create an empty message on the client to begin the response. + /// + /// The chat ID + /// The user ID + /// Prompt used to generate the message + /// Content of the message + /// The cancellation token. + /// Citations for the message + /// Total token usage of response completion + /// The created chat message + private async Task CreateBotMessageOnClient( + string chatId, + string userId, + string prompt, + string content, + CancellationToken cancellationToken, + IEnumerable? citations = null, + Dictionary? tokenUsage = null) + { + var chatMessage = CopilotChatMessage.CreateBotResponseMessage(chatId, content, prompt, citations, tokenUsage); + await this._messageRelayHubContext.Clients.Group(chatId).SendAsync("ReceiveMessage", chatId, userId, chatMessage, cancellationToken); + return chatMessage; + } + + /// + /// Update the response on the client. + /// + /// The message + /// The cancellation token. + private async Task UpdateMessageOnClient(CopilotChatMessage message, CancellationToken cancellationToken) + { + await this._messageRelayHubContext.Clients.Group(message.ChatId).SendAsync("ReceiveMessageUpdate", message, cancellationToken); + } + + /// + /// Update the status of the response on the client. + /// + /// The chat ID + /// Current status of the response + /// The cancellation token. + private async Task UpdateBotResponseStatusOnClientAsync(string chatId, string status, CancellationToken cancellationToken) + { + await this._messageRelayHubContext.Clients.Group(chatId).SendAsync("ReceiveBotResponseStatus", chatId, status, cancellationToken); + } + + /// + /// Set the system description in the prompt options. + /// + /// Id of the chat session + /// The cancellation token. + /// Throw if the chat session does not exist. + private async Task SetSystemDescriptionAsync(string chatId, CancellationToken cancellationToken) + { + ChatSession? chatSession = null; + if (!await this._chatSessionRepository.TryFindByIdAsync(chatId, callback: v => chatSession = v)) + { + throw new ArgumentException("Chat session does not exist."); + } + + this._promptOptions.SystemDescription = chatSession!.SafeSystemDescription; + } +} diff --git a/webapi/Plugins/Chat/SemanticChatMemory.cs b/webapi/Plugins/Chat/SemanticChatMemory.cs new file mode 100644 index 0000000..e016845 --- /dev/null +++ b/webapi/Plugins/Chat/SemanticChatMemory.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CopilotChat.WebApi.Plugins.Chat; + +/// +/// A collection of semantic chat memory. +/// +public class SemanticChatMemory +{ + /// + /// The chat memory items. + /// + [JsonPropertyName("items")] + public List Items { get; set; } = new List(); + + /// + /// Create and add a chat memory item. + /// + /// Label for the chat memory item. + /// Details for the chat memory item. + public void AddItem(string label, string details) + { + this.Items.Add(new SemanticChatMemoryItem(label, details)); + } + + /// + /// Serialize the chat memory to a Json string. + /// + /// A Json string representing the chat memory. + public override string ToString() + { + return JsonSerializer.Serialize(this); + } + + /// + /// Create a semantic chat memory from a Json string. + /// + /// Json string to deserialize. + /// A semantic chat memory. + public static SemanticChatMemory FromJson(string json) + { + var result = JsonSerializer.Deserialize(json); + return result ?? throw new ArgumentException("Failed to deserialize chat memory to json."); + } +} diff --git a/webapi/Plugins/Chat/SemanticChatMemoryExtractor.cs b/webapi/Plugins/Chat/SemanticChatMemoryExtractor.cs new file mode 100644 index 0000000..6c7f1d9 --- /dev/null +++ b/webapi/Plugins/Chat/SemanticChatMemoryExtractor.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using CopilotChat.WebApi.Extensions; +using CopilotChat.WebApi.Models.Request; +using CopilotChat.WebApi.Options; +using CopilotChat.WebApi.Plugins.Utils; +using Microsoft.Extensions.Logging; +using Microsoft.KernelMemory; +using Microsoft.SemanticKernel; + +namespace CopilotChat.WebApi.Plugins.Chat; + +/// +/// Helper class to extract and create kernel memory from chat history. +/// +internal static class SemanticChatMemoryExtractor +{ + /// + /// Extract and save kernel memory. + /// + /// The Chat ID. + /// The semantic kernel. + /// The Semantic Kernel context. + /// The prompts options. + /// The logger. + /// The cancellation token. + public static async Task ExtractSemanticChatMemoryAsync( + string chatId, + IKernelMemory memoryClient, + Kernel kernel, + KernelArguments kernelArguments, + PromptsOptions options, + ILogger logger, + CancellationToken cancellationToken) + { + foreach (string memoryType in Enum.GetNames(typeof(SemanticMemoryType))) + { + try + { + if (!options.TryGetMemoryContainerName(memoryType, out var memoryName)) + { + logger.LogInformation("Unable to extract kernel memory for invalid memory type {0}. Continuing...", memoryType); + continue; + } + var semanticMemory = await ExtractCognitiveMemoryAsync(memoryType, memoryName, logger); + foreach (var item in semanticMemory.Items) + { + await CreateMemoryAsync(memoryName, item.ToFormattedString()); + } + } + catch (Exception ex) when (!ex.IsCriticalException()) + { + // Skip kernel memory extraction for this item if it fails. + // We cannot rely on the model to response with perfect Json each time. + logger.LogInformation("Unable to extract kernel memory for {0}: {1}. Continuing...", memoryType, ex.Message); + continue; + } + } + + /// + /// Extracts the semantic chat memory from the chat session. + /// + async Task ExtractCognitiveMemoryAsync(string memoryType, string memoryName, ILogger logger) + { + if (!options.MemoryMap.TryGetValue(memoryName, out var memoryPrompt)) + { + throw new ArgumentException($"Memory name {memoryName} is not supported."); + } + + // Token limit for chat history + var tokenLimit = options.CompletionTokenLimit; + var remainingToken = + tokenLimit - + options.ResponseTokenLimit - + TokenUtils.TokenCount(memoryPrompt); + + var memoryExtractionArguments = new KernelArguments(kernelArguments); + memoryExtractionArguments["tokenLimit"] = remainingToken.ToString(new NumberFormatInfo()); + memoryExtractionArguments["memoryName"] = memoryName; + memoryExtractionArguments["format"] = options.MemoryFormat; + memoryExtractionArguments["knowledgeCutoff"] = options.KnowledgeCutoffDate; + + var completionFunction = kernel.CreateFunctionFromPrompt(memoryPrompt); + var result = await completionFunction.InvokeAsync( + kernel, + memoryExtractionArguments, + cancellationToken); + + // Get token usage from ChatCompletion result and add to context + string? tokenUsage = TokenUtils.GetFunctionTokenUsage(result, logger); + if (tokenUsage is not null) + { + // Since there are multiple memory types, total token usage is calculated by cumulating the token usage of each memory type. + kernelArguments[TokenUtils.GetFunctionKey($"SystemCognitive_{memoryType}")] = tokenUsage; + } + else + { + logger.LogError("Unable to determine token usage for {0}", $"SystemCognitive_{memoryType}"); + } + + SemanticChatMemory memory = SemanticChatMemory.FromJson(result.ToString()); + + return memory; + } + + /// + /// Create a memory item in the memory collection. + /// If there is already a memory item that has a high similarity score with the new item, it will be skipped. + /// + async Task CreateMemoryAsync(string memoryName, string memory) + { + try + { + // Search if there is already a memory item that has a high similarity score with the new item. + var searchResult = + await memoryClient.SearchMemoryAsync( + options.MemoryIndexName, + memory, + options.SemanticMemoryRelevanceUpper, + resultCount: 1, + chatId, + memoryName, + cancellationToken); + + if (searchResult.Results.Count == 0) + { + await memoryClient.StoreMemoryAsync(options.MemoryIndexName, chatId, memoryName, memory, cancellationToken: cancellationToken); + } + } + catch (Exception exception) when (!exception.IsCriticalException()) + { + // A store exception might be thrown if the collection does not exist, depending on the memory store connector. + logger.LogError(exception, "Unexpected failure searching {0}", options.MemoryIndexName); + } + } + } +} diff --git a/webapi/Plugins/Chat/SemanticChatMemoryItem.cs b/webapi/Plugins/Chat/SemanticChatMemoryItem.cs new file mode 100644 index 0000000..67697c9 --- /dev/null +++ b/webapi/Plugins/Chat/SemanticChatMemoryItem.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace CopilotChat.WebApi.Plugins.Chat; + +/// +/// A single entry in the chat memory. +/// +public class SemanticChatMemoryItem +{ + /// + /// Label for the chat memory item. + /// + [JsonPropertyName("label")] + public string Label { get; set; } + + /// + /// Details for the chat memory item. + /// + [JsonPropertyName("details")] + public string Details { get; set; } + + /// + /// Create a new chat memory item. + /// + /// Label of the item. + /// Details of the item. + public SemanticChatMemoryItem(string label, string details) + { + this.Label = label; + this.Details = details; + } + + /// + /// Format the chat memory item as a string. + /// + /// A formatted string representing the item. + public string ToFormattedString() + { + return $"{this.Label}: {this.Details?.Trim()}"; + } +} diff --git a/webapi/Plugins/Chat/SemanticMemoryRetriever.cs b/webapi/Plugins/Chat/SemanticMemoryRetriever.cs new file mode 100644 index 0000000..f237c97 --- /dev/null +++ b/webapi/Plugins/Chat/SemanticMemoryRetriever.cs @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CopilotChat.WebApi.Extensions; +using CopilotChat.WebApi.Models.Storage; +using CopilotChat.WebApi.Options; +using CopilotChat.WebApi.Plugins.Utils; +using CopilotChat.WebApi.Storage; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.KernelMemory; + +namespace CopilotChat.WebApi.Plugins.Chat; + +/// +/// This class provides the functions to query kernel memory. +/// +public class SemanticMemoryRetriever +{ + private readonly PromptsOptions _promptOptions; + + private readonly ChatSessionRepository _chatSessionRepository; + + private readonly IKernelMemory _memoryClient; + + private readonly List _memoryNames; + + /// + /// High level logger. + /// + private readonly ILogger _logger; + + /// + /// Create a new instance of SemanticMemoryRetriever. + /// + public SemanticMemoryRetriever( + IOptions promptOptions, + ChatSessionRepository chatSessionRepository, + IKernelMemory memoryClient, + ILogger logger) + { + this._promptOptions = promptOptions.Value; + this._chatSessionRepository = chatSessionRepository; + this._memoryClient = memoryClient; + this._logger = logger; + + this._memoryNames = new List { + this._promptOptions.DocumentMemoryName, + this._promptOptions.LongTermMemoryName, + this._promptOptions.WorkingMemoryName + }; + } + + /// + /// Query relevant memories based on the query. + /// + /// A string containing the relevant memories. + public async Task<(string, IDictionary)> QueryMemoriesAsync( + [Description("Query to match.")] string query, + [Description("Chat ID to query history from")] string chatId, + [Description("Maximum number of tokens")] int tokenLimit) + { + ChatSession? chatSession = null; + if (!await this._chatSessionRepository.TryFindByIdAsync(chatId, callback: v => chatSession = v)) + { + throw new ArgumentException($"Chat session {chatId} not found."); + } + + var remainingToken = tokenLimit; + + // Search for relevant memories. + List<(Citation Citation, Citation.Partition Memory)> relevantMemories = new(); + List tasks = new(); + foreach (var memoryName in this._memoryNames) + { + tasks.Add(SearchMemoryAsync(memoryName)); + } + // Global document memory. + tasks.Add(SearchMemoryAsync(this._promptOptions.DocumentMemoryName, isGlobalMemory: true)); + // Wait for all tasks to complete. + await Task.WhenAll(tasks); + + var builderMemory = new StringBuilder(); + IDictionary citationMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (relevantMemories.Count > 0) + { + (var memoryMap, citationMap) = ProcessMemories(); + FormatMemories(); + FormatSnippets(); + + /// + /// Format long term and working memories. + /// + void FormatMemories() + { + foreach (var memoryName in this._promptOptions.MemoryMap.Keys) + { + if (memoryMap.TryGetValue(memoryName, out var memories)) + { + foreach ((var memoryContent, _) in memories) + { + if (builderMemory.Length == 0) + { + builderMemory.Append("Past memories (format: [memory type]