├── spring-chatgpt-sample-cli ├── src │ └── main │ │ ├── resources │ │ └── application.properties │ │ └── java │ │ └── com │ │ └── microsoft │ │ └── azure │ │ └── spring │ │ └── chatgpt │ │ └── sample │ │ └── cli │ │ ├── CliApplication.java │ │ ├── AzSearchConfig.java │ │ └── Config.java └── pom.xml ├── spring-chatgpt-sample-webapi ├── src │ └── main │ │ ├── resources │ │ ├── application.properties │ │ └── static │ │ │ ├── favicon.png │ │ │ ├── lowpoly.png │ │ │ ├── cross.svg │ │ │ ├── loading.svg │ │ │ ├── README.md │ │ │ ├── chatgpt.svg │ │ │ ├── index.html │ │ │ ├── user.svg │ │ │ ├── main.js │ │ │ └── main.css │ │ └── java │ │ └── com │ │ └── microsoft │ │ └── azure │ │ └── spring │ │ └── chatgpt │ │ └── sample │ │ └── webapi │ │ ├── models │ │ └── ChatCompletionsRequest.java │ │ ├── SpringChatgptSampleApplication.java │ │ ├── controllers │ │ └── ChatController.java │ │ ├── AzSearchConfig.java │ │ └── Config.java └── pom.xml ├── assets ├── chatgpt.png └── resources.png ├── docs └── workflow.png ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CHANGELOG.md ├── env.sh.sample ├── scripts ├── prepdocs.sh └── prepdocs.ps1 ├── infra ├── main.parameters.json ├── modules │ ├── storage │ │ └── storage.bicep │ ├── cognitive │ │ └── cognitive.bicep │ └── springapps │ │ └── springapps.bicep ├── main.bicep └── abbreviations.json ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── spring-chatgpt-sample-common ├── src │ └── main │ │ └── java │ │ └── com │ │ └── microsoft │ │ └── azure │ │ └── spring │ │ └── chatgpt │ │ └── sample │ │ └── common │ │ ├── vectorstore │ │ ├── DocEntry.java │ │ ├── VectorStore.java │ │ ├── EmbeddingMath.java │ │ ├── AzureCognitiveSearchVectorStore.java │ │ └── SimpleMemoryVectorStore.java │ │ ├── prompt │ │ └── PromptTemplate.java │ │ ├── AzureOpenAIClient.java │ │ ├── reader │ │ └── SimpleFolderReader.java │ │ ├── DocumentIndexPlanner.java │ │ ├── ChatPlanner.java │ │ └── TextSplitter.java └── pom.xml ├── .gitignore ├── azure.yaml ├── LICENSE.md ├── pom.xml ├── CONTRIBUTING.md ├── mvnw.cmd ├── checkstyle.xml ├── mvnw └── README.md /spring-chatgpt-sample-cli/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/chatgpt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/spring-chatgpt-sample/HEAD/assets/chatgpt.png -------------------------------------------------------------------------------- /docs/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/spring-chatgpt-sample/HEAD/docs/workflow.png -------------------------------------------------------------------------------- /assets/resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/spring-chatgpt-sample/HEAD/assets/resources.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/spring-chatgpt-sample/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/spring-chatgpt-sample/HEAD/spring-chatgpt-sample-webapi/src/main/resources/static/favicon.png -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/static/lowpoly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/spring-chatgpt-sample/HEAD/spring-chatgpt-sample-webapi/src/main/resources/static/lowpoly.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /env.sh.sample: -------------------------------------------------------------------------------- 1 | export AZURE_OPENAI_ENDPOINT= 2 | export AZURE_OPENAI_APIKEY= 3 | export AZURE_OPENAI_CHATDEPLOYMENTID=gpt-35-turbo 4 | export AZURE_OPENAI_EMBEDDINGDEPLOYMENTID=text-embedding-ada-002 5 | export VECTORSTORE_FILE=doc_store.json 6 | export VECTORSTORE_TYPE=memory -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/java/com/microsoft/azure/spring/chatgpt/sample/webapi/models/ChatCompletionsRequest.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.webapi.models; 2 | 3 | import com.azure.ai.openai.models.ChatMessage; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | @Data 9 | public class ChatCompletionsRequest { 10 | private List messages; 11 | } 12 | -------------------------------------------------------------------------------- /scripts/prepdocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | FILE="doc_store.json" 4 | if [ -e $FILE ] 5 | then 6 | echo "File $FILE already exists." 7 | else 8 | curl -O https://asawikigpt.blob.core.windows.net/demo/doc_store.json 9 | echo "The file $FILE has been downloaded." 10 | fi 11 | 12 | az storage file upload -s vectorstore --source $FILE --account-name $STORAGE_ACCOUNT_NAME --subscription $AZURE_SUBSCRIPTION_ID --account-key $STORAGE_ACCOUNT_KEY 13 | 14 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/static/cross.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "environmentName": { 6 | "value": "${AZURE_ENV_NAME}" 7 | }, 8 | "location": { 9 | "value": "${AZURE_LOCATION}" 10 | }, 11 | "relativePath": { 12 | "value": "${SERVICE_SPRING_CHATGPT_SAMPLE_WEBAPI_RELATIVE_PATH=}" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/vectorstore/DocEntry.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common.vectorstore; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | import lombok.extern.jackson.Jacksonized; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | @Builder 11 | @Jacksonized 12 | public class DocEntry { 13 | private final String id; 14 | 15 | private final String hash; 16 | 17 | private final String text; 18 | 19 | private final List embedding; 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | env.sh 3 | target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/**/target/ 6 | !**/src/test/**/target/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | 23 | ### NetBeans ### 24 | /nbproject/private/ 25 | /nbbuild/ 26 | /dist/ 27 | /nbdist/ 28 | /.nb-gradle/ 29 | build/ 30 | !**/src/main/**/build/ 31 | !**/src/test/**/build/ 32 | 33 | ### VS Code ### 34 | .vscode/ 35 | 36 | ### AZD 37 | .azure -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/vectorstore/VectorStore.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common.vectorstore; 2 | 3 | import java.util.List; 4 | 5 | public interface VectorStore { 6 | void saveDocument(DocEntry doc); 7 | 8 | DocEntry getDocument(String key); 9 | 10 | void removeDocument(String key); 11 | 12 | List searchTopKNearest(List embedding, int k); 13 | 14 | List searchTopKNearest(List embedding, int k, double cutOff); 15 | 16 | void persist(); 17 | } 18 | -------------------------------------------------------------------------------- /scripts/prepdocs.ps1: -------------------------------------------------------------------------------- 1 | $file = 'doc_store.json' 2 | 3 | #If the file does not exist, download it. 4 | if (-not(Test-Path -Path $file -PathType Leaf)) { 5 | try { 6 | Invoke-WebRequest -Uri https://asawikigpt.blob.core.windows.net/demo/doc_store.json -OutFile $file 7 | Write-Host "The file [$file] has been downloaded." 8 | } 9 | catch { 10 | throw $_.Exception.Message 11 | } 12 | } 13 | # If the file already exists, show the message and do nothing. 14 | else { 15 | Write-Host "The file [$file] already exists." 16 | } 17 | az storage file upload -s vectorstore --source $file --account-name $Env:STORAGE_ACCOUNT_NAME --subscription $Env:AZURE_SUBSCRIPTION_ID --account-key $Env:STORAGE_ACCOUNT_KEY 18 | 19 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/prompt/PromptTemplate.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common.prompt; 2 | 3 | import java.util.List; 4 | 5 | public class PromptTemplate { 6 | 7 | private static final String template = """ 8 | Context information is below. 9 | --------------------- 10 | %s 11 | --------------------- 12 | Given the context information and not prior knowledge, answer the question: %s 13 | """; 14 | 15 | public static String formatWithContext(List context, String question) { 16 | String merged = String.join("\n", context); 17 | return String.format(template, merged, question); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/static/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /infra/modules/storage/storage.bicep: -------------------------------------------------------------------------------- 1 | param storageAccountName string 2 | param fileShareName string 3 | param location string = resourceGroup().location 4 | param tags object 5 | param utcValue string = utcNow() 6 | param filename string = '../../../doc_store.json' 7 | 8 | resource storage 'Microsoft.Storage/storageAccounts@2022-09-01' = { 9 | name: storageAccountName 10 | location: location 11 | kind: 'StorageV2' 12 | tags: tags 13 | sku: { 14 | name: 'Standard_LRS' 15 | } 16 | properties: { 17 | largeFileSharesState: 'Enabled' 18 | } 19 | } 20 | 21 | resource fileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2022-09-01' = { 22 | name: '${storage.name}/default/${fileShareName}' 23 | properties: { 24 | shareQuota: 1024 25 | enabledProtocols: 'SMB' 26 | } 27 | } 28 | 29 | output storageAccountKey string = storage.listKeys().keys[0].value -------------------------------------------------------------------------------- /infra/modules/cognitive/cognitive.bicep: -------------------------------------------------------------------------------- 1 | param accountName string 2 | param tags object 3 | param location string = resourceGroup().location 4 | param deployments array = [] 5 | 6 | resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { 7 | name: accountName 8 | location: location 9 | tags: tags 10 | kind: 'OpenAI' 11 | sku: { 12 | name: 'S0' 13 | } 14 | properties: { 15 | customSubDomainName: accountName 16 | } 17 | } 18 | 19 | @batchSize(1) 20 | resource embedding 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { 21 | parent: account 22 | name: deployment.name 23 | properties: { 24 | model: deployment.model 25 | } 26 | sku: { 27 | name: 'Standard' 28 | capacity: deployment.capacity 29 | } 30 | }] 31 | 32 | output endpoint string = account.properties.endpoint 33 | output key string = account.listKeys().key1 -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/static/README.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | 1. Open `main.js` file with an editor 4 | 1. `CONTEXT_MESSAGE_COUNT` is the number of the latest messages to send for context 5 | 1. `DEFAULT_GREETING_MESSAGE` is the default message for a new chat 6 | 1. `API_URL` is the API endpoint for OpenAI 7 | 1. `API_HEADER` is an optional header for an API request 8 | 9 | # Quick start 10 | 11 | 1. Go to the root folder of the `index.html` 12 | 1. Run `python -m http.server` 13 | 1. Open `http://localhost:8000` in the browser 14 | 15 | # Data on local machine 16 | 17 | All chat history is saved in `LocalStorage.chatHistory` 18 | 19 | # External libs 20 | 21 | * VueJS 22 | * Marked 23 | 24 | To host the external libs on your server instead of using a CDN, download the libs and upload to your server. See external lib references in `index.html` file. Do not forget to update the reference `src` to your server path as well. -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: spring-chatgpt-sample 4 | metadata: 5 | template: spring-chatgpt-sample@0.0.1-beta 6 | hooks: 7 | postprovision: 8 | posix: 9 | shell: sh 10 | interactive: true 11 | run: ./scripts/prepdocs.sh 12 | windows: 13 | shell: pwsh 14 | interactive: true 15 | run: ./scripts/prepdocs.ps1 16 | services: 17 | spring-chatgpt-sample-webapi: 18 | project: ./spring-chatgpt-sample-webapi/ 19 | host: springapp 20 | language: java 21 | spring: 22 | deploymentName: default 23 | hooks: 24 | prepackage: 25 | posix: 26 | shell: sh 27 | run: cd ..;./mvnw clean install -N;cd spring-chatgpt-sample-common;../mvnw clean install 28 | windows: 29 | shell: pwsh 30 | run: cd ..;./mvnw.cmd clean install -N;cd spring-chatgpt-sample-common;../mvnw.cmd clean install 31 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/java/com/microsoft/azure/spring/chatgpt/sample/webapi/SpringChatgptSampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.webapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 8 | 9 | @SpringBootApplication 10 | public class SpringChatgptSampleApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(SpringChatgptSampleApplication.class, args); 14 | } 15 | 16 | @Bean 17 | public WebMvcConfigurer corsConfigurer() { 18 | return new WebMvcConfigurer() { 19 | @Override 20 | public void addCorsMappings(CorsRegistry registry) { 21 | registry.addMapping("/**"); 22 | } 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/java/com/microsoft/azure/spring/chatgpt/sample/webapi/controllers/ChatController.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.webapi.controllers; 2 | 3 | 4 | import com.azure.ai.openai.models.ChatCompletions; 5 | import com.microsoft.azure.spring.chatgpt.sample.common.ChatPlanner; 6 | import com.microsoft.azure.spring.chatgpt.sample.webapi.models.ChatCompletionsRequest; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | @RestController 14 | @RequestMapping("/chat") 15 | @RequiredArgsConstructor 16 | public class ChatController { 17 | 18 | private final ChatPlanner planner; 19 | 20 | @PostMapping("/completions") 21 | public ChatCompletions chatCompletion(@RequestBody ChatCompletionsRequest request) { 22 | return planner.chat(request.getMessages()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /spring-chatgpt-sample-cli/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.microsoft.azure 7 | spring-chatgpt-sample 8 | 0.0.1-SNAPSHOT 9 | ../pom.xml 10 | 11 | spring-chatgpt-sample-cli 12 | 0.0.1-SNAPSHOT 13 | spring-chatgpt-sample-cli 14 | Demo project for Spring Boot 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter 20 | 21 | 22 | com.azure 23 | azure-ai-openai 24 | 25 | 26 | com.microsoft.azure 27 | spring-chatgpt-sample-common 28 | 0.0.1-SNAPSHOT 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-cli/src/main/java/com/microsoft/azure/spring/chatgpt/sample/cli/CliApplication.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.cli; 2 | 3 | import com.microsoft.azure.spring.chatgpt.sample.common.DocumentIndexPlanner; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.boot.ApplicationArguments; 6 | import org.springframework.boot.ApplicationRunner; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | 10 | import java.io.IOException; 11 | 12 | @SpringBootApplication 13 | @RequiredArgsConstructor 14 | public class CliApplication implements ApplicationRunner { 15 | 16 | private final DocumentIndexPlanner indexPlanner; 17 | 18 | public static void main(String[] args) { 19 | SpringApplication.run(CliApplication.class, args); 20 | } 21 | 22 | @Override 23 | public void run(ApplicationArguments args) throws IOException { 24 | var from = args.getOptionValues("from"); 25 | if (from == null || from.size() != 1) { 26 | System.err.println("argument --from is required."); 27 | System.exit(-1); 28 | } 29 | indexPlanner.buildFromFolder(from.get(0)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.microsoft.azure 7 | spring-chatgpt-sample 8 | 0.0.1-SNAPSHOT 9 | ../pom.xml 10 | 11 | spring-chatgpt-sample-webapi 12 | 0.0.1-SNAPSHOT 13 | spring-chatgpt-sample-webapi 14 | Demo project for Spring Boot 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-web 20 | 21 | 22 | com.microsoft.azure 23 | spring-chatgpt-sample-common 24 | 0.0.1-SNAPSHOT 25 | 26 | 27 | 28 | com.azure 29 | azure-ai-openai 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | test 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/vectorstore/EmbeddingMath.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common.vectorstore; 2 | 3 | import java.util.List; 4 | 5 | public class EmbeddingMath { 6 | public static double cosineSimilarity(List extends Number> vectorX, List extends Number> vectorY) { 7 | if (vectorX.size() != vectorY.size()) { 8 | throw new IllegalArgumentException("Vectors lengths must be equal"); 9 | } 10 | 11 | double dotProduct = dotProduct(vectorX, vectorY); 12 | double normX = norm(vectorX); 13 | double normY = norm(vectorY); 14 | 15 | if (normX == 0 || normY == 0) { 16 | throw new IllegalArgumentException("Vectors cannot have zero norm"); 17 | } 18 | 19 | return dotProduct / (Math.sqrt(normX) * Math.sqrt(normY)); 20 | } 21 | 22 | public static double dotProduct(List extends Number> vectorX, List extends Number> vectorY) { 23 | if (vectorX.size() != vectorY.size()) { 24 | throw new IllegalArgumentException("Vectors lengths must be equal"); 25 | } 26 | 27 | double result = 0; 28 | for (int i = 0; i < vectorX.size(); ++i) { 29 | result += vectorX.get(i).doubleValue() * vectorY.get(i).doubleValue(); 30 | } 31 | 32 | return result; 33 | } 34 | 35 | public static double norm(List extends Number> vector) { 36 | return dotProduct(vector, vector); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-cli/src/main/java/com/microsoft/azure/spring/chatgpt/sample/cli/AzSearchConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.cli; 2 | 3 | import com.azure.core.credential.AzureKeyCredential; 4 | import com.azure.search.documents.SearchClient; 5 | import com.azure.search.documents.SearchClientBuilder; 6 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.AzureCognitiveSearchVectorStore; 7 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.VectorStore; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Configuration 14 | @ConditionalOnProperty(name = "vector-store.type", havingValue = "azure-search") 15 | public class AzSearchConfig { 16 | 17 | @Value("${azure.cognitive-search.endpoint}") 18 | private String acsEndpoint; 19 | 20 | @Value("${azure.cognitive-search.api-key}") 21 | private String acsApiKey; 22 | 23 | @Value("${azure.cognitive-search.index}") 24 | private String acsIndexName; 25 | 26 | @Bean 27 | public VectorStore vectorStore() { 28 | final SearchClient searchClient = new SearchClientBuilder() 29 | .endpoint(acsEndpoint) 30 | .credential(new AzureKeyCredential(acsApiKey)) 31 | .indexName(acsIndexName) 32 | .buildClient(); 33 | return new AzureCognitiveSearchVectorStore(searchClient); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/java/com/microsoft/azure/spring/chatgpt/sample/webapi/AzSearchConfig.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.webapi; 2 | 3 | import com.azure.core.credential.AzureKeyCredential; 4 | import com.azure.search.documents.SearchClient; 5 | import com.azure.search.documents.SearchClientBuilder; 6 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.AzureCognitiveSearchVectorStore; 7 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.VectorStore; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Configuration 14 | @ConditionalOnProperty(name = "vector-store.type", havingValue = "azure-search") 15 | public class AzSearchConfig { 16 | 17 | @Value("${azure.cognitive-search.endpoint}") 18 | private String acsEndpoint; 19 | 20 | @Value("${azure.cognitive-search.api-key}") 21 | private String acsApiKey; 22 | 23 | @Value("${azure.cognitive-search.index}") 24 | private String acsIndexName; 25 | 26 | @Bean 27 | public VectorStore vectorStore() { 28 | final SearchClient searchClient = new SearchClientBuilder() 29 | .endpoint(acsEndpoint) 30 | .credential(new AzureKeyCredential(acsApiKey)) 31 | .indexName(acsIndexName) 32 | .buildClient(); 33 | return new AzureCognitiveSearchVectorStore(searchClient); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/AzureOpenAIClient.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common; 2 | 3 | import com.azure.ai.openai.OpenAIClient; 4 | import com.azure.ai.openai.models.ChatCompletions; 5 | import com.azure.ai.openai.models.ChatCompletionsOptions; 6 | import com.azure.ai.openai.models.ChatMessage; 7 | import com.azure.ai.openai.models.Embeddings; 8 | import com.azure.ai.openai.models.EmbeddingsOptions; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import java.util.List; 13 | 14 | @RequiredArgsConstructor 15 | @Slf4j 16 | public class AzureOpenAIClient { 17 | 18 | private static final double TEMPERATURE = 0.7; 19 | 20 | private final OpenAIClient client; 21 | 22 | private final String embeddingDeploymentId; 23 | 24 | private final String chatDeploymentId; 25 | 26 | public Embeddings getEmbeddings(List texts) { 27 | var response = client.getEmbeddings(embeddingDeploymentId, 28 | new EmbeddingsOptions(texts)); 29 | log.info("Finished an embedding call with {} tokens.", response.getUsage().getTotalTokens()); 30 | return response; 31 | } 32 | 33 | public ChatCompletions getChatCompletions(List messages) { 34 | var chatCompletionsOptions = new ChatCompletionsOptions(messages) 35 | .setTemperature(TEMPERATURE); 36 | var response = client.getChatCompletions(chatDeploymentId, chatCompletionsOptions); 37 | log.info("Finished a chat completion call with {} tokens", response.getUsage().getTotalTokens()); 38 | return response; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/reader/SimpleFolderReader.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common.reader; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.nio.file.Paths; 8 | import java.util.List; 9 | import java.util.function.BiFunction; 10 | 11 | @RequiredArgsConstructor 12 | public class SimpleFolderReader { 13 | 14 | private final String from; 15 | 16 | private final List allowedExts = List.of("txt", "md"); 17 | 18 | public void run(BiFunction handler) throws IOException { 19 | Files.walk(Paths.get(from)) 20 | .filter(Files::isRegularFile) 21 | .forEach(file -> { 22 | String fileName = file.getFileName().toString(); 23 | String ext = getFileExtension(fileName); 24 | if (allowedExts.contains(ext)) { 25 | try { 26 | String content = Files.readString(file); 27 | handler.apply(fileName, content); 28 | } catch (IOException e) { 29 | e.printStackTrace(); 30 | } 31 | } 32 | }); 33 | } 34 | 35 | private static String getFileExtension(String fileName) { 36 | int dotIndex = fileName.lastIndexOf('.'); 37 | 38 | if (dotIndex > 0 && dotIndex < fileName.length() - 1) { 39 | return fileName.substring(dotIndex + 1); 40 | } else { 41 | return ""; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.microsoft.azure 8 | spring-chatgpt-sample 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | spring-chatgpt-sample-common 13 | 14 | 15 | 16 | com.azure 17 | azure-ai-openai 18 | 19 | 20 | com.knuddels 21 | jtokkit 22 | 23 | 24 | com.azure 25 | azure-search-documents 26 | 27 | 28 | 29 | com.azure 30 | azure-core-serializer-json-jackson 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-maven-plugin 41 | 42 | true 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-cli/src/main/java/com/microsoft/azure/spring/chatgpt/sample/cli/Config.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.cli; 2 | 3 | import com.azure.ai.openai.OpenAIClientBuilder; 4 | import com.azure.core.credential.AzureKeyCredential; 5 | import com.microsoft.azure.spring.chatgpt.sample.common.AzureOpenAIClient; 6 | import com.microsoft.azure.spring.chatgpt.sample.common.DocumentIndexPlanner; 7 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.SimpleMemoryVectorStore; 8 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.VectorStore; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | 14 | @Configuration 15 | public class Config { 16 | 17 | @Value("${azure.openai.embedding-deployment-id}") 18 | private String embeddingDeploymentId; 19 | 20 | @Value("${azure.openai.endpoint}") 21 | private String openAiEndpoint; 22 | 23 | @Value("${azure.openai.api-key}") 24 | private String openAiApiKey; 25 | 26 | @Value("${vector-store.file:}") 27 | private String vectorJsonFile; 28 | 29 | 30 | @Bean 31 | public DocumentIndexPlanner planner(AzureOpenAIClient openAIClient, VectorStore vectorStore) { 32 | return new DocumentIndexPlanner(openAIClient, vectorStore); 33 | } 34 | 35 | @Bean 36 | public AzureOpenAIClient azureOpenAIClient() { 37 | var innerClient = new OpenAIClientBuilder() 38 | .endpoint(openAiEndpoint) 39 | .credential(new AzureKeyCredential(openAiApiKey)) 40 | .buildClient(); 41 | return new AzureOpenAIClient(innerClient, embeddingDeploymentId, null); 42 | } 43 | 44 | @Bean 45 | @ConditionalOnProperty(name = "vector-store.type", havingValue = "memory", matchIfMissing = true) 46 | public VectorStore vectorStore() { 47 | return new SimpleMemoryVectorStore(vectorJsonFile); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/DocumentIndexPlanner.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common; 2 | 3 | import com.microsoft.azure.spring.chatgpt.sample.common.reader.SimpleFolderReader; 4 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.DocEntry; 5 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.VectorStore; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.io.IOException; 10 | import java.util.List; 11 | import java.util.UUID; 12 | 13 | @RequiredArgsConstructor 14 | @Slf4j 15 | public class DocumentIndexPlanner { 16 | 17 | private final AzureOpenAIClient client; 18 | 19 | private final VectorStore vectorStore; 20 | 21 | public void buildFromFolder(String folderPath) throws IOException { 22 | if (folderPath == null) { 23 | throw new IllegalArgumentException("folderPath shouldn't be empty."); 24 | } 25 | 26 | SimpleFolderReader reader = new SimpleFolderReader(folderPath); 27 | TextSplitter splitter = new TextSplitter(); 28 | reader.run((fileName, content) -> { 29 | log.info("String to process {}...", fileName); 30 | var textChunks = splitter.split(content); 31 | for (var chunk: textChunks) { 32 | var response = client.getEmbeddings(List.of(chunk)); 33 | var embedding = response.getData().get(0).getEmbedding().stream().map(Double::floatValue).toList(); 34 | String key = UUID.randomUUID().toString(); 35 | vectorStore.saveDocument(DocEntry.builder() 36 | .id(key) 37 | .hash("") 38 | .embedding(embedding) 39 | .text(chunk) 40 | .build()); 41 | } 42 | return null; 43 | }); 44 | 45 | vectorStore.persist(); 46 | log.info("All documents are loaded to the vector store."); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/java/com/microsoft/azure/spring/chatgpt/sample/webapi/Config.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.webapi; 2 | 3 | import com.azure.ai.openai.OpenAIClientBuilder; 4 | import com.azure.core.credential.AzureKeyCredential; 5 | import com.microsoft.azure.spring.chatgpt.sample.common.AzureOpenAIClient; 6 | import com.microsoft.azure.spring.chatgpt.sample.common.ChatPlanner; 7 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.SimpleMemoryVectorStore; 8 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.VectorStore; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | 14 | @Configuration 15 | public class Config { 16 | 17 | 18 | @Value("${azure.openai.embedding-deployment-id}") 19 | private String embeddingDeploymentId; 20 | 21 | @Value("${azure.openai.chat-deployment-id}") 22 | private String chatDeploymentId; 23 | 24 | @Value("${azure.openai.endpoint}") 25 | private String endpoint; 26 | 27 | @Value("${azure.openai.api-key}") 28 | private String apiKey; 29 | 30 | @Value("${vector-store.file}") 31 | private String vectorJsonFile; 32 | 33 | @Bean 34 | public ChatPlanner planner(AzureOpenAIClient openAIClient, VectorStore vectorStore) { 35 | return new ChatPlanner(openAIClient, vectorStore); 36 | } 37 | 38 | @Bean 39 | public AzureOpenAIClient azureOpenAIClient() { 40 | var innerClient = new OpenAIClientBuilder() 41 | .endpoint(endpoint) 42 | .credential(new AzureKeyCredential(apiKey)) 43 | .buildClient(); 44 | return new AzureOpenAIClient(innerClient, embeddingDeploymentId, chatDeploymentId); 45 | } 46 | 47 | @Bean 48 | @ConditionalOnProperty(name = "vector-store.type", havingValue = "memory", matchIfMissing = true) 49 | public VectorStore vectorStore() { 50 | return SimpleMemoryVectorStore.loadFromJsonFile(vectorJsonFile); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/vectorstore/AzureCognitiveSearchVectorStore.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common.vectorstore; 2 | 3 | import com.azure.core.util.Context; 4 | import com.azure.search.documents.SearchClient; 5 | import com.azure.search.documents.models.SearchOptions; 6 | import com.azure.search.documents.models.SearchQueryVector; 7 | import com.azure.search.documents.util.SearchPagedIterable; 8 | 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | public class AzureCognitiveSearchVectorStore implements VectorStore { 13 | 14 | private final SearchClient searchClient; 15 | 16 | public AzureCognitiveSearchVectorStore(SearchClient searchClient) { 17 | this.searchClient = searchClient; 18 | } 19 | 20 | @Override 21 | public void saveDocument(DocEntry doc) { 22 | searchClient.uploadDocuments(List.of(doc)); 23 | } 24 | 25 | @Override 26 | public DocEntry getDocument(String key) { 27 | return searchClient.getDocument(key, DocEntry.class); 28 | } 29 | 30 | @Override 31 | public void removeDocument(String key) { 32 | searchClient.deleteDocuments(List.of(DocEntry.builder().id(key).build())); 33 | } 34 | 35 | @Override 36 | public List searchTopKNearest(List embedding, int k) { 37 | return searchTopKNearest(embedding, k, 0); 38 | } 39 | 40 | @Override 41 | public List searchTopKNearest(List embedding, int k, double cutOff) { 42 | SearchQueryVector searchQueryVector = new SearchQueryVector() 43 | .setValue(embedding) 44 | .setKNearestNeighborsCount(k) 45 | .setFields("embedding"); // the field name from the class DocEntry 46 | 47 | SearchPagedIterable searchResults = searchClient.search(null, 48 | new SearchOptions().setVectors(searchQueryVector), Context.NONE); 49 | return searchResults.stream() 50 | .filter(r -> r.getScore() >= cutOff) 51 | .map(r -> r.getDocument(DocEntry.class)) 52 | .collect(Collectors.toList()); 53 | } 54 | 55 | @Override 56 | public void persist() { 57 | // do nothing 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/ChatPlanner.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common; 2 | 3 | import com.azure.ai.openai.models.ChatCompletions; 4 | import com.azure.ai.openai.models.ChatMessage; 5 | import com.azure.ai.openai.models.ChatRole; 6 | import com.microsoft.azure.spring.chatgpt.sample.common.prompt.PromptTemplate; 7 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.DocEntry; 8 | import com.microsoft.azure.spring.chatgpt.sample.common.vectorstore.VectorStore; 9 | import lombok.RequiredArgsConstructor; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | @RequiredArgsConstructor 15 | public class ChatPlanner { 16 | 17 | private final AzureOpenAIClient client; 18 | 19 | private final VectorStore store; 20 | 21 | public ChatCompletions chat(List messages) { 22 | if (messages == null || messages.isEmpty()) { 23 | throw new IllegalArgumentException("message shouldn't be empty."); 24 | } 25 | 26 | var lastUserMessage = messages.get(messages.size() - 1); 27 | if (lastUserMessage.getRole() != ChatRole.USER) { 28 | throw new IllegalArgumentException("The last message should be in user role."); 29 | } 30 | String question = lastUserMessage.getContent(); 31 | 32 | // step 1. Convert the user's query text to an embedding 33 | var response = client.getEmbeddings(List.of(question)); 34 | var embedding = response.getData().get(0).getEmbedding().stream().map(Double::floatValue).toList(); 35 | 36 | // step 2. Query Top-K nearest text chunks from the vector store 37 | var candidateDocs = store.searchTopKNearest(embedding, 5, 0.4).stream() 38 | .map(DocEntry::getText).toList(); 39 | 40 | // step 3. Populate the prompt template with the chunks 41 | var prompt = PromptTemplate.formatWithContext(candidateDocs, question); 42 | var processedMessages = new ArrayList<>(messages); 43 | processedMessages.set(messages.size() - 1, new ChatMessage(ChatRole.USER, prompt)); 44 | 45 | // step 4. Call to OpenAI chat completion API 46 | var answer = client.getChatCompletions(processedMessages); 47 | return answer; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/static/chatgpt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /infra/modules/springapps/springapps.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param asaInstanceName string 3 | param asaManagedEnvironmentName string 4 | param appName string 5 | param tags object = {} 6 | param relativePath string 7 | param environmentVariables object = {} 8 | param fileShareName string 9 | param storageAccountName string 10 | param storageMountName string 11 | 12 | resource asaManagedEnvironment 'Microsoft.App/managedEnvironments@2022-11-01-preview' = { 13 | name: asaManagedEnvironmentName 14 | location: location 15 | tags: tags 16 | properties: { 17 | workloadProfiles: [ 18 | { 19 | name: 'Consumption' 20 | workloadProfileType: 'Consumption' 21 | } 22 | ] 23 | } 24 | 25 | resource persistentStorage 'storages' = { 26 | name: 'vectorstore' 27 | properties: { 28 | azureFile: { 29 | accessMode: 'ReadOnly' 30 | accountKey: storage.listKeys().keys[0].value 31 | accountName: storageAccountName 32 | shareName: fileShareName 33 | } 34 | } 35 | } 36 | } 37 | 38 | 39 | resource asaInstance 'Microsoft.AppPlatform/Spring@2023-03-01-preview' = { 40 | name: asaInstanceName 41 | location: location 42 | tags: union(tags, { 'azd-service-name': appName }) 43 | sku: { 44 | name: 'S0' 45 | tier: 'StandardGen2' 46 | } 47 | properties: { 48 | managedEnvironmentId: asaManagedEnvironment.id 49 | } 50 | } 51 | 52 | resource asaApp 'Microsoft.AppPlatform/Spring/apps@2023-03-01-preview' = { 53 | name: appName 54 | location: location 55 | parent: asaInstance 56 | properties: { 57 | public: true 58 | activeDeploymentName: 'default' 59 | customPersistentDisks: [ 60 | { 61 | customPersistentDiskProperties: { 62 | mountPath: '/opt/spring-chatgpt-sample' 63 | type: 'AzureFileVolume' 64 | shareName: fileShareName 65 | } 66 | storageId: storageMountName 67 | } 68 | ] 69 | } 70 | } 71 | 72 | resource asaDeployment 'Microsoft.AppPlatform/Spring/apps/deployments@2023-03-01-preview' = { 73 | name: 'default' 74 | parent: asaApp 75 | properties: { 76 | source: { 77 | type: 'Jar' 78 | relativePath: relativePath 79 | runtimeVersion: 'Java_17' 80 | jvmOptions: '-Xms1024m -Xmx2048m' 81 | } 82 | deploymentSettings: { 83 | resourceRequests: { 84 | cpu: '1' 85 | memory: '2Gi' 86 | } 87 | scale: { 88 | maxReplicas: 2 89 | minReplicas: 2 90 | } 91 | environmentVariables: environmentVariables 92 | } 93 | } 94 | } 95 | 96 | resource storage 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { 97 | name: storageAccountName 98 | } 99 | 100 | output name string = asaApp.name 101 | output uri string = 'https://${asaApp.properties.url}' -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Spring ChatGPT Sample 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{ item.title }} 20 | 21 | {{ item.messages.length }} {{ item.messages.length > 1 ? 'messages' : 'message' }} 22 | {{ parseTimestamp(item.timestamp) }} 23 | 24 | 25 | 26 | 27 | New chat 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {{ parseTimestamp(item.timestamp) }} 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Send 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the the environment which is used to generate a short unique hash used in all resources.') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('Primary location for all resources') 10 | param location string 11 | 12 | @description('Relative Path of ASA Jar') 13 | param relativePath string 14 | 15 | var abbrs = loadJsonContent('./abbreviations.json') 16 | var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) 17 | var storageAccountName = '${abbrs.storageStorageAccounts}${resourceToken}' 18 | var fileShareName = 'vectorstore' 19 | var storageMountName = 'vectorstore' 20 | var cognitiveAccountName = '${abbrs.cognitiveServicesAccounts}${resourceToken}' 21 | var asaInstanceName = '${abbrs.springApps}${resourceToken}' 22 | var asaManagedEnvironmentName = '${abbrs.appContainerAppsManagedEnvironment}${resourceToken}' 23 | var appName = 'spring-chatgpt-sample-webapi' 24 | var tags = { 25 | 'azd-env-name': environmentName 26 | 'spring-cloud-azure': 'true' 27 | } 28 | 29 | resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { 30 | name: '${abbrs.resourcesResourceGroups}${environmentName}-${resourceToken}' 31 | location: location 32 | tags: tags 33 | } 34 | 35 | module storage 'modules/storage/storage.bicep' = { 36 | name: '${deployment().name}--storage' 37 | scope: resourceGroup(rg.name) 38 | params: { 39 | location: location 40 | tags: tags 41 | storageAccountName: storageAccountName 42 | fileShareName: fileShareName 43 | } 44 | } 45 | 46 | module cognitive 'modules/cognitive/cognitive.bicep' = { 47 | name: '${deployment().name}--cog' 48 | scope: resourceGroup(rg.name) 49 | params: { 50 | location: location 51 | tags: tags 52 | accountName: cognitiveAccountName 53 | deployments: [ 54 | { 55 | name: 'gpt-35-turbo' 56 | model: { 57 | format: 'OpenAI' 58 | name: 'gpt-35-turbo' 59 | version: '0301' 60 | } 61 | capacity: 30 62 | } 63 | { 64 | name: 'text-embedding-ada-002' 65 | model: { 66 | format: 'OpenAI' 67 | name: 'text-embedding-ada-002' 68 | version: '2' 69 | } 70 | capacity: 30 71 | } 72 | ] 73 | } 74 | } 75 | 76 | module springApps 'modules/springapps/springapps.bicep' = { 77 | name: '${deployment().name}--asa' 78 | scope: resourceGroup(rg.name) 79 | params: { 80 | location: location 81 | appName: appName 82 | tags: tags 83 | asaInstanceName: asaInstanceName 84 | asaManagedEnvironmentName: asaManagedEnvironmentName 85 | relativePath: relativePath 86 | storageAccountName: storageAccountName 87 | fileShareName: fileShareName 88 | storageMountName: storageMountName 89 | environmentVariables: { 90 | AZURE_OPENAI_ENDPOINT: cognitive.outputs.endpoint 91 | AZURE_OPENAI_APIKEY: cognitive.outputs.key 92 | AZURE_OPENAI_CHATDEPLOYMENTID: 'gpt-35-turbo' 93 | AZURE_OPENAI_EMBEDDINGDEPLOYMENTID: 'text-embedding-ada-002' 94 | VECTORSTORE_FILE: '/opt/spring-chatgpt-sample/doc_store.json' 95 | } 96 | } 97 | dependsOn: [ 98 | storage 99 | ] 100 | } 101 | 102 | output STORAGE_ACCOUNT_NAME string = '${storageAccountName}' 103 | output STORAGE_ACCOUNT_KEY string = '${storage.outputs.storageAccountKey}' 104 | output AZURE_RESOURCE_GROUP string = rg.name 105 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.microsoft.azure 6 | spring-chatgpt-sample 7 | 0.0.1-SNAPSHOT 8 | spring-chatgpt-sample 9 | Demo project for Spring Boot 10 | pom 11 | 12 | spring-chatgpt-sample-common 13 | spring-chatgpt-sample-webapi 14 | spring-chatgpt-sample-cli 15 | 16 | 17 | 18 | org.springframework.boot 19 | spring-boot-starter-parent 20 | 3.1.2 21 | 22 | 23 | 24 | 25 | 17 26 | 17 27 | 17 28 | UTF-8 29 | 30 | 31 | 32 | 33 | org.projectlombok 34 | lombok 35 | 1.18.30 36 | true 37 | 38 | 39 | 40 | 41 | 42 | 43 | com.azure 44 | azure-ai-openai 45 | 1.0.0-beta.3 46 | 47 | 48 | com.knuddels 49 | jtokkit 50 | 0.6.1 51 | 52 | 53 | com.azure 54 | azure-search-documents 55 | 11.6.0-beta.8 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-maven-plugin 65 | 66 | 67 | 68 | org.projectlombok 69 | lombok 70 | 71 | 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-checkstyle-plugin 77 | 3.3.0 78 | 79 | checkstyle.xml 80 | 81 | 82 | 83 | validate 84 | validate 85 | 86 | check 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/TextSplitter.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common; 2 | 3 | import com.knuddels.jtokkit.Encodings; 4 | import com.knuddels.jtokkit.api.Encoding; 5 | import com.knuddels.jtokkit.api.EncodingRegistry; 6 | import com.knuddels.jtokkit.api.EncodingType; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class TextSplitter { 12 | private static final int DEFAULT_CHUNK_SIZE = 400; // The target size of each text chunk in tokens 13 | private static final int MIN_CHUNK_SIZE_CHARS = 350; // The minimum size of each text chunk in characters 14 | private static final int MIN_CHUNK_LENGTH_TO_EMBED = 5; // Discard chunks shorter than this 15 | private static final int MAX_NUM_CHUNKS = 10000; // The maximum number of chunks to generate from a text 16 | 17 | private final EncodingRegistry registry = Encodings.newLazyEncodingRegistry(); 18 | 19 | private final Encoding encoding = registry.getEncoding(EncodingType.CL100K_BASE); 20 | 21 | public List split(String text) { 22 | return split(text, DEFAULT_CHUNK_SIZE); 23 | } 24 | 25 | public List split(String text, int chunkSize) { 26 | if (text == null || text.trim().isEmpty()) { 27 | return new ArrayList<>(); 28 | } 29 | 30 | List tokens = getEncodedTokens(text); 31 | List chunks = new ArrayList<>(); 32 | int num_chunks = 0; 33 | while (!tokens.isEmpty() && num_chunks < MAX_NUM_CHUNKS) { 34 | List chunk = tokens.subList(0, Math.min(chunkSize, tokens.size())); 35 | String chunkText = decodeTokens(chunk); 36 | 37 | // Skip the chunk if it is empty or whitespace 38 | if (chunkText.trim().isEmpty()) { 39 | tokens = tokens.subList(chunk.size(), tokens.size()); 40 | continue; 41 | } 42 | 43 | // Find the last period or punctuation mark in the chunk 44 | int lastPunctuation = Math.max( 45 | chunkText.lastIndexOf('.'), 46 | Math.max( 47 | chunkText.lastIndexOf('?'), 48 | Math.max( 49 | chunkText.lastIndexOf('!'), 50 | chunkText.lastIndexOf('\n') 51 | ) 52 | ) 53 | ); 54 | 55 | if (lastPunctuation != -1 && lastPunctuation > MIN_CHUNK_SIZE_CHARS) { 56 | // Truncate the chunk text at the punctuation mark 57 | chunkText = chunkText.substring(0, lastPunctuation + 1); 58 | } 59 | String chunk_text_to_append = chunkText.replace("\n", " ").trim(); 60 | if (chunk_text_to_append.length() > MIN_CHUNK_LENGTH_TO_EMBED) { 61 | chunks.add(chunk_text_to_append); 62 | } 63 | 64 | // Remove the tokens corresponding to the chunk text from the remaining tokens 65 | tokens = tokens.subList(getEncodedTokens(chunkText).size(), tokens.size()); 66 | 67 | num_chunks++; 68 | } 69 | 70 | // Handle the remaining tokens 71 | if (!tokens.isEmpty()) { 72 | String remaining_text = decodeTokens(tokens).replace("\n", " ").trim(); 73 | if (remaining_text.length() > MIN_CHUNK_LENGTH_TO_EMBED) { 74 | chunks.add(remaining_text); 75 | } 76 | } 77 | 78 | return chunks; 79 | } 80 | 81 | private List getEncodedTokens(String text) { 82 | return encoding.encode(text); 83 | } 84 | 85 | private String decodeTokens(List tokens) { 86 | return encoding.decode(tokens); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-common/src/main/java/com/microsoft/azure/spring/chatgpt/sample/common/vectorstore/SimpleMemoryVectorStore.java: -------------------------------------------------------------------------------- 1 | package com.microsoft.azure.spring.chatgpt.sample.common.vectorstore; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | import java.io.File; 9 | import java.io.FileWriter; 10 | import java.io.IOException; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.Comparator; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | 17 | public class SimpleMemoryVectorStore implements VectorStore { 18 | 19 | private final VectorStoreData data; 20 | 21 | @Setter 22 | @Getter 23 | private String persistFilePath; 24 | 25 | public SimpleMemoryVectorStore() { 26 | this.data = new VectorStoreData(); 27 | } 28 | 29 | public SimpleMemoryVectorStore(String persistFilePath) { 30 | if (persistFilePath.isBlank()) { 31 | throw new IllegalArgumentException("persistFilePath shouldn't be empty."); 32 | } 33 | this.data = new VectorStoreData(); 34 | this.persistFilePath = persistFilePath; 35 | } 36 | 37 | SimpleMemoryVectorStore(String persistFilePath, VectorStoreData data) { 38 | if (persistFilePath.isBlank()) { 39 | throw new IllegalArgumentException("persistFilePath shouldn't be empty."); 40 | } 41 | this.persistFilePath = persistFilePath; 42 | this.data = data; 43 | } 44 | 45 | @Override 46 | public void saveDocument(DocEntry doc) { 47 | data.store.put(doc.getId(), doc); 48 | } 49 | 50 | @Override 51 | public DocEntry getDocument(String key) { 52 | return data.store.getOrDefault(key, null); 53 | } 54 | 55 | @Override 56 | public void removeDocument(String key) { 57 | data.store.remove(key); 58 | } 59 | 60 | @Override 61 | public List searchTopKNearest(List embedding, int k) { 62 | return searchTopKNearest(embedding, k, 0); 63 | } 64 | 65 | @Override 66 | public List searchTopKNearest(List embedding, int k, double cutOff) { 67 | var similarities = data.store.values().stream().map(entry -> new Similarity( 68 | entry.getId(), 69 | EmbeddingMath.cosineSimilarity(embedding, entry.getEmbedding()))); 70 | var docs = similarities.filter(s -> s.similarity >= cutOff) 71 | .sorted(Comparator.comparingDouble(s -> s.similarity).reversed()) 72 | .limit(k) 73 | .map(s -> data.store.get(s.key)) 74 | .toList(); 75 | return docs; 76 | } 77 | 78 | public void persist() { 79 | if (persistFilePath.isBlank()) { 80 | throw new IllegalStateException("persistFilePath shouldn't be empty."); 81 | } 82 | var objectWriter = new ObjectMapper().writer().withDefaultPrettyPrinter(); 83 | try (var fileWriter = new FileWriter(persistFilePath, StandardCharsets.UTF_8)) { 84 | objectWriter.writeValue(fileWriter, data); 85 | } catch (IOException e) { 86 | throw new RuntimeException(e); 87 | } 88 | } 89 | 90 | public static SimpleMemoryVectorStore loadFromJsonFile(String filePath) { 91 | var reader = new ObjectMapper().reader(); 92 | try { 93 | var data = reader.readValue(new File(filePath), VectorStoreData.class); 94 | return new SimpleMemoryVectorStore(filePath, data); 95 | } catch (IOException e) { 96 | throw new RuntimeException(e); 97 | } 98 | } 99 | 100 | @AllArgsConstructor 101 | private static class Similarity { 102 | private String key; 103 | private double similarity; 104 | } 105 | 106 | @Setter 107 | @Getter 108 | private static class VectorStoreData { 109 | private Map store = new ConcurrentHashMap<>(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/static/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /infra/abbreviations.json: -------------------------------------------------------------------------------- 1 | { 2 | "analysisServicesServers": "as", 3 | "apiManagementService": "apim-", 4 | "appConfigurationConfigurationStores": "appcs-", 5 | "appManagedEnvironments": "cae-", 6 | "appContainerApps": "ca-", 7 | "appContainerAppsManagedEnvironment": "env-", 8 | "authorizationPolicyDefinitions": "policy-", 9 | "automationAutomationAccounts": "aa-", 10 | "blueprintBlueprints": "bp-", 11 | "blueprintBlueprintsArtifacts": "bpa-", 12 | "cacheRedis": "redis-", 13 | "cdnProfiles": "cdnp-", 14 | "cdnProfilesEndpoints": "cdne-", 15 | "cognitiveServicesAccounts": "cog-", 16 | "cognitiveServicesFormRecognizer": "cog-fr-", 17 | "cognitiveServicesTextAnalytics": "cog-ta-", 18 | "computeAvailabilitySets": "avail-", 19 | "computeCloudServices": "cld-", 20 | "computeDiskEncryptionSets": "des", 21 | "computeDisks": "disk", 22 | "computeDisksOs": "osdisk", 23 | "computeGalleries": "gal", 24 | "computeSnapshots": "snap-", 25 | "computeVirtualMachines": "vm", 26 | "computeVirtualMachineScaleSets": "vmss-", 27 | "containerInstanceContainerGroups": "ci", 28 | "containerRegistryRegistries": "cr", 29 | "containerServiceManagedClusters": "aks-", 30 | "databricksWorkspaces": "dbw-", 31 | "dataFactoryFactories": "adf-", 32 | "dataLakeAnalyticsAccounts": "dla", 33 | "dataLakeStoreAccounts": "dls", 34 | "dataMigrationServices": "dms-", 35 | "dBforMySQLServers": "mysql-", 36 | "dBforPostgreSQLServers": "psql-", 37 | "devicesIotHubs": "iot-", 38 | "devicesProvisioningServices": "provs-", 39 | "devicesProvisioningServicesCertificates": "pcert-", 40 | "documentDBDatabaseAccounts": "cosmos-", 41 | "eventGridDomains": "evgd-", 42 | "eventGridDomainsTopics": "evgt-", 43 | "eventGridEventSubscriptions": "evgs-", 44 | "eventHubNamespaces": "evhns-", 45 | "eventHubNamespacesEventHubs": "evh-", 46 | "hdInsightClustersHadoop": "hadoop-", 47 | "hdInsightClustersHbase": "hbase-", 48 | "hdInsightClustersKafka": "kafka-", 49 | "hdInsightClustersMl": "mls-", 50 | "hdInsightClustersSpark": "spark-", 51 | "hdInsightClustersStorm": "storm-", 52 | "hybridComputeMachines": "arcs-", 53 | "insightsActionGroups": "ag-", 54 | "insightsComponents": "appi-", 55 | "keyVaultVaults": "kv-", 56 | "kubernetesConnectedClusters": "arck", 57 | "kustoClusters": "dec", 58 | "kustoClustersDatabases": "dedb", 59 | "logicIntegrationAccounts": "ia-", 60 | "logicWorkflows": "logic-", 61 | "machineLearningServicesWorkspaces": "mlw-", 62 | "managedIdentityUserAssignedIdentities": "id-", 63 | "managementManagementGroups": "mg-", 64 | "migrateAssessmentProjects": "migr-", 65 | "networkApplicationGateways": "agw-", 66 | "networkApplicationSecurityGroups": "asg-", 67 | "networkAzureFirewalls": "afw-", 68 | "networkBastionHosts": "bas-", 69 | "networkConnections": "con-", 70 | "networkDnsZones": "dnsz-", 71 | "networkExpressRouteCircuits": "erc-", 72 | "networkFirewallPolicies": "afwp-", 73 | "networkFirewallPoliciesWebApplication": "waf", 74 | "networkFirewallPoliciesRuleGroups": "wafrg", 75 | "networkFrontDoors": "fd-", 76 | "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", 77 | "networkLoadBalancersExternal": "lbe-", 78 | "networkLoadBalancersInternal": "lbi-", 79 | "networkLoadBalancersInboundNatRules": "rule-", 80 | "networkLocalNetworkGateways": "lgw-", 81 | "networkNatGateways": "ng-", 82 | "networkNetworkInterfaces": "nic-", 83 | "networkNetworkSecurityGroups": "nsg-", 84 | "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", 85 | "networkNetworkWatchers": "nw-", 86 | "networkPrivateDnsZones": "pdnsz-", 87 | "networkPrivateLinkServices": "pl-", 88 | "networkPublicIPAddresses": "pip-", 89 | "networkPublicIPPrefixes": "ippre-", 90 | "networkRouteFilters": "rf-", 91 | "networkRouteTables": "rt-", 92 | "networkRouteTablesRoutes": "udr-", 93 | "networkTrafficManagerProfiles": "traf-", 94 | "networkVirtualNetworkGateways": "vgw-", 95 | "networkVirtualNetworks": "vnet-", 96 | "networkVirtualNetworksSubnets": "snet-", 97 | "networkVirtualNetworksVirtualNetworkPeerings": "peer-", 98 | "networkVirtualWans": "vwan-", 99 | "networkVpnGateways": "vpng-", 100 | "networkVpnGatewaysVpnConnections": "vcn-", 101 | "networkVpnGatewaysVpnSites": "vst-", 102 | "notificationHubsNamespaces": "ntfns-", 103 | "notificationHubsNamespacesNotificationHubs": "ntf-", 104 | "operationalInsightsWorkspaces": "log-", 105 | "portalDashboards": "dash-", 106 | "powerBIDedicatedCapacities": "pbi-", 107 | "purviewAccounts": "pview-", 108 | "recoveryServicesVaults": "rsv-", 109 | "resourcesResourceGroups": "rg-", 110 | "searchSearchServices": "srch-", 111 | "serviceBusNamespaces": "sb-", 112 | "serviceBusNamespacesQueues": "sbq-", 113 | "serviceBusNamespacesTopics": "sbt-", 114 | "serviceEndPointPolicies": "se-", 115 | "serviceFabricClusters": "sf-", 116 | "signalRServiceSignalR": "sigr", 117 | "springApps": "asa-", 118 | "springAppsServicePlanName": "plan-", 119 | "sqlManagedInstances": "sqlmi-", 120 | "sqlServers": "sql-", 121 | "sqlServersDataWarehouse": "sqldw-", 122 | "sqlServersDatabases": "sqldb-", 123 | "sqlServersDatabasesStretch": "sqlstrdb-", 124 | "storageStorageAccounts": "st", 125 | "storageStorageAccountsVm": "stvm", 126 | "storSimpleManagers": "ssimp", 127 | "streamAnalyticsCluster": "asa-", 128 | "synapseWorkspaces": "syn", 129 | "synapseWorkspacesAnalyticsWorkspaces": "synw", 130 | "synapseWorkspacesSqlPoolsDedicated": "syndp", 131 | "synapseWorkspacesSqlPoolsSpark": "synsp", 132 | "timeSeriesInsightsEnvironments": "tsi-", 133 | "webServerFarms": "plan-", 134 | "webSitesAppService": "app-", 135 | "webSitesAppServiceEnvironment": "ase-", 136 | "webSitesFunctions": "func-", 137 | "webStaticSites": "stapp-" 138 | } -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/static/main.js: -------------------------------------------------------------------------------- 1 | const CONTEXT_MESSAGE_COUNT = 5; 2 | const DEFAULT_GREETING_MESSAGE = "Hello! How can I assist you today?"; 3 | const API_URL = '/chat/completions'; 4 | const API_HEADER = { 5 | "Content-Type": "application/json", 6 | }; 7 | 8 | var app = new Vue({ 9 | el: '#main', 10 | data: { 11 | chatHistory: { 12 | data: JSON.parse(localStorage.getItem("chatHistory")) ?? [], 13 | activeChatId: null, 14 | job: {} 15 | } 16 | }, 17 | created: function () { 18 | if (this.chatHistory.data.length === 0) { 19 | this.newChat(); 20 | } else { 21 | const activeChatId = this.chatHistory.data[this.chatHistory.data.length - 1].id; 22 | this.openChat(activeChatId); 23 | } 24 | }, 25 | methods: { 26 | parseMarkdown: marked.parse, 27 | scrollThreadToBottom: function () { 28 | const chatMessageList = document.getElementById("chat-thread-message-list"); 29 | chatMessageList.scrollTop = chatMessageList.scrollHeight; 30 | }, 31 | scrollHistoryToTop: function () { 32 | const chatHistoryList = document.getElementById("chat-history-list"); 33 | chatHistoryList.scrollTop = 0; 34 | }, 35 | parseTimestamp: function (timestamp) { 36 | const day = new Date(timestamp); 37 | const date = `${day.getMonth()+1}/${day.getDate()}/${day.getFullYear()}`; 38 | const time = day.toLocaleTimeString('en-US', {hour12: true, hour: 'numeric', minute: 'numeric', second: 'numeric'}); 39 | const dateTime = `${date}, ${time}`; 40 | 41 | return dateTime; 42 | }, 43 | openChat: function (id) { 44 | this.chatHistory.activeChatId = id; 45 | 46 | setTimeout(() => { 47 | this.scrollThreadToBottom(); 48 | }, 0); 49 | }, 50 | newChat: function () { 51 | const timestamp = new Date().getTime(); 52 | const newChatHistory = { 53 | id: Math.random().toString(16).slice(2), 54 | title: "New chat", 55 | timestamp: timestamp, 56 | messages: [ 57 | { 58 | sender: "bot", 59 | text: DEFAULT_GREETING_MESSAGE, 60 | timestamp: timestamp 61 | } 62 | ] 63 | } 64 | 65 | this.chatHistory.data.push(newChatHistory); 66 | this.chatHistory.activeChatId = newChatHistory.id; 67 | 68 | this.saveChatHistory(); 69 | 70 | setTimeout(() => { 71 | this.scrollHistoryToTop(); 72 | }, 0); 73 | }, 74 | sendMessage: function () { 75 | const message = document.getElementById("chat-input-box").value.trim(); 76 | 77 | if (message === "") { 78 | return; 79 | } 80 | 81 | document.getElementById("chat-input-box").value = ""; 82 | const activeChat = this.chatHistory.data.find(chat => chat.id === this.chatHistory.activeChatId); 83 | activeChat.messages.push({ 84 | sender: "human", 85 | text: message, 86 | timestamp: new Date().getTime() 87 | }); 88 | 89 | if (activeChat.title === "New chat") { 90 | activeChat.title = message; 91 | } 92 | 93 | this.saveChatHistory(); 94 | this.addJob(activeChat.id); 95 | 96 | setTimeout(() => { 97 | this.scrollThreadToBottom(); 98 | }, 0); 99 | }, 100 | addJob: async function (id) { 101 | this.chatHistory.job[id] = this.chatHistory.job[id] ?? []; 102 | const currentChat = this.chatHistory.data.find(chat => chat.id === id); 103 | const requestId = Math.random().toString(16).slice(2); 104 | this.chatHistory.job[id].push(requestId); 105 | const messages = currentChat.messages.slice(1).slice(0 - CONTEXT_MESSAGE_COUNT).map(message => { 106 | return { 107 | content: message.text, 108 | role: message.sender === "bot" ? "assistant" : "user" 109 | } 110 | }); 111 | const response = await fetch(API_URL, { 112 | method: 'POST', 113 | headers: API_HEADER ?? {}, 114 | body: JSON.stringify({ messages }) 115 | }); 116 | 117 | try { 118 | const data = await response.json(); 119 | currentChat.messages.push({ 120 | sender: "bot", 121 | text: data.choices[0].message.content, 122 | timestamp: new Date().getTime() 123 | }); 124 | 125 | this.saveChatHistory(); 126 | } finally { 127 | const jobIndex = this.chatHistory.job[id].findIndex(job => job === requestId); 128 | this.chatHistory.job[id].splice(jobIndex, 1); 129 | if (this.chatHistory.job[id].length === 0) { 130 | delete this.chatHistory.job[id]; 131 | } 132 | } 133 | 134 | setTimeout(() => { 135 | this.scrollThreadToBottom(); 136 | }, 0); 137 | }, 138 | removeChat: function (id) { 139 | const index = this.chatHistory.data.findIndex(chat => chat.id === id); 140 | this.chatHistory.data.splice(index, 1); 141 | if (this.chatHistory.activeChatId === id) { 142 | if (this.chatHistory.data.length > 0) { 143 | this.chatHistory.activeChatId = this.chatHistory.data[0].id; 144 | } else { 145 | this.newChat(); 146 | } 147 | } 148 | 149 | this.saveChatHistory(); 150 | 151 | setTimeout(() => { 152 | this.scrollThreadToBottom(); 153 | }, 0); 154 | }, 155 | pressToSendMessage: function (event) { 156 | if (event.keyCode === 13 && event.ctrlKey) { 157 | this.sendMessage(); 158 | event.preventDefault(); 159 | 160 | return false; 161 | } 162 | }, 163 | saveChatHistory: function () { 164 | const chatHistory = JSON.stringify(this.chatHistory.data); 165 | localStorage.setItem("chatHistory", chatHistory); 166 | } 167 | } 168 | }); -------------------------------------------------------------------------------- /spring-chatgpt-sample-webapi/src/main/resources/static/main.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 5px; 3 | height: 5px; 4 | } 5 | 6 | ::-webkit-scrollbar-track { 7 | background-color: transparent; 8 | } 9 | 10 | ::-webkit-scrollbar-thumb { 11 | background-color: #ccc; 12 | border-radius: 10px; 13 | background-clip: content-box; 14 | border: 1px solid transparent; 15 | } 16 | 17 | [v-cloak] { 18 | display: none; 19 | } 20 | 21 | html, body { 22 | margin: 0; 23 | padding: 0; 24 | width: 100%; 25 | height: 100%; 26 | font-family: Arial, Helvetica, sans-serif; 27 | min-width: 800px; 28 | } 29 | 30 | body { 31 | background-image: url(lowpoly.png); 32 | background-size: cover; 33 | } 34 | 35 | body::before { 36 | content: ""; 37 | display: block; 38 | position: fixed; 39 | top: 0; 40 | left: 0; 41 | width: 100%; 42 | height: 100%; 43 | background-image: linear-gradient(to top left, #bc138f, #4537c2, #110f29); 44 | mix-blend-mode: hard-light; 45 | z-index: -1; 46 | } 47 | 48 | a { 49 | color: #FFF; 50 | background: #bc138f; 51 | } 52 | 53 | #main { 54 | display: flex; 55 | flex-direction: row; 56 | width: 100%; 57 | height: 100%; 58 | } 59 | 60 | #chat-history { 61 | flex: 2; 62 | display: flex; 63 | flex-direction: column; 64 | background: rgba(255, 255, 255, 0.15); 65 | background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 60%, rgba(255, 255, 255, 0.1)61%, rgba(255, 255, 255, 0.1)70%, rgba(255, 255, 255, 0.05) 71%); 66 | backdrop-filter: blur(5px); 67 | flex: 0 0 400px; 68 | } 69 | 70 | #chat-history-list { 71 | flex: 1; 72 | display: flex; 73 | flex-direction: column; 74 | overflow-y: scroll; 75 | } 76 | 77 | #chat-history-new { 78 | display: flex; 79 | flex-direction: row; 80 | justify-content: center; 81 | align-items: center; 82 | padding: 30px; 83 | } 84 | 85 | .chat-history-new-button { 86 | width: 200px; 87 | padding: 20px; 88 | border-radius: 10px; 89 | background-image: linear-gradient(#272b51, #1b2339); 90 | border: none; 91 | color: #FFF; 92 | font-size: 1.2em; 93 | cursor: pointer; 94 | } 95 | 96 | .chat-history-new-button:hover { 97 | background-image: linear-gradient(#333862, #27304b); 98 | } 99 | 100 | #chat-thread { 101 | flex: 2; 102 | display: flex; 103 | flex-direction: column; 104 | } 105 | 106 | .chat-summary { 107 | display: flex; 108 | flex-direction: column; 109 | padding: 10px; 110 | margin: 20px; 111 | border-radius: 10px; 112 | background-image: linear-gradient(90deg, #bc138f, #4537c2, #110f29); 113 | background-repeat: no-repeat; 114 | background-position: -400px 0; 115 | background-color: #110f29; 116 | cursor: pointer; 117 | transition: all 0.2s ease-in-out; 118 | } 119 | 120 | .chat-summary:hover, .chat-summary.active { 121 | background-position: 0 0; 122 | box-shadow: 2px 2px 5px rgba(15, 22, 43, 0.75); 123 | } 124 | 125 | .chat-summary-title { 126 | font-weight: bold; 127 | font-size: 1.2em; 128 | margin-bottom: 10px; 129 | color: rgba(255, 255, 255, 0.8); 130 | padding-right: 30px; 131 | position: relative; 132 | } 133 | 134 | .close-button { 135 | width: 16px; 136 | height: 16px; 137 | border-radius: 50%; 138 | border: 2px solid #bc138f; 139 | background: url(cross.svg) center no-repeat; 140 | background-size: contain; 141 | font-size: 1.2em; 142 | text-align: center; 143 | line-height: 20px; 144 | float: right; 145 | mix-blend-mode: color-dodge; 146 | opacity: 0; 147 | transition: all 0.2s ease-in-out; 148 | position: absolute; 149 | right: 0; 150 | top: 5px; 151 | } 152 | 153 | .chat-summary:hover .chat-summary-title { 154 | color: #FFF; 155 | } 156 | 157 | .chat-summary:hover .close-button { 158 | right: 5px; 159 | opacity: 1; 160 | } 161 | 162 | .chat-summary-meta { 163 | display: flex; 164 | flex-direction: row; 165 | justify-content: space-between; 166 | } 167 | 168 | .chat-summary-message-count { 169 | color: #999; 170 | } 171 | 172 | .chat-summary-timestamp { 173 | color: #999; 174 | } 175 | 176 | #chat-thread-message-list { 177 | flex: 1; 178 | display: flex; 179 | flex-direction: column; 180 | overflow-y: scroll; 181 | } 182 | 183 | .chat-thread-message { 184 | display: flex; 185 | flex-direction: row; 186 | padding: 30px; 187 | } 188 | 189 | .chat-thread-message-bot { 190 | justify-content: flex-start; 191 | } 192 | 193 | .chat-thread-message-human { 194 | flex-direction: row-reverse; 195 | } 196 | 197 | #chat-thread-message-bot-typing { 198 | line-height: 38px; 199 | } 200 | 201 | #chat-thread-message-bot-typing img { 202 | height: 20px; 203 | vertical-align: middle; 204 | margin: 0 20px; 205 | } 206 | 207 | .chat-thread-avatar { 208 | width: 50px; 209 | height: 50px; 210 | border-radius: 50%; 211 | flex: 0 0 50px; 212 | } 213 | 214 | .chat-thread-message-bot .chat-thread-avatar { 215 | background: #FFF url(chatgpt.svg) center no-repeat; 216 | background-size: 75%; 217 | } 218 | 219 | .chat-thread-message-human .chat-thread-avatar { 220 | background: #FFF url(user.svg) center no-repeat; 221 | background-size: 75%; 222 | } 223 | 224 | .chat-thread-message-content { 225 | margin-left: 10px; 226 | margin-right: 10px; 227 | padding: 10px 30px; 228 | border-radius: 10px; 229 | background-color: rgba(15, 22, 43, 0.5); 230 | backdrop-filter: blur(5px); 231 | color: #FFF; 232 | } 233 | 234 | .chat-thread-message-timestamp { 235 | color: #999; 236 | font-size: 0.8em; 237 | margin-bottom: 5px; 238 | } 239 | 240 | .chat-thread-input-box { 241 | display: flex; 242 | flex-direction: row; 243 | padding: 30px; 244 | } 245 | 246 | #chat-input-box { 247 | flex: 1; 248 | padding: 20px; 249 | border-radius: 10px; 250 | background-color: rgba(15, 22, 43, 0.5); 251 | backdrop-filter: blur(5px); 252 | border: none; 253 | color: #FFF; 254 | font-size: 1.2em; 255 | resize: none; 256 | } 257 | 258 | .chat-thread-send-button { 259 | margin-left: 10px; 260 | width: 100px; 261 | padding: 20px; 262 | border-radius: 10px; 263 | background-image: linear-gradient(#272b51, #1b2339); 264 | border: none; 265 | color: #FFF; 266 | font-size: 1.2em; 267 | cursor: pointer; 268 | } 269 | 270 | .chat-thread-send-button:hover { 271 | background-image: linear-gradient(#333862, #27304b); 272 | } 273 | 274 | .chat-thread-send-button:active { 275 | background-color: #ddd; 276 | } 277 | 278 | .chat-thread-send-button:focus { 279 | outline: none; 280 | } 281 | 282 | .chat-thread-send-button:disabled { 283 | background-color: #ccc; 284 | } 285 | 286 | .chat-thread-send-button:disabled:hover { 287 | cursor: not-allowed; 288 | } 289 | 290 | .chat-thread-send-button:disabled:active { 291 | background-color: #ccc; 292 | } 293 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 97 | 98 | 99 | 100 | 101 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 111 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 133 | 134 | 135 | 137 | 138 | 139 | 140 | 142 | 143 | 144 | 145 | 147 | 148 | 149 | 150 | 152 | 153 | 154 | 155 | 157 | 159 | 161 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch scripts 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - azdeveloper 5 | - java 6 | - bicep 7 | - javascript 8 | - html 9 | products: 10 | - azure 11 | - azure-spring-apps 12 | - azure-blob-storage 13 | - azure-openai 14 | urlFragment: spring-chatgpt-sample 15 | name: Spring ChatGPT Application using Azure OpenAI on Azure Spring Apps (Java) 16 | description: This sample shows how to build a ChatGPT like application with Java in Spring and run on Azure Spring Apps. It enables ChatGPT to use your private data to answer the questions. 17 | --- 18 | 19 | 20 | # Spring ChatGPT Sample 21 | 22 | This sample shows how to build a ChatGPT like application in Spring and run on Azure Spring Apps. 23 | It enables ChatGPT to use your private data to answer the questions. 24 | 25 | 26 | ## How it works 27 | 28 |  29 | 30 | 1. Query flow (Web API) 31 | 1. Convert the user's query text to an embedding. 32 | 1. Query Top-K nearest text chunks from the vector store (by cosine similarity). 33 | 1. Populate the prompt template with the chunks. 34 | 1. Call to OpenAI text completion API. 35 | 1. Indexing flow (CLI) 36 | 1. Load the documents from the local disk / Azure storage. 37 | 1. Split the text into chunks. 38 | 1. Convert text chunks into embeddings 39 | 1. Save the embeddings into Vector Store 40 | 41 | 42 | ## Run with Azure Developer CLI (AZD) 43 | You can provision Azure resources and run this application on Azure with Azure Developer CLI (AZD), which can help you build and run your application in the cloud quickly without running multiple az cli commands step by step. For more information, see [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview). 44 | 45 | Let's jump in and get this up and running in Azure. When you are finished, you will have a fully functional chatgpt app deployed to the cloud. In later steps, you'll see how to setup a pipeline and run the application. 46 | 47 |  48 | 49 | Screenshot of the deployed chatgpt app 50 | 51 | ### Prerequisites 52 | 53 | The following prerequisites are required to use this application. Please ensure that you have them all installed locally. 54 | 55 | - [Azure Developer CLI 1.2.0 or later](https://aka.ms/azd-install) 56 | - [Java 17 or later](https://learn.microsoft.com/java/openjdk/install) 57 | - [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) 58 | - An Azure subscription with access granted to Azure OpenAI (see more [here](https://customervoice.microsoft.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR7en2Ais5pxKtso_Pz4b1_xUOFA5Qk1UWDRBMjg0WFhPMkIzTzhKQ1dWNyQlQCN0PWcu)) 59 | - [Powershell 7](https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.3) if you use windows 60 | 61 | If you are using Azure Developer CLI with the version lower than 1.2.0, then you will need to enable the feature for Azure Spring Apps support manually by the following command: 62 | ```bash 63 | azd config set alpha.springapp on 64 | ``` 65 | 66 | ### Quickstart 67 | 68 | To learn how to get started with any template, follow the steps in [this quickstart](https://learn.microsoft.com/azure/developer/azure-developer-cli/get-started?tabs=localinstall&pivots=programming-language-java) with this template(`Azure-Samples/spring-chatgpt-sample`). 69 | 70 | This quickstart will show you how to authenticate on Azure, initialize using a template, provision infrastructure and deploy code on Azure via the following commands: 71 | 72 | ```bash 73 | # Log in to azd. Only required once per-install. 74 | azd auth login 75 | 76 | # First-time project setup. Initialize a project in the current directory, using this template. 77 | azd init --template Azure-Samples/spring-chatgpt-sample 78 | 79 | # Provision and deploy to Azure 80 | azd up 81 | ``` 82 | 83 | The template by default uses a pre-built documentation [vector store](https://asawikigpt.blob.core.windows.net/demo/doc_store.json) of the [public documents](https://github.com/MicrosoftDocs/azure-docs/tree/main/articles/spring-apps) of the Azure Spring Apps. 84 | 85 | If you want to load your own documents to the vector store, you can use the following command before running `azd up`: 86 | ```shell 87 | # under the root of the project 88 | mvn clean package 89 | java -jar spring-chatgpt-sample-cli/target/spring-chatgpt-sample-cli-0.0.1-SNAPSHOT.jar --from=//// 90 | ``` 91 | 92 | ### Application Architecture 93 | 94 | This application utilizes the following Azure resources: 95 | 96 | - [**Azure Spring Apps**](https://docs.microsoft.com/azure/spring-apps/) to host the application 97 | - [**Azure OpenAI**](https://docs.microsoft.com/azure/cognitive-services/openai/) for ChatGPT 98 | - [**Azure Storage**](https://docs.microsoft.com/azure/storage/) as the ASA file volume for the vector store to achieve persistent storage 99 | 100 | Here's a high level architecture diagram that illustrates these components. Notice that these are all contained within a single [resource group](https://docs.microsoft.com/azure/azure-resource-manager/management/manage-resource-groups-portal), that will be created for you when you create the resources. 101 | 102 |  103 | 104 | > This template provisions resources to an Azure subscription that you will select upon provisioning them. Please refer to the [Pricing calculator for Microsoft Azure](https://azure.microsoft.com/pricing/calculator/) and, if needed, update the included Azure resource definitions found in `infra/main.bicep` to suit your needs. 105 | 106 | ### Application Code 107 | 108 | This template is structured to follow the [Azure Developer CLI](https://aka.ms/azure-dev/overview). You can learn more about `azd` architecture in [the official documentation](https://learn.microsoft.com/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create#understand-the-azd-architecture). 109 | 110 | ### Next Steps 111 | 112 | At this point, you have a complete application deployed on Azure. But there is much more that the Azure Developer CLI can do. These next steps will introduce you to additional commands that will make creating applications on Azure much easier. Using the Azure Developer CLI, you can delete the resources easily. 113 | 114 | - [`azd down`](https://learn.microsoft.com/azure/developer/azure-developer-cli/reference#azd-down) - to delete all the Azure resources created with this template 115 | 116 | 117 | ### Additional `azd` commands 118 | 119 | The Azure Developer CLI includes many other commands to help with your Azure development experience. You can view these commands at the terminal by running `azd help`. You can also view the full list of commands on our [Azure Developer CLI command](https://aka.ms/azure-dev/ref) page. 120 | 121 | 122 | ## Run with Azure CLI 123 | **As an alternative to AZD**, you can run this sample app using the Azure CLI by following these steps. 124 | 125 | ### Prerequisites 126 | 127 | - JDK 17 128 | - Maven 129 | - Azure CLI 130 | - An Azure subscription with access granted to Azure OpenAI (see more [here](https://aka.ms/oai/access)) 131 | 132 | ### Prepare Azure Spring Apps instance 133 | 134 | 1. Use the following commands to define variables for this quickstart with the names of your resources and desired settings: 135 | 136 | ```bash 137 | LOCATION="eastus" 138 | RESOURCE_GROUP="" 139 | MANAGED_ENVIRONMENT="" 140 | SERVICE_NAME="" 141 | APP_NAME="" 142 | OPENAI_RESOURCE_NAME="" 143 | ``` 144 | 145 | 1. Use the following command to create a resource group: 146 | 147 | ```bash 148 | az group create \ 149 | --resource-group ${RESOURCE_GROUP} \ 150 | --location ${LOCATION} 151 | ``` 152 | 153 | 1. An Azure Container Apps environment creates a secure boundary around a group of applications. Apps deployed to the same environment are deployed in the same virtual network and write logs to the same log analytics workspace. For more information, see [Log Analytics workspace overview](../azure-monitor/logs/log-analytics-workspace-overview.md). Use the following command to create the environment: 154 | 155 | ```bash 156 | az containerapp env create \ 157 | --resource-group ${RESOURCE_GROUP} \ 158 | --name ${MANAGED_ENVIRONMENT} \ 159 | --location ${LOCATION} \ 160 | --enable-workload-profiles 161 | ``` 162 | 163 | 1. Use the following command to create a variable to store the environment resource ID: 164 | 165 | ```bash 166 | MANAGED_ENV_RESOURCE_ID=$(az containerapp env show \ 167 | --resource-group ${RESOURCE_GROUP} \ 168 | --name ${MANAGED_ENVIRONMENT} \ 169 | --query id \ 170 | --output tsv) 171 | ``` 172 | 173 | 1. Use the following command to create an Azure Spring Apps service instance. An instance of the Azure Spring Apps Standard consumption and dedicated plan is built on top of the Azure Container Apps environment. Create your Azure Spring Apps instance by specifying the resource ID of the environment you created. 174 | 175 | ```bash 176 | az spring create \ 177 | --resource-group ${RESOURCE_GROUP} \ 178 | --name ${SERVICE_NAME} \ 179 | --managed-environment ${MANAGED_ENV_RESOURCE_ID} \ 180 | --sku standardGen2 \ 181 | --location ${LOCATION} 182 | ``` 183 | 184 | ### Prepare Azure OpenAI Service 185 | 186 | 1. Run the following command to create an Azure OpenAI resource in the the resource group. 187 | 188 | ```bash 189 | az cognitiveservices account create \ 190 | -n ${OPENAI_RESOURCE_NAME} \ 191 | -g ${RESOURCE_GROUP} \ 192 | -l ${LOCATION} \ 193 | --kind OpenAI \ 194 | --sku s0 \ 195 | --custom-domain ${OPENAI_RESOURCE_NAME} 196 | ``` 197 | 198 | 1. Create the model deployments for `text-embedding-ada-002` and `gpt-35-turbo` in your Azure OpenAI service. 199 | ```bash 200 | az cognitiveservices account deployment create \ 201 | -g ${RESOURCE_GROUP} \ 202 | -n ${OPENAI_RESOURCE_NAME} \ 203 | --deployment-name text-embedding-ada-002 \ 204 | --model-name text-embedding-ada-002 \ 205 | --model-version "2" \ 206 | --model-format OpenAI 207 | 208 | az cognitiveservices account deployment create \ 209 | -g ${RESOURCE_GROUP} \ 210 | -n ${OPENAI_RESOURCE_NAME} \ 211 | --deployment-name gpt-35-turbo \ 212 | --model-name gpt-35-turbo \ 213 | --model-version "0301" \ 214 | --model-format OpenAI 215 | ``` 216 | 217 | ### Clone and Build the repo 218 | 219 | 1. Run `git clone https://github.com/Azure-Samples/spring-chatgpt-sample.git` 220 | 2. Run `cd spring-chatgpt-sample`. 221 | 3. Run `cp env.sh.sample env.sh` and substitute the placeholders. 222 | 4. Build with `mvn clean package`. 223 | 224 | 225 | ### Preprocess the documents 226 | 227 | Before running the web app, you need to preprocess the documents and load them into the vector store: 228 | ```bash 229 | source env.sh 230 | java -jar spring-chatgpt-sample-cli/target/spring-chatgpt-sample-cli-0.0.1-SNAPSHOT.jar --from=//// 231 | ``` 232 | 233 | Or [dowload](https://asawikigpt.blob.core.windows.net/demo/doc_store.json) the pre-built vector store of the [public documents](https://github.com/MicrosoftDocs/azure-docs/tree/main/articles/spring-apps) of the Azure Spring Apps. 234 | 235 | ### Run in local 236 | 237 | To run the demo in the local machine, please follow these steps: 238 | 239 | 1. Launch the web app 240 | ```bash 241 | source env.sh 242 | java -jar spring-chatgpt-sample-webapi/target/spring-chatgpt-sample-webapi-0.0.1-SNAPSHOT.jar 243 | ``` 244 | 245 | 1. Open `http://localhost:8080` in your browser. 246 | 247 | ### Run in Azure Spring Apps 248 | 249 | 1. Use the following command to specify the app name on Azure Spring Apps and to allocate required resources: 250 | 251 | ```bash 252 | az spring app create \ 253 | --resource-group ${RESOURCE_GROUP} \ 254 | --service ${SERVICE_NAME} \ 255 | --name ${APP_NAME} \ 256 | --cpu 2 \ 257 | --memory 4Gi \ 258 | --min-replicas 2 \ 259 | --max-replicas 2 \ 260 | --assign-endpoint true 261 | ``` 262 | 263 | 1. Create a Azure storage account and a file share, then add the storage link in the Azure Container Apps environment by using the following commands. The `az containerapp env storage set` command creates a link between the environment and the file share. 264 | 265 | ```bash 266 | STORAGE_ACCOUNT_NAME="" 267 | FILE_SHARE_NAME="vectorstore" 268 | STORAGE_MOUNT_NAME="vectorstore" 269 | 270 | az storage account create \ 271 | --resource-group ${RESOURCE_GROUP} \ 272 | --name ${STORAGE_ACCOUNT_NAME} \ 273 | --kind StorageV2 \ 274 | --sku Standard_LRS \ 275 | --enable-large-file-share \ 276 | --output none 277 | 278 | az storage share-rm create \ 279 | --resource-group ${RESOURCE_GROUP} \ 280 | --storage-account ${STORAGE_ACCOUNT_NAME} \ 281 | --name ${FILE_SHARE_NAME} \ 282 | --quota 1024 \ 283 | --enabled-protocols SMB \ 284 | --output none 285 | 286 | STORAGE_ACCOUNT_KEY=$(az storage account keys list \ 287 | --resource-group ${RESOURCE_GROUP} \ 288 | --account-name ${STORAGE_ACCOUNT_NAME} \ 289 | --query "[0].value" \ 290 | -o tsv) 291 | 292 | az containerapp env storage set \ 293 | --resource-group ${RESOURCE_GROUP} \ 294 | --name ${MANAGED_ENVIRONMENT} \ 295 | --storage-name ${STORAGE_MOUNT_NAME} \ 296 | --azure-file-account-name ${STORAGE_ACCOUNT_NAME} \ 297 | --azure-file-account-key ${STORAGE_ACCOUNT_KEY} \ 298 | --azure-file-share-name ${FILE_SHARE_NAME} \ 299 | --access-mode ReadOnly 300 | ``` 301 | 302 | 1. Add the persistent storage to the app by using the following command: 303 | 304 | ```bash 305 | az spring app append-persistent-storage \ 306 | --resource-group ${RESOURCE_GROUP} \ 307 | --service ${SERVICE_NAME} \ 308 | --name ${APP_NAME} \ 309 | --persistent-storage-type AzureFileVolume \ 310 | --mount-path /opt/spring-chatgpt-sample \ 311 | --storage-name ${STORAGE_MOUNT_NAME} 312 | ``` 313 | 314 | 1. Upload the vector store file to the Azure storage account built in the previous step. 315 | 316 | ```bash 317 | az storage file upload -s ${FILE_SHARE_NAME} --account-name ${STORAGE_ACCOUNT_NAME} \ 318 | --account-key ${STORAGE_ACCOUNT_KEY} --source ./doc_store.json 319 | ``` 320 | 321 | 1. Use the following command to deploy the *.jar* file for the app: 322 | 323 | ```bash 324 | az spring app deploy \ 325 | --resource-group ${RESOURCE_GROUP} \ 326 | --service ${SERVICE_NAME} \ 327 | --name ${APP_NAME} \ 328 | --artifact-path spring-chatgpt-sample-webapi/target/spring-chatgpt-sample-webapi-0.0.1-SNAPSHOT.jar \ 329 | --env AZURE_OPENAI_ENDPOINT= AZURE_OPENAI_APIKEY= AZURE_OPENAI_CHATDEPLOYMENTID=gpt-35-turbo AZURE_OPENAI_EMBEDDINGDEPLOYMENTID=text-embedding-ada-002 VECTORSTORE_FILE=/opt/spring-chatgpt-sample/doc_store.json \ 330 | --runtime-version Java_17 331 | ``` 332 | 333 | 334 | ## Reporting Issues and Feedback 335 | 336 | If you have any feature requests, issues, or areas for improvement, please [file an issue](https://aka.ms/azure-dev/issues). To keep up-to-date, ask questions, or share suggestions, join our [GitHub Discussions](https://aka.ms/azure-dev/discussions). You may also contact us via AzDevTeam@microsoft.com. 337 | --------------------------------------------------------------------------------