├── src ├── backend │ ├── .funcignore │ ├── host.json │ ├── requirements.txt │ ├── .gitignore │ └── function_app.py └── frontend │ ├── src │ ├── vite-env.d.ts │ ├── main.tsx │ ├── components │ │ ├── ChatAnswer │ │ │ ├── ChatAnswer.module.css │ │ │ └── ChatAnswer.tsx │ │ ├── ChatQuestion │ │ │ ├── ChatQuestion.tsx │ │ │ └── ChatQuestion.module.css │ │ ├── ChatAnswerError │ │ │ ├── ChatAnswerError.module.css │ │ │ └── ChatAnswerError.tsx │ │ ├── ChatAnswerLoading │ │ │ ├── ChatAnswerLoading.module.css │ │ │ └── ChatAnswerLoading.tsx │ │ └── InputQuestion │ │ │ ├── InputQuestion.module.css │ │ │ └── InputQuestion.tsx │ ├── App.css │ ├── index.css │ ├── assets │ │ └── react.svg │ └── App.tsx │ ├── staticwebapp.config.json │ ├── vite.config.ts │ ├── tsconfig.node.json │ ├── scripts │ ├── prepackage.sh │ └── prepackage.ps1 │ ├── index.html │ ├── .gitignore │ ├── .eslintrc.cjs │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ └── public │ └── vite.svg ├── data └── 001018385.pdf ├── .gitignore ├── scripts ├── requirements.txt ├── cosmosreadwriterole.json ├── local.settings.json ├── postprovision.sh ├── postprovision.ps1 └── indexer.py ├── .vscode ├── extensions.json ├── settings.json ├── launch.json └── tasks.json ├── swa-cli.config.json ├── infra ├── modules │ ├── monitor │ │ └── appi.bicep │ ├── app │ │ ├── asp.bicep │ │ ├── stapp.bicep │ │ └── func.bicep │ ├── storage │ │ └── storage.bicep │ ├── security │ │ └── role.bicep │ ├── ai │ │ ├── srch.bicep │ │ └── cognitiveservices.bicep │ └── db │ │ └── cosmosdb.bicep ├── main.parameters.json ├── abbreviations.json └── main.bicep ├── README.md ├── azure.yaml └── LICENSE.md /src/backend/.funcignore: -------------------------------------------------------------------------------- 1 | .venv -------------------------------------------------------------------------------- /src/frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /data/001018385.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noriyukitakei/aoai-rag-starter/HEAD/data/001018385.pdf -------------------------------------------------------------------------------- /src/frontend/staticwebapp.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "platform": { 3 | "apiRuntime": "python:3.10" 4 | } 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .azure/* 4 | .azurite/* 5 | .env 6 | scripts/.venv 7 | src/frontend/.env.production 8 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-search-documents==11.6.0b2 2 | langchain==0.1.11 3 | azure-ai-formrecognizer==3.3.2 4 | openai==1.13.3 5 | azure-identity==1.15.0 -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions", 4 | "ms-python.python", 5 | "ms-azuretools.vscode-azurestaticwebapps", 6 | "azurite.azurite" 7 | ] 8 | } -------------------------------------------------------------------------------- /src/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | 5 | ReactDOM.createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /src/frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /src/frontend/scripts/prepackage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # フロントエンドアプリケーションがビルドされる前に実行されるスクリプト 4 | 5 | # フロントエンドエンドアプリケーション内に埋め込むバックエンドAPIのエンドポイントを取得し、 6 | # .env.productionファイルに書き出す。この環境変数はビルドされたフロントエンドアプリケーション内で使われる。 7 | azd env get-values 2>/dev/null | grep "^API_ENDPOINT" | sed 's/^API_ENDPOINT="\([^"]*\)"$/VITE_API_ENDPOINT=\1/' > .env.production -------------------------------------------------------------------------------- /src/frontend/src/components/ChatAnswer/ChatAnswer.module.css: -------------------------------------------------------------------------------- 1 | .chatMessage { 2 | padding: 10px; 3 | margin: 5px 0; 4 | border-radius: 20px; 5 | max-width: 66%; 6 | display: flex; 7 | align-items: center; 8 | word-break: break-word; 9 | } 10 | 11 | .otherMessage { 12 | background: #f0f0f0; 13 | align-self: flex-start; 14 | } 15 | -------------------------------------------------------------------------------- /src/frontend/src/components/ChatAnswer/ChatAnswer.tsx: -------------------------------------------------------------------------------- 1 | import styles from './ChatAnswer.module.css'; 2 | 3 | interface Props { 4 | answer: string|undefined; 5 | } 6 | 7 | export const ChatAnswer = (props: Props) => { 8 | return ( 9 |
{props.answer}
10 | ); 11 | }; -------------------------------------------------------------------------------- /src/frontend/src/components/ChatQuestion/ChatQuestion.tsx: -------------------------------------------------------------------------------- 1 | import styles from './ChatQuestion.module.css'; 2 | 3 | interface Props { 4 | question: string; 5 | } 6 | 7 | export const ChatQuestion = (props: Props) => { 8 | return ( 9 |
{props.question}
10 | ); 11 | }; -------------------------------------------------------------------------------- /src/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | カスタムチャットUI 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/frontend/src/components/ChatQuestion/ChatQuestion.module.css: -------------------------------------------------------------------------------- 1 | .chatMessage { 2 | padding: 10px; 3 | margin: 5px 0; 4 | border-radius: 20px; 5 | max-width: 66%; 6 | display: flex; 7 | align-items: center; 8 | word-break: break-word; 9 | } 10 | 11 | .myMessage { 12 | background: #007bff; 13 | color: white; 14 | align-self: flex-end; 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /swa-cli.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/azure/static-web-apps-cli/schema", 3 | "configurations": { 4 | "rag-sample": { 5 | "appLocation": "src/frontend", 6 | "outputLocation": "dist", 7 | "appBuildCommand": "npm run build", 8 | "run": "npm run dev", 9 | "appDevserverUrl": "http://localhost:5173" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | # Do not include azure-functions-worker in this file 2 | # The Python Worker is managed by the Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions 6 | azure-search-documents==11.6.0b2 7 | openai==1.13.3 8 | azure-identity==1.15.0 9 | tiktoken==0.6.0 10 | azure-cosmos==4.6.0 -------------------------------------------------------------------------------- /src/frontend/src/components/ChatAnswerError/ChatAnswerError.module.css: -------------------------------------------------------------------------------- 1 | .chatMessage { 2 | padding: 10px; 3 | margin: 5px 0; 4 | border-radius: 20px; 5 | max-width: 66%; 6 | display: flex; 7 | align-items: center; 8 | word-break: break-word; 9 | color: red; 10 | } 11 | 12 | .otherMessage { 13 | background: #f0f0f0; 14 | align-self: flex-start; 15 | } 16 | -------------------------------------------------------------------------------- /src/frontend/src/components/ChatAnswerLoading/ChatAnswerLoading.module.css: -------------------------------------------------------------------------------- 1 | .chatMessage { 2 | padding: 10px; 3 | margin: 5px 0; 4 | border-radius: 20px; 5 | width: 50%; 6 | max-width: 66%; 7 | display: flex; 8 | align-items: center; 9 | word-break: break-word; 10 | } 11 | 12 | .otherMessage { 13 | background: #f0f0f0; 14 | align-self: flex-start; 15 | } 16 | -------------------------------------------------------------------------------- /src/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/frontend/src/components/ChatAnswerError/ChatAnswerError.tsx: -------------------------------------------------------------------------------- 1 | import styles from './ChatAnswerError.module.css'; 2 | 3 | interface Props { 4 | errorMessage: string|undefined; 5 | } 6 | 7 | export const ChatAnswerError = (props: Props) => { 8 | return ( 9 |
{props.errorMessage}
10 | ); 11 | }; -------------------------------------------------------------------------------- /src/frontend/src/components/InputQuestion/InputQuestion.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | background: #007bff; 3 | color: white; 4 | border: none; 5 | padding: 10px 20px; 6 | border-radius: 18px; 7 | cursor: pointer; 8 | } 9 | 10 | .button_disabled { 11 | background: #ccc; 12 | color: white; 13 | border: none; 14 | padding: 10px 20px; 15 | border-radius: 18px; 16 | cursor: pointer; 17 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.deploySubpath": "src/backend", 3 | "azureFunctions.scmDoBuildDuringDeployment": true, 4 | "azureFunctions.pythonVenv": ".venv", 5 | "azureFunctions.projectLanguage": "Python", 6 | "azureFunctions.projectRuntime": "~4", 7 | "debug.internalConsoleOptions": "neverOpen", 8 | "azureFunctions.projectLanguageModel": 2, 9 | "azurite.location": ".azurite" 10 | } -------------------------------------------------------------------------------- /src/frontend/scripts/prepackage.ps1: -------------------------------------------------------------------------------- 1 | # PowerShell 2 | 3 | # フロントエンドアプリケーションがビルドされる前に実行されるスクリプト 4 | 5 | # フロントエンドエンドアプリケーション内に埋め込むバックエンドAPIのエンドポイントを取得し、 6 | # .env.productionファイルに書き出す。この環境変数はビルドされたフロントエンドアプリケーション内で使われる。 7 | $API_ENDPOINT = (azd env get-values 2>$null | Select-String "^API_ENDPOINT" | ForEach-Object { $_ -replace '^API_ENDPOINT=', '' -replace '"', '' }) 8 | "VITE_API_ENDPOINT=$API_ENDPOINT" | Out-File -FilePath .env.production -------------------------------------------------------------------------------- /infra/modules/monitor/appi.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 6 | name: name 7 | location: location 8 | kind: 'web' 9 | tags: tags 10 | properties: { 11 | Application_Type: 'web' 12 | Request_Source: 'rest' 13 | } 14 | } 15 | 16 | output name string = applicationInsights.name 17 | -------------------------------------------------------------------------------- /scripts/cosmosreadwriterole.json: -------------------------------------------------------------------------------- 1 | { 2 | "RoleName": "MyReadWriteRole", 3 | "Type": "CustomRole", 4 | "AssignableScopes": ["/"], 5 | "Permissions": [{ 6 | "DataActions": [ 7 | "Microsoft.DocumentDB/databaseAccounts/readMetadata", 8 | "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*", 9 | "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*" 10 | ] 11 | }] 12 | } -------------------------------------------------------------------------------- /infra/modules/app/asp.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | resource hostingPlan 'Microsoft.Web/serverfarms@2021-03-01' = { 6 | name: name 7 | location: location 8 | tags: tags 9 | sku: { 10 | name: 'Y1' 11 | tier: 'Dynamic' 12 | size: 'Y1' 13 | family: 'Y' 14 | capacity: 0 15 | } 16 | properties: { 17 | reserved: true 18 | } 19 | } 20 | 21 | output id string = hostingPlan.id 22 | -------------------------------------------------------------------------------- /infra/modules/storage/storage.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param sku object = {} 4 | param tags object = {} 5 | 6 | resource storageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = { 7 | name: name 8 | location: location 9 | sku: sku 10 | kind: 'Storage' 11 | tags: tags 12 | properties: { 13 | supportsHttpsTrafficOnly: true 14 | defaultToOAuthAuthentication: true 15 | } 16 | } 17 | 18 | output name string = storageAccount.name 19 | -------------------------------------------------------------------------------- /src/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rag-sample 2 | 3 | 本ソースコードは、生成AIによるチャットシステムを実現するRAG(Retrieval-Augmented Generation)のサンプルコードです。 4 | 5 | 「シンプル」「強力」「すぐ動く」をモットーにしたアプリケーションで、これらかRAGを始める人に参考にしてもらうべく開発しました。 6 | 7 | - シンプル 8 | 余計な機能がなく、シンプルなチャットのインターフェースのみを提供しています。これにより、ソースコードの可読性が高まり、中身を理解しやすくなっています。 9 | 10 | - 強力 11 | シンプルとはいえど機能は強力です。Azureの最先端の検索手法である「セマンティックハイブリッド検索」、Azure Static Web Apps、Azure Functionsによるフルサーバーレス構成といった最新技術が集結されています。 12 | 13 | - すぐ動く 14 | Azure Developer CLI、BicepによってIaC(Infrastructure as Code)を実現しており、コマンド一発でAzureにリソースがデプロイされて動くようになっています。 15 | -------------------------------------------------------------------------------- /infra/modules/app/stapp.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Static Web Apps instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | 7 | param sku object = { 8 | name: 'Free' 9 | tier: 'Free' 10 | } 11 | 12 | resource stapp 'Microsoft.Web/staticSites@2021-03-01' = { 13 | name: name 14 | location: location 15 | tags: tags 16 | sku: sku 17 | properties: { 18 | provider: 'Custom' 19 | } 20 | } 21 | 22 | output name string = stapp.name 23 | output uri string = 'https://${stapp.properties.defaultHostname}' 24 | -------------------------------------------------------------------------------- /infra/modules/security/role.bicep: -------------------------------------------------------------------------------- 1 | param principalId string 2 | 3 | @allowed([ 4 | 'Device' 5 | 'ForeignGroup' 6 | 'Group' 7 | 'ServicePrincipal' 8 | 'User' 9 | ]) 10 | param principalType string = 'ServicePrincipal' 11 | param roleDefinitionId string 12 | 13 | resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 14 | name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) 15 | properties: { 16 | principalId: principalId 17 | principalType: principalType 18 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/backend/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | csx 4 | .vs 5 | edge 6 | Publish 7 | 8 | *.user 9 | *.suo 10 | *.cscfg 11 | *.Cache 12 | project.lock.json 13 | 14 | /packages 15 | /TestResults 16 | 17 | /tools/NuGet.exe 18 | /App_Data 19 | /secrets 20 | /data 21 | .secrets 22 | appsettings.json 23 | local.settings.json 24 | 25 | node_modules 26 | dist 27 | 28 | # Local python packages 29 | .python_packages/ 30 | 31 | # Python Environments 32 | .env 33 | .venv 34 | env/ 35 | venv/ 36 | ENV/ 37 | env.bak/ 38 | venv.bak/ 39 | 40 | # Byte-compiled / optimized / DLL files 41 | __pycache__/ 42 | *.py[cod] 43 | *$py.class 44 | 45 | # Azurite artifacts 46 | __blobstorage__ 47 | __queuestorage__ 48 | __azurite_db*__.json -------------------------------------------------------------------------------- /src/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/frontend/src/components/ChatAnswerLoading/ChatAnswerLoading.tsx: -------------------------------------------------------------------------------- 1 | import styles from './ChatAnswerLoading.module.css'; 2 | import { useSpring, animated } from '@react-spring/web'; 3 | import { useState } from 'react'; 4 | 5 | export const ChatAnswerLoading = () => { 6 | const [dots, setDots] = useState('.'); 7 | const stylesa = useSpring({ 8 | from: { opacity: 0 }, 9 | to: { opacity: 1 }, 10 | reset: true, 11 | reverse: dots.length === 3, 12 | delay: 200, 13 | onRest: () => setDots(dots => dots.length < 3 ? dots + '.' : '.'), 14 | }); 15 | 16 | return ( 17 |
18 | 回答を生成中です🖊 19 | {dots} 20 |
21 | ); 22 | }; -------------------------------------------------------------------------------- /scripts/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "python", 5 | "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", 6 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 7 | "AOAI_ENDPOINT": "${AOAI_ENDPOINT}", 8 | "SEARCH_SERVICE_ENDPOINT": "${SEARCH_SERVICE_ENDPOINT}", 9 | "COSMOSDB_ENDPOINT": "${COSMOSDB_ENDPOINT}", 10 | "COSMOSDB_DATABASE": "${COSMOSDB_DATABASE}", 11 | "COSMOSDB_CONTAINER": "${COSMOSDB_CONTAINER}", 12 | "AOAI_MODEL": "${AOAI_MODEL}", 13 | "AOAI_GPT_35_TURBO_DEPLOYMENT": "${AOAI_GPT_35_TURBO_DEPLOYMENT}", 14 | "AOAI_GPT_4_DEPLOYMENT": "${AOAI_GPT_4_DEPLOYMENT}", 15 | "AOAI_GPT_4_32K_DEPLOYMENT": "${AOAI_GPT_4_32K_DEPLOYMENT}", 16 | "AOAI_TEXT_EMBEDDING_ADA_002_DEPLOYMENT": "${AOAI_TEXT_EMBEDDING_ADA_002_DEPLOYMENT}", 17 | "AOAI_API_VERSION": "${AOAI_API_VERSION}" 18 | } 19 | } -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | name: rag-sample 2 | metadata: 3 | template: rag-sample@0.0.1-beta 4 | services: 5 | web: 6 | project: ./src/frontend 7 | dist: dist 8 | language: ts 9 | host: staticwebapp 10 | hooks: 11 | prepackage: 12 | windows: 13 | shell: pwsh 14 | run: ./scripts/prepackage.ps1 15 | interactive: true 16 | continueOnError: false 17 | posix: 18 | shell: sh 19 | run: ./scripts/prepackage.sh 20 | interactive: true 21 | continueOnError: false 22 | api: 23 | project: ./src/backend 24 | language: py 25 | host: function 26 | hooks: 27 | postprovision: 28 | windows: 29 | shell: pwsh 30 | run: ./scripts/postprovision.ps1 31 | interactive: true 32 | continueOnError: false 33 | posix: 34 | shell: sh 35 | run: ./scripts/postprovision.sh 36 | interactive: true 37 | continueOnError: false -------------------------------------------------------------------------------- /src/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "postbuild": "cpx staticwebapp.config.json ./dist", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "@react-spring/web": "^9.7.3", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.2.56", 20 | "@types/react-dom": "^18.2.19", 21 | "@typescript-eslint/eslint-plugin": "^7.0.2", 22 | "@typescript-eslint/parser": "^7.0.2", 23 | "@vitejs/plugin-react-swc": "^3.5.0", 24 | "cpx": "^1.5.0", 25 | "eslint": "^8.56.0", 26 | "eslint-plugin-react-hooks": "^4.6.0", 27 | "eslint-plugin-react-refresh": "^0.4.5", 28 | "typescript": "^5.2.2", 29 | "vite": "^5.1.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Noriyuki Takei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /infra/modules/ai/srch.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | 5 | param sku object = { 6 | name: 'standard' 7 | } 8 | 9 | param authOptions object = {} 10 | param semanticSearch string = 'disabled' 11 | 12 | resource search 'Microsoft.Search/searchServices@2021-04-01-preview' = { 13 | name: name 14 | location: location 15 | tags: tags 16 | identity: { 17 | type: 'SystemAssigned' 18 | } 19 | properties: { 20 | authOptions: authOptions 21 | disableLocalAuth: false 22 | disabledDataExfiltrationOptions: [] 23 | encryptionWithCmk: { 24 | enforcement: 'Unspecified' 25 | } 26 | hostingMode: 'default' 27 | networkRuleSet: { 28 | bypass: 'None' 29 | ipRules: [] 30 | } 31 | partitionCount: 1 32 | publicNetworkAccess: 'Enabled' 33 | replicaCount: 1 34 | semanticSearch: semanticSearch 35 | } 36 | sku: sku 37 | } 38 | 39 | output id string = search.id 40 | output endpoint string = 'https://${name}.search.windows.net/' 41 | output name string = search.name 42 | -------------------------------------------------------------------------------- /src/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | height: 100%; 3 | margin: 0; 4 | font-family: Arial, sans-serif; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | background: #E7EFF9; 9 | } 10 | 11 | .chat-container { 12 | width: 600px; 13 | margin: 0 auto; 14 | padding: 20px; 15 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 16 | border-radius: 8px; 17 | background: white; 18 | display: flex; 19 | flex-direction: column; 20 | height: 80vh; 21 | overflow: auto; 22 | } 23 | 24 | .chat-message { 25 | padding: 10px; 26 | margin: 5px 0; 27 | border-radius: 20px; 28 | max-width: 66%; 29 | display: flex; 30 | align-items: center; 31 | word-break: break-word; 32 | } 33 | 34 | .other-message { 35 | background: #f0f0f0; 36 | align-self: flex-start; 37 | } 38 | 39 | .my-message { 40 | background: #007bff; 41 | color: white; 42 | align-self: flex-end; 43 | } 44 | 45 | .chat-input { 46 | border-top: 1px solid #f0f0f0; 47 | padding-top: 10px; 48 | display: flex; 49 | } 50 | 51 | .chat-input input { 52 | flex-grow: 1; 53 | border-radius: 18px; 54 | padding: 10px; 55 | border: 1px solid #007bff; 56 | margin-right: 8px; 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "[Web]SWA: Run rag-sample", 6 | "request": "launch", 7 | "type": "chrome", 8 | "url": "http://localhost:4280", 9 | "preLaunchTask": "swa: start rag-sample", 10 | "webRoot": "${workspaceFolder}/src/frontend/src", 11 | "timeout": 30000 12 | }, 13 | { 14 | "name": "[API]Attach to Python Functions", 15 | "type": "python", 16 | "request": "attach", 17 | "port": 9091, 18 | "preLaunchTask": "func: host start" 19 | }, 20 | { 21 | "name": "[Indexer]Python: indexer", 22 | "type": "python", 23 | "program": "${workspaceFolder}/scripts/indexer.py", 24 | "request": "launch", 25 | "console": "integratedTerminal", 26 | "python": "${workspaceFolder}/scripts/.venv/bin/python", 27 | "envFile": "${workspaceFolder}/scripts/.env", 28 | "args": [ 29 | "--docs", 30 | "./data/*", 31 | // "--remove" 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /infra/modules/ai/cognitiveservices.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param tags object = {} 4 | param publicNetworkAccess string = 'Enabled' 5 | param customSubDomainName string = name 6 | param sku object = { 7 | name: 'S0' 8 | } 9 | param kind string 10 | param deployments array = [] 11 | 12 | resource aisa 'Microsoft.CognitiveServices/accounts@2023-10-01-preview' = { 13 | name: name 14 | location: location 15 | tags: tags 16 | sku: sku 17 | kind: kind 18 | properties: { 19 | customSubDomainName: customSubDomainName 20 | networkAcls: { 21 | defaultAction: 'Allow' 22 | } 23 | publicNetworkAccess: publicNetworkAccess 24 | } 25 | } 26 | 27 | @batchSize(1) 28 | resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { 29 | parent: aisa 30 | name: deployment.name 31 | properties: { 32 | model: deployment.model 33 | raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null 34 | } 35 | sku: contains(deployment, 'sku') ? deployment.sku : { 36 | name: 'Standard' 37 | capacity: 20 38 | } 39 | }] 40 | 41 | output name string = aisa.name 42 | output endpoint string = aisa.properties.endpoint 43 | -------------------------------------------------------------------------------- /src/frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /src/frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "label": "func: host start", 7 | "command": "host start", 8 | "problemMatcher": "$func-python-watch", 9 | "isBackground": true, 10 | "dependsOn": "pip install (functions)", 11 | "options": { 12 | "cwd": "${workspaceFolder}/src/backend" 13 | } 14 | }, 15 | { 16 | "label": "pip install (functions)", 17 | "type": "shell", 18 | "osx": { 19 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 20 | }, 21 | "windows": { 22 | "command": "${config:azureFunctions.pythonVenv}\\Scripts\\python -m pip install -r requirements.txt" 23 | }, 24 | "linux": { 25 | "command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt" 26 | }, 27 | "problemMatcher": [], 28 | "options": { 29 | "cwd": "${workspaceFolder}/src/backend" 30 | } 31 | }, 32 | { 33 | "type": "shell", 34 | "label": "swa start", 35 | "command": "swa start http://localhost:5173/ --api-devserver-url http://localhost:7071 --run \"npm run dev\"", 36 | "isBackground": true, 37 | "problemMatcher": [ 38 | { 39 | "pattern": [ 40 | { 41 | "regexp": ".", 42 | "file": 1, 43 | "location": 2, 44 | "message": 3 45 | } 46 | ], 47 | "background": { 48 | "activeOnStart": true, 49 | "beginsPattern": ".", 50 | "endsPattern": "Waiting for .+7071 to be ready", 51 | } 52 | } 53 | ], 54 | "options": { 55 | "cwd": "${workspaceFolder}/src/frontend" 56 | }, 57 | }, 58 | ] 59 | } -------------------------------------------------------------------------------- /infra/modules/db/cosmosdb.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param cosmosDbDatabaseName string 3 | param cosmosDbContainerName string 4 | param location string 5 | param tags object = {} 6 | 7 | resource cosmosDbAccount 'Microsoft.DocumentDB/databaseAccounts@2021-04-15' = { 8 | name: name 9 | location: location 10 | tags: tags 11 | kind: 'GlobalDocumentDB' 12 | properties: { 13 | consistencyPolicy: { 14 | defaultConsistencyLevel: 'Session' 15 | } 16 | databaseAccountOfferType: 'Standard' 17 | locations: [ 18 | { 19 | locationName: location 20 | }] 21 | publicNetworkAccess: 'Enabled' 22 | } 23 | } 24 | 25 | resource cosmosDbDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2021-04-15' = { 26 | parent: cosmosDbAccount 27 | name: cosmosDbDatabaseName 28 | properties: { 29 | resource: { 30 | id: cosmosDbDatabaseName 31 | } 32 | options: { 33 | throughput: 400 34 | } 35 | } 36 | 37 | } 38 | 39 | resource cosmosDbContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2021-04-15' = { 40 | parent: cosmosDbDatabase 41 | name: cosmosDbContainerName 42 | properties: { 43 | resource: { 44 | id: cosmosDbContainerName 45 | partitionKey: { 46 | paths: [ 47 | '/id' 48 | ] 49 | kind: 'Hash' 50 | } 51 | } 52 | } 53 | } 54 | 55 | output id string = cosmosDbAccount.id 56 | output name string = cosmosDbAccount.name 57 | output endpoint string = cosmosDbAccount.properties.documentEndpoint 58 | output databaseName string = cosmosDbDatabase.name 59 | output containerName string = cosmosDbContainer.name 60 | output accountName string = cosmosDbAccount.name 61 | -------------------------------------------------------------------------------- /src/frontend/src/components/InputQuestion/InputQuestion.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import styles from './InputQuestion.module.css'; 3 | 4 | interface Props { 5 | onSend: (question: string) => void; 6 | isLoading: boolean; 7 | } 8 | 9 | export const InputQuestion = ({ onSend, isLoading }: Props) => { 10 | const [question, setQuestion] = useState(""); 11 | 12 | // 漢字変換候補決定の際のEnterキーを押下しても質問が送信されないようにするためのステートを定義する 13 | const [composing, setComposition] = useState(false); 14 | const startComposition = () => setComposition(true); 15 | const endComposition = () => setComposition(false); 16 | 17 | // 質問を送信する関数を定義する 18 | const sendQuestion = () => { 19 | // 質問が空白の場合は何もしない 20 | if (!question.trim()) { 21 | return; 22 | } 23 | 24 | // 質問を送信する 25 | onSend(question); 26 | 27 | // 質問を空白にする 28 | setQuestion(""); 29 | }; 30 | 31 | // Enterキーが押されたときに質問を送信する 32 | const onEnterPress = (ev: React.KeyboardEvent) => { 33 | // 回答が生成中の場合は何もしない 34 | if (isLoading || composing) { 35 | return; 36 | } 37 | 38 | 39 | // Enterキーが押されたかつShiftキーが押されていない場合 40 | if (ev.key === "Enter" && !ev.shiftKey) { 41 | ev.preventDefault(); 42 | sendQuestion(); 43 | } 44 | }; 45 | 46 | return ( 47 | <> 48 | setQuestion(e.target.value)} 53 | onKeyDown={onEnterPress} 54 | onCompositionStart={startComposition} 55 | onCompositionEnd={endComposition} 56 | /> 57 | 61 | 62 | 63 | ); 64 | 65 | } -------------------------------------------------------------------------------- /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 | "principalId": { 12 | "value": "${AZURE_PRINCIPAL_ID}" 13 | }, 14 | "resourceGroupName": { 15 | "value": "${AZURE_RESOURCE_GROUP}" 16 | }, 17 | "webServiceName": { 18 | "value": "${AZURE_WEB_SERVICE_NAME}" 19 | }, 20 | "webServiceLocation": { 21 | "value": "eastus2" 22 | }, 23 | "apiServiceName": { 24 | "value": "${AZURE_API_SERVICE_NAME}" 25 | }, 26 | "apiServiceLocation": { 27 | "value": "${AZURE_API_LOCATION}" 28 | }, 29 | "storageServiceSkuName": { 30 | "value": "${AZURE_STORAGE_SERVICE_SKU}" 31 | }, 32 | "storageServiceLocation": { 33 | "value": "${AZURE_STORAGE_SERVICE_LOCATION}" 34 | }, 35 | "searchServiceName": { 36 | "value": "${AZURE_SEARCH_SERVICE_NAME}" 37 | }, 38 | "searchServiceSkuName": { 39 | "value": "${AZURE_SEARCH_SERVICE_SKU}" 40 | }, 41 | "searchServiceLocation": { 42 | "value": "${AZURE_SEARCH_SERVICE_LOCATION}" 43 | }, 44 | "formRecognizerServiceName": { 45 | "value": "${AZURE_FORM_RECOGNIZER_SERVICE_NAME}" 46 | }, 47 | "formRecognizerSkuName": { 48 | "value": "${AZURE_FORM_RECOGNIZER_SERVICE_SKU}" 49 | }, 50 | "formRecognizerServiceLocation": { 51 | "value": "${AZURE_FORM_RECOGNIZER_SERVICE_LOCATION}" 52 | }, 53 | "openAiServiceName": { 54 | "value": "${AZURE_OPENAI_SERVICE_NAME}" 55 | }, 56 | "openAiModelName": { 57 | "value": "${AZURE_OPENAI_MODEL_NAME}" 58 | }, 59 | "openAiSkuName": { 60 | "value": "${AZURE_OPENAI_SERVICE_SKU}" 61 | }, 62 | "openAiServiceLocation": { 63 | "value": "${AZURE_OPENAI_SERVICE_LOCATION}" 64 | }, 65 | "cognitiveServicesName": { 66 | "value": "${AZURE_COGNITIVE_SERVICES_NAME}" 67 | }, 68 | "applicationInsightsServiceLocation": { 69 | "value": "${AZURE_APPLICATION_INSIGHTS_SERVICE_LOCATION}" 70 | }, 71 | "cosmosDbLocation": { 72 | "value": "${AZURE_COSMOS_DB_LOCATION}" 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | import { ChatQuestion } from "./components/ChatQuestion/ChatQuestion"; 3 | import { ChatAnswer } from "./components/ChatAnswer/ChatAnswer"; 4 | import { ChatAnswerLoading } from "./components/ChatAnswerLoading/ChatAnswerLoading"; 5 | import { ChatAnswerError } from "./components/ChatAnswerError/ChatAnswerError"; 6 | import { InputQuestion } from "./components/InputQuestion/InputQuestion"; 7 | 8 | 9 | import "./index.css"; 10 | 11 | function App() { 12 | const [answers, setAnswers] = useState<{user: string, response: string}[]>([]); 13 | 14 | // 回答一覧の末尾にスクロールするための参照を定義する 15 | const chatMessageStreamEnd = useRef(null); 16 | 17 | // 回答生成中、つまりAPIからの返答を待っている状態かどうかを示すステートを定義する 18 | const [isLoading, setIsLoading] = useState(false); 19 | 20 | // 最後に送信された質問を保持するための参照を定義する 21 | const lastQuestionRef = useRef(""); 22 | 23 | // エラーが発生したかどうかを示すステートを定義する 24 | const [isError, setIsError] = useState(false); 25 | 26 | // Azure OpenAI Serviceにリクエストを送る関数を定義する 27 | const makeApiRequest = async (question: string) => { 28 | // ユーザーからの質問が格納されているquestionを使ってAzure OpenAI Serviceにリクエストを送り、回答を取得する 29 | 30 | // 回答生成中の状態にする。これにより、送信ボタンが無効になる。 31 | setIsLoading(true); 32 | 33 | // エラーが発生している場合は、エラー状態を解除する 34 | setIsError(false); 35 | 36 | // 最後に送信された質問を保持する 37 | lastQuestionRef.current = question; 38 | 39 | // .env.productionにVITE_API_URLが定義されていれば、それを使ってAPIリクエストを送る。 40 | // 定義されていなければ空白とする。つまり、.env.productionにVITE_API_URLが定義されていない場合は、 41 | // ローカルの開発環境であると判断し、定義されている場合は本番環境であると判断する。 42 | const api_host = import.meta.env.VITE_API_ENDPOINT || ""; 43 | const api_url = `${api_host}/api/GenerateAnswerWithAOAI`; 44 | 45 | // answersというステートには、ユーザーからの質問とそれに対する回答が格納されており、画面に表示されている内容を表している。 46 | // このanswersをAzure OpenAI Serviceに送信するためのリクエスト形式に変換して、historyという変数に格納する。 47 | const history: {user: string, assistant: string|undefined}[] = answers.map(a => ({ user: a.user, assistant: a.response })); 48 | 49 | // 新しい質問をhistoryに追加する。assistantはまだ回答が生成されていないのでundefinedとする。 50 | history.push({user: question, assistant: undefined}); 51 | 52 | try { 53 | // Azure OpenAI Serviceにリクエストを送る 54 | const response = await fetch(api_url, { 55 | method: 'POST', 56 | headers: { 57 | 'Content-Type': 'application/json' 58 | }, 59 | body: JSON.stringify(history) 60 | }); 61 | 62 | // APIからのレスポンスをJSON形式で取得する 63 | const answer = await response.json(); 64 | 65 | // 回答生成中の状態を解除する 66 | setIsLoading(false); 67 | 68 | // answersステートに新しい質問と回答を追加する。 69 | setAnswers([...answers, {user: question, response: answer.answer}]); 70 | 71 | } catch (error) { 72 | // エラーが発生した場合は、エラー状態を設定する 73 | setIsError(true); 74 | 75 | // 回答生成中の状態を解除する 76 | setIsLoading(false); 77 | } 78 | 79 | }; 80 | 81 | // ページが読み込まれたときに、チャット画面の末尾にスクロールする 82 | useEffect(() => chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }), [isLoading]); 83 | 84 | return ( 85 | <> 86 |
87 | 88 | {answers.map((answer) => ( 89 | <> 90 | 91 | 92 | 93 | ))} 94 | {isLoading && 95 | <> 96 | 97 | 98 | 99 | } 100 | {isError && 101 | <> 102 | 103 | 104 | 105 | } 106 |
107 |
108 |
109 | makeApiRequest(question)} isLoading={isLoading}/> 110 |
111 | 112 | ); 113 | } 114 | 115 | export default App; 116 | -------------------------------------------------------------------------------- /scripts/postprovision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Azure Developer CLIによってazd provideコマンドが実行された後に実行されるスクリプト 4 | 5 | # Azuer Functionsをローカルで起動するための設定ファイルであるlocal.settings.jsonを作成する 6 | # まずは、azdコマンドを使って、Azure Functionsの設定値を取得する 7 | AOAI_ENDPOINT=$(azd env get-values 2>/dev/null | grep "^AOAI_ENDPOINT" | cut -d'=' -f2 | tr -d '"') 8 | SEARCH_SERVICE_ENDPOINT=$(azd env get-values 2>/dev/null | grep "^SEARCH_SERVICE_ENDPOINT" | cut -d'=' -f2 | tr -d '"') 9 | COSMOSDB_ENDPOINT=$(azd env get-values 2>/dev/null | grep "^COSMOSDB_ENDPOINT" | cut -d'=' -f2 | tr -d '"') 10 | COSMOSDB_DATABASE=$(azd env get-values 2>/dev/null | grep "^COSMOSDB_DATABASE" | cut -d'=' -f2 | tr -d '"') 11 | COSMOSDB_CONTAINER=$(azd env get-values 2>/dev/null | grep "^COSMOSDB_CONTAINER" | cut -d'=' -f2 | tr -d '"') 12 | AOAI_MODEL=$(azd env get-values 2>/dev/null | grep "^AOAI_MODEL" | cut -d'=' -f2 | tr -d '"') 13 | AOAI_GPT_35_TURBO_DEPLOYMENT=$(azd env get-values 2>/dev/null | grep "^AOAI_GPT_35_TURBO_DEPLOYMENT" | cut -d'=' -f2 | tr -d '"') 14 | AOAI_GPT_4_DEPLOYMENT=$(azd env get-values 2>/dev/null | grep "^AOAI_GPT_4_DEPLOYMENT" | cut -d'=' -f2 | tr -d '"') 15 | AOAI_GPT_4_32K_DEPLOYMENT=$(azd env get-values 2>/dev/null | grep "^AOAI_GPT_4_32K_DEPLOYMENT" | cut -d'=' -f2 | tr -d '"') 16 | AOAI_TEXT_EMBEDDING_ADA_002_DEPLOYMENT=$(azd env get-values 2>/dev/null | grep "^AOAI_TEXT_EMBEDDING_ADA_002_DEPLOYMENT" | cut -d'=' -f2 | tr -d '"') 17 | AOAI_API_VERSION=$(azd env get-values 2>/dev/null | grep "^AOAI_API_VERSION" | cut -d'=' -f2 | tr -d '"') 18 | 19 | # local.settings.jsonのテンプレートファイルを読み込み、先程取得した設定値で置換して、local.settings.jsonを作成する 20 | cat scripts/local.settings.json | \ 21 | sed "s|\${AOAI_ENDPOINT}|$AOAI_ENDPOINT|g" | \ 22 | sed "s|\${SEARCH_SERVICE_ENDPOINT}|$SEARCH_SERVICE_ENDPOINT|g" | \ 23 | sed "s|\${COSMOSDB_ENDPOINT}|$COSMOSDB_ENDPOINT|g" | \ 24 | sed "s|\${COSMOSDB_DATABASE}|$COSMOSDB_DATABASE|g" | \ 25 | sed "s|\${COSMOSDB_CONTAINER}|$COSMOSDB_CONTAINER|g" | \ 26 | sed "s|\${AOAI_MODEL}|$AOAI_MODEL|g" | \ 27 | sed "s|\${AOAI_GPT_35_TURBO_DEPLOYMENT}|$AOAI_GPT_35_TURBO_DEPLOYMENT|g" | \ 28 | sed "s|\${AOAI_GPT_4_DEPLOYMENT}|$AOAI_GPT_4_DEPLOYMENT|g" | \ 29 | sed "s|\${AOAI_GPT_4_32K_DEPLOYMENT}|$AOAI_GPT_4_32K_DEPLOYMENT|g" | \ 30 | sed "s|\${AOAI_TEXT_EMBEDDING_ADA_002_DEPLOYMENT}|$AOAI_TEXT_EMBEDDING_ADA_002_DEPLOYMENT|g" | \ 31 | sed "s|\${AOAI_API_VERSION}|$AOAI_API_VERSION|g" \ 32 | > ./src/backend/local.settings.json 33 | 34 | # このスクリプトで使う環境変数をexportし、さらにインデクサーで使う環境変数を.envファイルに書き出す 35 | echo "" > ./scripts/.env 36 | while IFS='=' read -r key value; do 37 | value=$(echo "$value" | sed 's/^"//' | sed 's/"$//') 38 | export "$key=$value" # このスクリプトで使う環境変数をexport 39 | echo "$key=$value" >> ./scripts/.env # インデクサーをローカル環境で使うための環境変数を.envファイルに書き出し 40 | done < func.HttpResponse: 113 | """ 114 | HTTP POSTリクエストを受け取り、Azure OpenAIを用いて回答を生成する。 115 | """ 116 | 117 | logging.info('Python HTTP trigger function processed a request.') 118 | 119 | # POSTリクエストから会話履歴が格納されたJSON配列を取得する。 120 | history = req.get_json() 121 | 122 | # [{question: "こんにちは。げんきですか?", answer: "元気です。"}, {question: "今日の天気は?", answer: "晴れです。"}...]というJSON配列から 123 | # 最も末尾に格納されているJSONオブジェクトのquestionを取得する。 124 | question = history[-1].get('user') 125 | 126 | # Azure AI Seacheにセマンティックハイブリッド検索を行い、回答を生成する。 127 | answer = semantic_hybrid_search(question, history) 128 | 129 | return func.HttpResponse( 130 | json.dumps({"answer": answer}), 131 | mimetype="application/json", 132 | status_code=200 133 | ) 134 | 135 | def semantic_hybrid_search(query: str, history: list[dict]): 136 | """ 137 | セマンティックサーチとハイブリッドサーチを組み合わせて回答を生成する。 138 | """ 139 | 140 | # 利用するモデルからデプロイ名を取得する。 141 | gpt_deploy = gpt_models.get(gpt_model).get("deployment") 142 | 143 | # 利用するモデルの最大トークン数を取得する。 144 | max_tokens = gpt_models.get(gpt_model).get("max_tokens")*0.8 # ギリギリ際を攻めるとエラーが出るため、最大トークン数の80%のトークン数を指定する。 145 | 146 | # Azure OpenAI Serviceの埋め込み用APIを用いて、ユーザーからの質問をベクトル化する。 147 | # セマンティックハイブリッド検索に必要な「ベクトル化されたクエリ」「キーワード検索用クエリ」のうち、ベクトル化されたクエリを生成する。 148 | response = openai_client.embeddings.create( 149 | input = query, 150 | model = text_embedding_ada_002_deploy 151 | ) 152 | vector_query = VectorizedQuery(vector=response.data[0].embedding, k_nearest_neighbors=3, fields="contentVector") 153 | 154 | # ユーザーからの質問を元に、Azure AI Searchに投げる検索クエリを生成する。 155 | # セマンティックハイブリッド検索に必要な「ベクトル化されたクエリ」「キーワード検索用クエリ」のうち、検索クエリを生成する。 156 | messages_for_search_query = [] 157 | 158 | # 会話履歴の最後に、キーワード検索用クエリを生成するためのプロンプトを追加する。 159 | for h in history[:-1]: 160 | messages_for_search_query.append({"role": "user", "content": h["user"]}) 161 | messages_for_search_query.append({"role": "assistant", "content": h["assistant"]}) 162 | messages_for_search_query.append({"role": "user", "content": query_prompt_template.format(query=query)}) 163 | 164 | messages_for_search_query = trim_messages(messages_for_search_query, max_tokens) 165 | 166 | response = openai_client.chat.completions.create( 167 | model=gpt_deploy, 168 | messages=messages_for_search_query 169 | ) 170 | search_query = response.choices[0].message.content 171 | 172 | # 「ベクトル化されたクエリ」「キーワード検索用クエリ」を用いて、Azure AI Searchに対してセマンティックハイブリッド検索を行う。 173 | results = search_client.search(query_type='semantic', semantic_configuration_name='default', 174 | search_text=search_query, 175 | vector_queries=[vector_query], 176 | select=['id', 'content'], query_caption='extractive', query_answer="extractive", highlight_pre_tag='', highlight_post_tag='', top=2) 177 | 178 | # セマンティックアンサーを取得する。 179 | semantic_answers = results.get_answers() 180 | 181 | messages_for_semantic_answer = [] 182 | 183 | messages_for_semantic_answer.append({"role": "system", "content": system_message_chat_conversation}) 184 | 185 | # Azure OpenAI Serviceに回答を生成する際の会話履歴を生成する。まずは画面から渡された会話履歴を 186 | # Azure OpenAI Seriviceに渡す形式({"role": "XXX", "content": "XXX"})に変換する。 187 | for h in history[:-1]: 188 | messages_for_semantic_answer.append({"role": "user", "content": h["user"]}) 189 | messages_for_semantic_answer.append({"role": "assistant", "content": h["assistant"]}) 190 | 191 | # セマンティックアンサーの有無で返答を変える 192 | user_message = "" 193 | if len(semantic_answers) == 0: 194 | # Azure AI Searchがセマンティックアンサーを返さなかった場合は、 195 | # topで指定された複数のドキュメントを回答生成のための情報源として利用する。 196 | sources = ["[Source" + result["id"] + "]: " + result["content"] for result in results] 197 | source = "\n".join(sources) 198 | 199 | user_message = """ 200 | {query} 201 | 202 | Sources: 203 | {source} 204 | """.format(query=query, source=source) 205 | else: 206 | # Azure AI Searchがセマンティックアンサーを返した場合は、それを回答生成のための情報源として利用する。 207 | user_message = """ 208 | {query} 209 | 210 | Sources: 211 | {source} 212 | """.format(query=query, source=semantic_answers[0].text) 213 | 214 | messages_for_semantic_answer.append({"role": "user", "content": user_message}) 215 | 216 | # Azure OpenAI ServiceのAPIの仕様で定められたトークン数の制限に基づき、 217 | # 指定されたトークン数に従って、会話履歴を古い順から削除する。 218 | messages_for_semantic_answer = trim_messages(messages_for_semantic_answer, max_tokens) 219 | 220 | # Azure OpenAI Serviceに回答生成を依頼する。 221 | response = openai_client.chat.completions.create( 222 | model=gpt_deploy, 223 | messages=messages_for_semantic_answer 224 | ) 225 | response_text = response.choices[0].message.content 226 | 227 | # チャットログをCosmos DBに書き込む。 228 | write_chatlog("guest", query, response_text) 229 | 230 | return response_text 231 | 232 | def trim_messages(messages, max_tokens): 233 | """ 234 | 会話履歴の合計のトークン数が最大トークン数を超えないように、古いメッセージから削除する。 235 | """ 236 | 237 | # 利用するモデルからエンコーディングを取得する。 238 | encoding = gpt_models.get(gpt_model).get("encoding") 239 | 240 | # 各メッセージのトークン数を計算 241 | token_counts = [(message, len(encoding.encode(message["content"]))) for message in messages] 242 | total_tokens = sum(count for _, count in token_counts) 243 | 244 | # トークン数が最大トークン数を超えないように、古いメッセージから削除する 245 | # もし最大トークン数を超える場合は、systemメッセージ以外のメッセージを古い順から削除する。 246 | # この処理をトークン数が最大トークン数を下回るまで行う。 247 | while total_tokens > max_tokens: 248 | messages.pop(1) 249 | total_tokens -= token_counts.pop(1)[1] 250 | if total_tokens <= max_tokens: 251 | break 252 | 253 | return messages 254 | 255 | def write_chatlog(user_name: str, input: str, response: str): 256 | """ 257 | チャットログをCosmos DBに書き込む。 258 | """ 259 | properties = { 260 | "user" : user_name, 261 | "input" : input, 262 | "response" : response 263 | } 264 | 265 | container.create_item(body=properties, enable_automatic_id_generation=True) -------------------------------------------------------------------------------- /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 | // 各リソースのリソース名を定義する。ここで何も定義されていなければ、リソース名は自動生成される。 13 | param resourceGroupName string = '' // ここで指定されたリソースグループ名に各リソースが所属する 14 | param webServiceName string = '' // フロントエンドのアプリをデプロイするAzure Static Web Appsの名前 15 | param webServiceLocation string = 'eastus2' // フロントエンドのアプリをデプロイするAzure Static Web Appsのリージョン(特に指定がなければazd upコマンド実施の際に指定されたリージョンとなる) 16 | //param webServiceLocation string = location 17 | param apiServiceName string = '' // バックエンドのAPIをデプロイするAzure Functionsの名前 18 | param apiServiceLocation string = '' // バックエンドのAPIをデプロイするAzure Functionsのリージョン(特に指定がなければazd upコマンド実施の際に指定されたリージョンとなる) 19 | param storageServiceSkuName string = '' // ストレージアカウントのSKU名(特に指定がなければStandard_LRSとなる) 20 | param searchServiceLocation string = '' // Azure AI Searchのリージョン(特に指定がなければazd upコマンド実施の際に指定されたリージョンとなる) 21 | param searchServiceName string = '' // Azure AI Searchのリソース名 22 | param searchServiceSkuName string = '' // Azure AI SearchのSKU名(特に指定がなければstandardとなる) 23 | param formRecognizerServiceName string = '' // Document Intelligenceのリソース名 24 | param formRecognizerSkuName string = '' // Document IntelligenceのSKU名(特に指定がなければS0となる) 25 | param formRecognizerLocation string = '' // Document Intelligenceのリージョン(特に指定がなければazd upコマンド実施の際に指定されたリージョンとなる) 26 | param openAiServiceName string = '' // Azure OpenAI Serviceのリソース名 27 | param openAiSkuName string = '' // Azure OpenAI ServiceのSKU名(特に指定がなければS0となる) 28 | param openAiServiceLocation string= '' // Azure OpenAI Serviceのリージョン(特に指定がなければazd upコマンド実施の際に指定されたリージョンとなる) 29 | param openAiModelName string = '' // Azure OpenAI Serviceのモデル名(特に指定がなければgpt-35-turboとなる) 30 | param openAiGpt35TurboDeploymentName string = 'gpt-35-turbo-deploy' // Azure OpenAI Serviceのgpt-35-turboのデプロイメント名 31 | param openAiGpt4DeploymentName string = 'gpt-4-deploy' // Azure OpenAI Serviceのgpt-4のデプロイメント名 32 | param openAiGpt432kDeploymentName string = 'gpt-4-32k-deploy' // Azure OpenAI Serviceのgpt-4-32kのデプロイメント名 33 | param openAiTextEmbeddingAda002DeploymentName string = 'text-embedding-ada-002-deploy' // Azure OpenAI Serviceのtext-embedding-ada-002のデプロイメント名 34 | param openAiApiVersion string = '2023-12-01-preview' // Azure OpenAI ServiceのAPIバージョン 35 | param applicationInsightsServiceLocation string = '' // Application Insightsのリージョン(特に指定がなければazd upコマンド実施の際に指定されたリージョンとなる) 36 | param cosmosDbDatabaseName string = 'ChatHistory' // Cosmos DBのデータベース名 37 | param cosmosDbContainerName string = 'Prompts' // Cosmos DBのコンテナ名 38 | param cosmosDbLocation string = '' // Cosmos DBのリージョン(特に指定がなければazd upコマンド実施の際に指定されたリージョンとなる) 39 | 40 | // az cliでログインした際のユーザーのプリンシパルIDを指定する。 41 | // ローカルで動かす場合に、az cliでログインしているユーザーに権限を与えるために利用する。 42 | param principalId string = '' 43 | 44 | // リソース名などに利用する一意の識別子を生成する。 45 | var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) 46 | 47 | // タグを定義する。ここで定義されたタグは、すべてのリソースに付与される。 48 | var tags = { 'azd-env-name': environmentName } 49 | 50 | // Cloud Adoption Frameworkのリソース命名規則に準じた省略語を読み込む。 51 | var abbrs = loadJsonContent('./abbreviations.json') 52 | 53 | // リソースグループを作成する 54 | resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { 55 | name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' 56 | location: location 57 | tags: tags 58 | } 59 | 60 | 61 | // RAGのフロントエンドアプリをデプロイするためのAzure Static Web Appsを作成する。 62 | module web './modules/app/stapp.bicep' = { 63 | name: 'web' 64 | scope: rg 65 | params: { 66 | name: !empty(webServiceName) ? webServiceName : '${abbrs.webStaticSites}${resourceToken}' 67 | location: !empty(webServiceLocation) ? webServiceLocation : location 68 | // このタグは、Azure Devloper CLIのazd-service-nameタグを使って、リソースを特定するために使われる。 69 | // キーがazd-service-name、値がwebというタグの付与されているリソースにフロントエンドのアプリがデプロイされる。 70 | tags: union(tags, { 'azd-service-name': 'web' }) 71 | } 72 | } 73 | 74 | // RAGのバックエンドAPIをデプロイするためのAzure Functionsを作成する。 75 | module api './modules/app/func.bicep' = { 76 | name: 'api' 77 | scope: rg 78 | params: { 79 | name: !empty(apiServiceName) ? apiServiceName : '${abbrs.webSitesFunctions}${resourceToken}' 80 | location: !empty(apiServiceLocation) ? apiServiceLocation : location 81 | webServiceUri: web.outputs.uri 82 | appServicePlanId: appServicePlan.outputs.id 83 | // func.bicepでいろんなリソースのリソース情報を取得するために各リソースの名前を渡す。 84 | searchServiceName: searchService.outputs.name 85 | openAiServiceName: openAi.outputs.name 86 | openAiModelName: !empty(openAiModelName) ? openAiModelName : 'gpt-35-turbo' 87 | openAiApiVersion: openAiApiVersion 88 | openAiGpt35TurboDeploymentName: openAiGpt35TurboDeploymentName 89 | openAiGpt4DeploymentName: openAiGpt4DeploymentName 90 | openAiGpt432kDeploymentName: openAiGpt432kDeploymentName 91 | storageAccountName: storage.outputs.name 92 | cosmosDbAccountName: cosmosDb.outputs.name 93 | cosmosDbDatabaseName: cosmosDb.outputs.databaseName 94 | cosmosDbContainerName: cosmosDb.outputs.containerName 95 | applicationInsightsName: applicationInsights.outputs.name 96 | // このタグは、Azure Devloper CLIのazd-service-nameタグを使って、リソースを特定するために使われる。 97 | // キーがazd-service-name、値がapiというタグの付与されているリソースにバックエンドのAPIがデプロイされる。 98 | tags: union(tags, { 'azd-service-name': 'api' }) 99 | } 100 | } 101 | 102 | // Azure FunctionsのApp Service Planを作成する。 103 | module appServicePlan './modules/app/asp.bicep' = { 104 | name: 'appServicePlan' 105 | scope: rg 106 | params: { 107 | name: '${abbrs.webServerFarms}${resourceToken}' 108 | location: !empty(apiServiceLocation) ? apiServiceLocation : location 109 | 110 | tags: tags 111 | } 112 | } 113 | 114 | // Azure Functionsに必要なストレージアカウントを作成する。 115 | module storage './modules/storage/storage.bicep' = { 116 | name: 'storage' 117 | scope: rg 118 | params: { 119 | name: '${abbrs.storageStorageAccounts}${resourceToken}' 120 | location: !empty(searchServiceLocation) ? searchServiceLocation : location 121 | sku: { 122 | // このパラメータは、ストレージアカウントのSKU名を指定する。 123 | // 特に指定がなければStandard_LRSとなる。 124 | name: !empty(storageServiceSkuName) ? storageServiceSkuName : 'Standard_LRS' 125 | } 126 | tags: tags 127 | } 128 | } 129 | 130 | // Azure Functionsのログ出力に必要なApplication Insightsを作成する。 131 | module applicationInsights './modules/monitor/appi.bicep' = { 132 | name: 'appinsights' 133 | scope: rg 134 | params: { 135 | name: '${abbrs.insightsComponents}${resourceToken}' 136 | location: !empty(applicationInsightsServiceLocation) ? applicationInsightsServiceLocation : location 137 | tags: tags 138 | } 139 | } 140 | 141 | // Azure AI Searchを作成する。 142 | module searchService './modules/ai/srch.bicep' = { 143 | name: 'search-service' 144 | scope: rg 145 | params: { 146 | name: !empty(searchServiceName) ? searchServiceName : '${abbrs.searchSearchServices}${resourceToken}' 147 | location: !empty(searchServiceLocation) ? searchServiceLocation : location 148 | tags: tags 149 | authOptions: { 150 | aadOrApiKey: { 151 | aadAuthFailureMode: 'http401WithBearerChallenge' 152 | } 153 | } 154 | sku: { 155 | // このパラメータは、Azure AI SearchのSKU名を指定する。 156 | // 特に指定がなければstandardとなる。 157 | name: !empty(searchServiceSkuName) ? searchServiceSkuName : 'standard' 158 | } 159 | semanticSearch: 'free' 160 | } 161 | } 162 | 163 | // Document Intelligenceを作成する。 164 | module documentIntelligence './modules/ai/cognitiveservices.bicep' = { 165 | name: 'documentintelligence' 166 | scope: rg 167 | params: { 168 | name: !empty(formRecognizerServiceName) ? formRecognizerServiceName : '${abbrs.cognitiveServicesFormRecognizer}${resourceToken}' 169 | kind: 'FormRecognizer' 170 | location: !empty(formRecognizerLocation) ? formRecognizerLocation : location 171 | tags: tags 172 | sku: { 173 | // このパラメータは、Document IntelligenceのSKU名を指定する。 174 | // 特に指定がなければS0となる。 175 | name: !empty(formRecognizerSkuName) ? formRecognizerSkuName : 'S0' 176 | } 177 | } 178 | } 179 | 180 | // Azure OpenAI Serviceを作成する。 181 | module openAi './modules/ai/cognitiveservices.bicep' = { 182 | name: 'openai' 183 | scope: rg 184 | params: { 185 | name: !empty(openAiServiceName) ? openAiServiceName : '${abbrs.cognitiveServicesAccounts}${resourceToken}' 186 | kind: 'OpenAI' 187 | location: !empty(openAiServiceLocation) ? openAiServiceLocation : location 188 | tags: tags 189 | sku: { 190 | // このパラメータは、Azure OpenAI ServiceのSKU名を指定する。 191 | // 特に指定がなければS0となる。 192 | name: !empty(openAiSkuName) ? openAiSkuName : 'S0' 193 | } 194 | // Azure OpenAI Serviceのモデルをデプロイする。現時点では最も汎用的なモデルであるgpt-35-turboをデプロイする。 195 | // また、ベクトル検索に利用するためのtext-embedding-ada-002もデプロイする。 196 | deployments: [ 197 | { 198 | name: 'gpt-35-turbo-deploy' 199 | model: { 200 | format: 'OpenAI' 201 | name: 'gpt-35-turbo' 202 | version: '0613' 203 | } 204 | sku: { 205 | name: 'Standard' 206 | capacity: 80 207 | } 208 | } 209 | { 210 | name: 'gpt-4-deploy' 211 | model: { 212 | format: 'OpenAI' 213 | name: 'gpt-4' 214 | version: '0613' 215 | } 216 | sku: { 217 | name: 'Standard' 218 | capacity: 10 219 | } 220 | } 221 | { 222 | name: 'gpt-4-32k-deploy' 223 | model: { 224 | format: 'OpenAI' 225 | name: 'gpt-4-32k' 226 | version: '0613' 227 | } 228 | sku: { 229 | name: 'Standard' 230 | capacity: 20 231 | } 232 | } 233 | { 234 | name: 'text-embedding-ada-002-deploy' 235 | model: { 236 | format: 'OpenAI' 237 | name: 'text-embedding-ada-002' 238 | version: '2' 239 | } 240 | sku: { 241 | name: 'Standard' 242 | capacity: 80 243 | } 244 | } 245 | ] 246 | } 247 | } 248 | 249 | // Cosmos DBを作成する。 250 | module cosmosDb './modules/db/cosmosdb.bicep' = { 251 | name: 'cosmosdb' 252 | scope: rg 253 | params: { 254 | name: '${abbrs.documentDBDatabaseAccounts}${resourceToken}' 255 | location: !empty(cosmosDbLocation) ? cosmosDbLocation : location 256 | tags: union(tags, { 'azd-service-name': 'cosmosdb' }) 257 | cosmosDbDatabaseName: cosmosDbDatabaseName 258 | cosmosDbContainerName: cosmosDbContainerName 259 | } 260 | } 261 | 262 | // ローカルで動かす場合に、az cliでログインしているユーザーに権限を与えるためのロールを定義する。 263 | // Azure OpenAI ServiceのAPIを発行するために、データプレーンへのアクセス権を与える。 264 | module openAiRoleUser './modules/security/role.bicep' = { 265 | scope: rg 266 | name: 'openai-role-user' 267 | params: { 268 | principalId: principalId 269 | roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' 270 | principalType: 'User' 271 | } 272 | } 273 | 274 | // ローカルで動かす場合に、az cliでログインしているユーザーに権限を与えるためのロールを定義する。 275 | // Azure AI Searchのインデックスを閲覧するために、データプレーンへのアクセス権を与える。 276 | module searchRoleUser './modules/security/role.bicep' = { 277 | scope: rg 278 | name: 'search-role-user' 279 | params: { 280 | principalId: principalId 281 | roleDefinitionId: '1407120a-92aa-4202-b7e9-c0e197c71c8f' 282 | principalType: 'User' 283 | } 284 | } 285 | 286 | // ローカルで動かす場合に、az cliでログインしているユーザーに権限を与えるためのロールを定義する。 287 | // Document IntelligenceのAPIを発行するために、データプレーンへのアクセス権を与える。 288 | module formRecognizerRoleUser './modules/security/role.bicep' = { 289 | scope: rg 290 | name: 'formrecognizer-role-user' 291 | params: { 292 | principalId: principalId 293 | roleDefinitionId: 'a97b65f3-24c7-4388-baec-2e87135dc908' 294 | principalType: 'User' 295 | } 296 | } 297 | 298 | // ローカルで動かす場合に、az cliでログインしているユーザーに権限を与えるためのロールを定義する。 299 | // Azure AI Searchにインデックスを登録するために、データプレーンへのアクセス権を与える。 300 | // この権限はインデクサーで利用する。 301 | module searchContribRoleUser './modules/security/role.bicep' = { 302 | scope: rg 303 | name: 'search-contrib-role-user' 304 | params: { 305 | principalId: principalId 306 | roleDefinitionId: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' 307 | principalType: 'User' 308 | } 309 | } 310 | 311 | // Azure上で動かす場合に必要なマネージドIDに権限を与えるためのロールを定義する。 312 | // Azure OpenAI ServiceのAPIを発行するために、データプレーンへのアクセス権を与える。 313 | module openAiRoleBackend './modules/security/role.bicep' = { 314 | scope: rg 315 | name: 'openai-role-managed-identity' 316 | params: { 317 | principalId: api.outputs.identityPrincipalId 318 | roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' 319 | principalType: 'ServicePrincipal' 320 | } 321 | } 322 | 323 | // Azure上で動かす場合に必要なマネージドIDに権限を与えるためのロールを定義する。 324 | // Azure AI SearchのAPIを発行するために、データプレーンへのアクセス権を与える。 325 | module searchRoleBackend './modules/security/role.bicep' = { 326 | scope: rg 327 | name: 'search-role-managed-identity' 328 | params: { 329 | principalId: api.outputs.identityPrincipalId 330 | roleDefinitionId: '1407120a-92aa-4202-b7e9-c0e197c71c8f' 331 | principalType: 'ServicePrincipal' 332 | } 333 | } 334 | 335 | // 以降でoutputで定義される変数は環境変数として出力される。 336 | 337 | // フロントエンドのアプリをデプロイするときにAPIのエンドポイントを埋め込む必要がある。 338 | // そのため、APIのエンドポイントを出力する。 339 | var API_HOST = api.outputs.host 340 | output API_ENDPOINT string = 'https://${API_HOST}' 341 | 342 | // バックエンドのAPIやインデクサーで必要な各リソースのエンドポイントやデータベース名、コンテナ名などを出力する。 343 | output AOAI_ENDPOINT string = openAi.outputs.endpoint 344 | output SEARCH_SERVICE_ENDPOINT string = searchService.outputs.endpoint 345 | output COSMOSDB_ENDPOINT string = cosmosDb.outputs.endpoint 346 | output COSMOSDB_DATABASE string = cosmosDb.outputs.databaseName 347 | output COSMOSDB_CONTAINER string = cosmosDb.outputs.containerName 348 | output COSMOSDB_ACCOUNT string = cosmosDb.outputs.accountName 349 | output COSMOSDB_RESOURCE_GROUP string = rg.name 350 | output AOAI_MODEL string = !empty(openAiModelName) ? openAiModelName : 'gpt-4' 351 | output AOAI_GPT_35_TURBO_DEPLOYMENT string = openAiGpt35TurboDeploymentName 352 | output AOAI_GPT_4_DEPLOYMENT string = openAiGpt4DeploymentName 353 | output AOAI_GPT_4_32K_DEPLOYMENT string = openAiGpt432kDeploymentName 354 | output AOAI_TEXT_EMBEDDING_ADA_002_DEPLOYMENT string = openAiTextEmbeddingAda002DeploymentName 355 | output AOAI_API_VERSION string = openAiApiVersion 356 | output DOCUMENT_INTELLIGENCE_ENDPOINT string = documentIntelligence.outputs.endpoint 357 | 358 | // Azure Cosmos DBにAzure Functionsがアクセスするためのカスタムロールの作成が必要になる。 359 | // その作成したカスタムロールの権限の付与先となるマネージドIDを出力する。 360 | output BACKEND_IDENTITY_PRINCIPAL_ID string = api.outputs.identityPrincipalId 361 | --------------------------------------------------------------------------------