├── .azdo ├── azure-pipeline.yaml └── pr-ci.yaml ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE.md ├── README.ja.md ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── app ├── backend │ ├── .env.sample │ ├── app.py │ ├── approaches │ │ ├── approach.py │ │ └── chatreadretrieveread.py │ ├── chat_log │ │ ├── cosmosdb_logging.py │ │ └── temp_dev.py │ ├── requirements.txt │ ├── static │ │ ├── assets │ │ │ ├── github-8c68b25d.svg │ │ │ ├── index-51849b94.css │ │ │ ├── index-5316f52c.js │ │ │ └── index-5316f52c.js.map │ │ ├── favicon.svg │ │ └── index.html │ └── text.py └── frontend │ ├── .env.sample │ ├── .prettierrc.json │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ └── favicon.svg │ ├── src │ ├── Avatar.tsx │ ├── Bar.tsx │ ├── GraphService.ts │ ├── aadConfig.ts │ ├── api │ │ ├── api.ts │ │ ├── index.ts │ │ └── models.ts │ ├── assets │ │ └── github.svg │ ├── components │ │ ├── Answer │ │ │ ├── Answer.module.css │ │ │ ├── Answer.tsx │ │ │ ├── AnswerError.tsx │ │ │ ├── AnswerIcon.tsx │ │ │ ├── AnswerLoading.tsx │ │ │ ├── AnswerParser.tsx │ │ │ └── index.ts │ │ ├── ClearChatButton │ │ │ ├── ClearChatButton.module.css │ │ │ ├── ClearChatButton.tsx │ │ │ └── index.tsx │ │ ├── QuestionInput │ │ │ ├── QuestionInput.module.css │ │ │ ├── QuestionInput.tsx │ │ │ └── index.ts │ │ └── UserChatMessage │ │ │ ├── UserChatMessage.module.css │ │ │ ├── UserChatMessage.tsx │ │ │ ├── UserChatMessageIcon.tsx │ │ │ └── index.ts │ ├── context │ │ └── AppContext.tsx │ ├── index.css │ ├── index.tsx │ ├── pages │ │ ├── NoPage.tsx │ │ ├── chat │ │ │ ├── Chat.module.css │ │ │ └── Chat.tsx │ │ └── layout │ │ │ ├── Layout.module.css │ │ │ └── Layout.tsx │ ├── requirements.txt │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── yarn.lock └── docs ├── en ├── .gitkeep └── logging_cosmosdb.md ├── images ├── appcomponents.png ├── chatscreen.png └── network-control.png └── jp ├── ai-security-draft.md └── openai-ai-scale.md /.azdo/azure-pipeline.yaml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | trigger: 7 | - main 8 | 9 | pool: 10 | vmImage: ubuntu-latest 11 | 12 | steps: 13 | - script: echo Hello, world! 14 | displayName: 'Run a one-line script' 15 | 16 | - script: | 17 | echo Add other tasks to build, test, and deploy your project. 18 | echo See https://aka.ms/yaml 19 | displayName: 'Run a multi-line script' 20 | -------------------------------------------------------------------------------- /.azdo/pr-ci.yaml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - '*' 5 | paths: 6 | exclude: 7 | - 'README.md' 8 | - 'docs/*' 9 | 10 | pr: 11 | branches: 12 | include: 13 | - main 14 | 15 | jobs: 16 | - job: PrepEnv 17 | displayName: 'install dependency' 18 | - job: Build 19 | displayName: 'Build Job' 20 | steps: 21 | - task: Bash@3 22 | displayName: 'build frontend' 23 | inputs: 24 | targetType: 'inline' 25 | script: | 26 | ls 27 | pwd 28 | cd app/frontend/src 29 | npm install 30 | npm run build 31 | - task: Bash@3 32 | displayName: 'build backend' 33 | inputs: 34 | targetType: 'inline' 35 | script: | 36 | cd app/backend 37 | python -m venv .venv 38 | source .venv/bin/activate 39 | pip install -r requirements.txt 40 | python app.py & 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Install Python 2 | # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster 3 | ARG VARIANT=3.10-bullseye 4 | FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT} 5 | 6 | # Install Node.js 7 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 8 | ARG NODE_VERSION="16" 9 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 10 | 11 | # Install Azure CLI 12 | RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash 13 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenAI at Scale", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "context": "..", 6 | "args": { 7 | "VARIANT": "3.10-bullseye", 8 | // Options 9 | "NODE_VERSION": "16" 10 | } 11 | }, 12 | 13 | // Configure tool-specific properties. 14 | "customizations": { 15 | // Configure properties specific to VS Code. 16 | "vscode": { 17 | // Set *default* container specific settings.json values on container create. 18 | "settings": { 19 | "python.defaultInterpreterPath": "/usr/local/bin/python", 20 | "python.linting.enabled": true, 21 | "python.linting.pylintEnabled": true, 22 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", 23 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 24 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", 25 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", 26 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", 27 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", 28 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", 29 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", 30 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" 31 | }, 32 | 33 | // Add the IDs of extensions you want installed when the container is created. 34 | "extensions": [ 35 | "ms-python.python", 36 | "ms-python.vscode-pylance", 37 | "dbaeumer.vscode-eslint" 38 | ] 39 | } 40 | }, 41 | 42 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 43 | // "forwardPorts": [], 44 | 45 | // Use 'postCreateCommand' to run commands after the container is created. 46 | // "postCreateCommand": "pip3 install --user -r requirements.txt", 47 | 48 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 49 | "remoteUser": "vscode" 50 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | #-------------------------------------- 133 | # Node related ignore conf 134 | #-------------------------------------- 135 | # Logs 136 | logs 137 | *.log 138 | npm-debug.log* 139 | yarn-debug.log* 140 | yarn-error.log* 141 | lerna-debug.log* 142 | .pnpm-debug.log* 143 | 144 | # Diagnostic reports (https://nodejs.org/api/report.html) 145 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 146 | 147 | # Runtime data 148 | pids 149 | *.pid 150 | *.seed 151 | *.pid.lock 152 | 153 | # Directory for instrumented libs generated by jscoverage/JSCover 154 | lib-cov 155 | 156 | # Coverage directory used by tools like istanbul 157 | coverage 158 | *.lcov 159 | 160 | # nyc test coverage 161 | .nyc_output 162 | 163 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 164 | .grunt 165 | 166 | # Bower dependency directory (https://bower.io/) 167 | bower_components 168 | 169 | # node-waf configuration 170 | .lock-wscript 171 | 172 | # Compiled binary addons (https://nodejs.org/api/addons.html) 173 | build/Release 174 | 175 | # Dependency directories 176 | node_modules/ 177 | jspm_packages/ 178 | 179 | # Snowpack dependency directory (https://snowpack.dev/) 180 | web_modules/ 181 | 182 | # TypeScript cache 183 | *.tsbuildinfo 184 | 185 | # Optional npm cache directory 186 | .npm 187 | 188 | # Optional eslint cache 189 | .eslintcache 190 | 191 | # Optional stylelint cache 192 | .stylelintcache 193 | 194 | # Microbundle cache 195 | .rpt2_cache/ 196 | .rts2_cache_cjs/ 197 | .rts2_cache_es/ 198 | .rts2_cache_umd/ 199 | 200 | # Optional REPL history 201 | .node_repl_history 202 | 203 | # Output of 'npm pack' 204 | *.tgz 205 | 206 | # Yarn Integrity file 207 | .yarn-integrity 208 | 209 | # dotenv environment variable files 210 | .env 211 | .env.development.local 212 | .env.test.local 213 | .env.production.local 214 | .env.local 215 | 216 | # parcel-bundler cache (https://parceljs.org/) 217 | .cache 218 | .parcel-cache 219 | 220 | # Next.js build output 221 | .next 222 | out 223 | 224 | # Nuxt.js build / generate output 225 | .nuxt 226 | dist 227 | 228 | # Gatsby files 229 | .cache/ 230 | # Comment in the public line in if your project uses Gatsby and not Next.js 231 | # https://nextjs.org/blog/next-9-1#public-directory-support 232 | # public 233 | 234 | # vuepress build output 235 | .vuepress/dist 236 | 237 | # vuepress v2.x temp and cache directory 238 | .temp 239 | .cache 240 | 241 | # Docusaurus cache and generated files 242 | .docusaurus 243 | 244 | # Serverless directories 245 | .serverless/ 246 | 247 | # FuseBox cache 248 | .fusebox/ 249 | 250 | # DynamoDB Local files 251 | .dynamodb/ 252 | 253 | # TernJS port file 254 | .tern-port 255 | 256 | # Stores VSCode versions used for testing VSCode extensions 257 | .vscode-test 258 | 259 | # yarn v2 260 | .yarn/cache 261 | .yarn/unplugged 262 | .yarn/build-state.yml 263 | .yarn/install-state.gz 264 | .pnp.* 265 | 266 | 267 | #-------------------------------------- 268 | # Node related ignore conf 269 | #-------------------------------------- 270 | # Logs 271 | logs 272 | *.log 273 | npm-debug.log* 274 | yarn-debug.log* 275 | yarn-error.log* 276 | lerna-debug.log* 277 | .pnpm-debug.log* 278 | 279 | # Diagnostic reports (https://nodejs.org/api/report.html) 280 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 281 | 282 | # Runtime data 283 | pids 284 | *.pid 285 | *.seed 286 | *.pid.lock 287 | 288 | # Directory for instrumented libs generated by jscoverage/JSCover 289 | lib-cov 290 | 291 | # Coverage directory used by tools like istanbul 292 | coverage 293 | *.lcov 294 | 295 | # nyc test coverage 296 | .nyc_output 297 | 298 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 299 | .grunt 300 | 301 | # Bower dependency directory (https://bower.io/) 302 | bower_components 303 | 304 | # node-waf configuration 305 | .lock-wscript 306 | 307 | # Compiled binary addons (https://nodejs.org/api/addons.html) 308 | build/Release 309 | 310 | # Dependency directories 311 | node_modules/ 312 | jspm_packages/ 313 | 314 | # Snowpack dependency directory (https://snowpack.dev/) 315 | web_modules/ 316 | 317 | # TypeScript cache 318 | *.tsbuildinfo 319 | 320 | # Optional npm cache directory 321 | .npm 322 | 323 | # Optional eslint cache 324 | .eslintcache 325 | 326 | # Optional stylelint cache 327 | .stylelintcache 328 | 329 | # Microbundle cache 330 | .rpt2_cache/ 331 | .rts2_cache_cjs/ 332 | .rts2_cache_es/ 333 | .rts2_cache_umd/ 334 | 335 | # Optional REPL history 336 | .node_repl_history 337 | 338 | # Output of 'npm pack' 339 | *.tgz 340 | 341 | # Yarn Integrity file 342 | .yarn-integrity 343 | 344 | # dotenv environment variable files 345 | .env 346 | .env.development.local 347 | .env.test.local 348 | .env.production.local 349 | .env.local 350 | 351 | # parcel-bundler cache (https://parceljs.org/) 352 | .cache 353 | .parcel-cache 354 | 355 | # Next.js build output 356 | .next 357 | out 358 | 359 | # Nuxt.js build / generate output 360 | .nuxt 361 | dist 362 | 363 | # Gatsby files 364 | .cache/ 365 | # Comment in the public line in if your project uses Gatsby and not Next.js 366 | # https://nextjs.org/blog/next-9-1#public-directory-support 367 | # public 368 | 369 | # vuepress build output 370 | .vuepress/dist 371 | 372 | # vuepress v2.x temp and cache directory 373 | .temp 374 | .cache 375 | 376 | # Docusaurus cache and generated files 377 | .docusaurus 378 | 379 | # Serverless directories 380 | .serverless/ 381 | 382 | # FuseBox cache 383 | .fusebox/ 384 | 385 | # DynamoDB Local files 386 | .dynamodb/ 387 | 388 | # TernJS port file 389 | .tern-port 390 | 391 | # Stores VSCode versions used for testing VSCode extensions 392 | .vscode-test 393 | 394 | # yarn v2 395 | .yarn/cache 396 | .yarn/unplugged 397 | .yarn/build-state.yml 398 | .yarn/install-state.gz 399 | .pnp.* 400 | 401 | .azure/* 402 | .azure 403 | 404 | 405 | # Azure az webapp deployment details 406 | .azure 407 | *_env 408 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to OpenAI at Scale 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/Azure/openai-at-scale/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/Azure/openai-at-scale/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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /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 22 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # OpenAI at Scale 2 | 3 | [](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=622759641&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestUs2&skip_quickstart=true&geo=SoutheastAsia) 4 | [](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/Azure/openai-at-scale) 5 | 6 | 🌏 [English](README.md) | 日本語 7 | 8 | ✨ **OpenAI at Scale** は Microsoft 社の FastTrack for Azure チームによるワークショップです。Azure 上にシンプルは GhatGPT のインターフェースを持つアプリケーションをビルドしデプロイします。 9 | 10 | 11 | 12 | 13 | 14 | ## 🚀 ハンズオンセッション 15 | 16 | ハンズオンセッションの資料はこちらです。 17 | 👉 [セッション資料](./docs/jp/openai-ai-scale.md) 📖 18 | 19 | 20 | ## 🎯 機能 21 | 22 | - チャットインタフェース 23 | - システムプロンプトとハイパーパラメータの設定 24 | - Azure Active Directory による認証と Microsoft Graph からのユーザー情報の取得 25 | - Azure Log Analytics によるアプリケーションログの取得 26 | - プロンプトログの Azure Cosmos DB への格納 27 | 28 | 29 | 30 | 31 | ## 🙋🏾♂️ 質問やフィードバック 32 | 33 | [GitHub Issues](https://github.com/Azure/openai-at-scale/issues) でこのリポジトリに関する質問やフィードバックをお寄せください。 34 | 35 | 36 | ## 📚 参考情報 37 | 38 | - [ChatGPT + Enterprise data with Azure OpenAI and Cognitive Search](https://github.com/Azure-Samples/azure-search-openai-demo) 39 | - This repo is based on this sample code. 40 | 41 | ## 🤝 貢献 42 | 43 | お客様や Microsoft 社員からの貢献を歓迎しています。[CONTRIBUTING](./CONTRIBUTING.md) を参照してください。このリポジトリの発展に貢献いただく全ての方に感謝します! 44 | 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAI at Scale 2 | 3 | [](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=622759641&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestUs2&skip_quickstart=true&geo=SoutheastAsia) 4 | [](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/Azure/openai-at-scale) 5 | 6 | 🌏 English | [日本語](README.ja.md) 7 | 8 | ✨ **OpenAI at Scale** is a workshop by **FastTrack for Azure in Microsoft** team that helps customers to build and deploy simple ChatGPT UI application on Azure. 9 | 10 | 11 | 12 | --- 13 | 14 | ## 🎯 Features 15 | 16 | - Chat UI 17 | - Configure system prompts and hyperparameters 18 | - Authenticate with Azure Active Directory and get user information from Microsoft Graph 19 | - Collect application logs with Azure Log Analytics 20 | - Store prompt log data to Azure Cosmos DB 21 | 22 | 23 | 24 | --- 25 | 26 | ## 🚀 Getting Started 27 | 28 | ### ⚒️ Prerequisites 29 | 30 | #### To run locally 31 | 32 | - OS - Windows 11, MacOS or Linux 33 | > ⚠ For Windows client user, please use Ubuntu 20.04 LTS (Windows subsystem for Linux) to run this application. 34 | > ⚠ GitHub Codespaces is supported as Linux envionment. 35 | 36 | - [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) (v4.28.1 or higher) 37 | - [Node.js](https://nodejs.org/en/download/) (v16.20 or higher) 38 | - [Python](https://www.python.org/downloads/) (v3.9 or higher) 39 | - [git client](https://git-scm.com/downloads) 40 | - [Docker Desktop](https://www.docker.com/products/docker-desktop) or any other Docker environment 41 | - Docker is used for Visual Studio Code Dev Container. 42 | 43 | 44 | #### To run on Azure 45 | 46 | - Azure subscription 47 | - Resources 48 | - Azure OpenAI Service 49 | - Azure Active Directory application 50 | - Azure Log Analytics 51 | - (Optional) Azure Cosmos DB 52 | > ⚠ Free account is not supported 53 | - Role 54 | - Contributor role or higher for Azure subscription 55 | - Permission to create Azure Active Directory application 56 | - Permission to create Azure OpenAI Service 57 | 58 | 59 | 60 | ### 1. Creating Azure OpenAI Service 🧠 61 | 62 | There are some ways to create Azure OpenAI Service, we recommend to use Azure Portal if you are not familiar with Azure CLI. 63 | 64 | Before started you need to choose a location for your service. You can find the list of available locations here -> supported location is here : [Supported locations](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/?regions=all&products=cognitive-services) 65 | 66 | - **Azure Portal** : 67 | You can follow the [official document](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal) to create Azure OpenAI Service. 68 | - **Azure CLI** : You can use the following command to create Azure OpenAI Service. 69 | 70 | command example 71 | 72 | ```shell 73 | # Set environment variables 74 | export SUBSCRIPTION_ID= 75 | export RESOURCE_GROUP= 76 | export LOCATION=eastus #hard coded to avoid confusion, you can change any avaiable location. 77 | export AZURE_OPENAI_SERVICE= 78 | export AZURE_OPENAI_CHATGPT_DEPLOYMENT= 79 | ``` 80 | 81 | ```shell 82 | az login #check your subscription id 83 | az account set --subscription $SUBSCRIPTION_ID 84 | az group create --name $RESOURCE_GROUP --location $LOCATION 85 | az cognitiveservices account create \ 86 | --name $AZURE_OPENAI_SERVICE \ 87 | --kind OpenAI \ 88 | --sku S0 \ 89 | --resource-group $RESOURCE_GROUP \ 90 | --location $LOCATION \ 91 | --yes 92 | az cognitiveservices account deployment create \ 93 | -g $RESOURCE_GROUP \ 94 | -n $AZURE_OPENAI_SERVICE \ 95 | --deployment-name $AZURE_OPENAI_CHATGPT_DEPLOYMENT \ 96 | --model-name gpt-35-turbo \ 97 | --model-version "0301" \ 98 | --model-format OpenAI \ 99 | --scale-settings-scale-type "Standard" 100 | ``` 101 | 102 | 103 | 104 | 105 | 106 | ### 2. Creating Azure Active Directory application 🔑 107 | 108 | Follow the steps in [register your application](https://learn.microsoft.com/azure/active-directory/develop/quickstart-register-app) to register your application. 109 | 110 | - Select **`Single-page application (SPA)`** as platform type 111 | - The Redirect URI will be **`http://localhost:5000`** and **`http://localhost:5173`** for local development 112 | - Keep the **`Application (client) ID`** and **`Directory (tenant) ID`** for later use 113 | 114 | 115 | 116 | ### (Optional) 3. Creating Azure Cosmos DB 🪐 117 | 118 | You can create a Azure Cosmos DB account by following instructions on the Azure docs [here](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/quickstart-portal) and please make sure you enable Analytical Store, more details can be found [here](https://learn.microsoft.com/en-us/azure/cosmos-db/analytical-store-introduction). 119 | 120 | - Select **`Core (SQL)`** as API 121 | - Container name will be **`chat_log`** and partition key will be **`/chat_session_id`** 122 | 123 | 124 | 125 | ### 4. Deploying to local environment 💻 126 | 127 | 128 | #### Environment variables 129 | 130 | You need to create `.env` files from `.env.sample` to set up your environment variables before you spin up your application. 131 | 132 | - `app/frontend/.env` 133 | - This file will be used for authentication function by Azure Active Directory SDK. 134 | 135 | ```shell 136 | # Azure Active Directory application 137 | VITE_CLIENTID="" 138 | VITE_TENANTID="" 139 | ``` 140 | 141 | - `app/backend/.env` 142 | - This file will be used for accessing to Azure OpenAI Service and Azure Cosmos DB. 143 | 144 | ```shell 145 | # Azure OpenAI Service 146 | AZURE_OPENAI_SERVICE="" 147 | OPENAI_API_KEY="" 148 | AZURE_OPENAI_CHATGPT_DEPLOYMENT="" 149 | 150 | 151 | # (Optional) Azure Cosmos DB 152 | AZURE_COSMOSDB_ENDPOINT="https://.documents.azure.com:443/" 153 | AZURE_COSMOSDB_KEY="" 154 | AZURE_COSMOSDB_DB="< your Azure Cosmos DB database name>" 155 | ``` 156 | > ⚠ Please use [Azure Key Vault](https://azure.microsoft.com/en-US/products/key-vault) to configure environment variables in production environments. 157 | 158 | 159 | command examples to get environment variables from Azure CLI. 160 | 161 | ```shell 162 | export RESOURCE_GROUP= 163 | export AZURE_OPENAI_SERVICE= 164 | export AZURE_OPENAI_CHATGPT_DEPLOYMENT= 165 | export OPENAI_API_KEY=`az cognitiveservices account keys list \ 166 | -n $AZURE_OPENAI_SERVICE \ 167 | -g $RESOURCE_GROUP \ 168 | -o json \ 169 | | jq -r .key1` 170 | ``` 171 | 172 | 173 | 174 | #### Python environment 175 | 176 | Python is required to run the backend Flask application. 177 | 178 | ##### Install Python libraries 179 | 180 | ```shell 181 | cd app/backend 182 | python -m venv ./backend_env 183 | source .backend_env/bin/activate #bash 184 | pip install -r requirements.txt 185 | ``` 186 | 187 | ##### Start Backend (Flask) 188 | 189 | ```shell 190 | cd app/backend 191 | flask run --debug #hot reload 192 | #python ./app.py 193 | ``` 194 | 195 | #### Node.js environment 196 | 197 | Node.js is required to run the frontend React application. 198 | 199 | ##### Install Node.js packages 200 | 201 | ```shell 202 | cd app/frontend 203 | npm install 204 | ``` 205 | 206 | ##### Start Frontend (React) 207 | For development 208 | 209 | ```shell 210 | npm run dev 211 | ``` 212 | 213 | For production 214 | 215 | ```shell 216 | npm run build 217 | ``` 218 | > It is used to optimize and reduce the size of all application files which are deployed in app/backend/static folder. 219 | 220 | 221 | 222 | ### 5. Deploying to Azure ☁️ 223 | 224 | #### Deploy to Azure App Service 225 | 226 | > ⚠ Before you run following command, you must run `npm run build` on app/frontend to set frontend files to backend static dir. 227 | 228 | - Example of Azure App Service 229 | - Deploy app to Azure App Service with easist way. 230 | 231 | ```shell 232 | cd app/backend 233 | az webapp up --runtime "python:3.10" --sku B1 -g 234 | ``` 235 | 236 | - Deploy Azure App Service Plan and Web App separately. 237 | - You can deploy an app with above command but the command doesn't allow to change detailed App Service Plan and Web App settings. So if you want to change these settings you can deploy it separately with following command. 238 | - Create Azure App Service Plan resources 239 | 240 | ```shell 241 | az appservice plan create -g --is-linux -n --sku --location eastus 242 | ``` 243 | 244 | - Create WebApp Resource on above App Service Plan 245 | 246 | ```shell 247 | az webapp create -g -n -p -r "python:3.10" 248 | ``` 249 | 250 | ⚡️completely optional: if your system needs to add private endpoint and/or VNET integration, you can add it here with following options. 251 | 252 | - VNET Integration 253 | 254 | ```shell 255 | # you need create vnet/subnet before execute this command 256 | az webapp create -g -n -p -r "python:3.10" --vnet --subnet 257 | ``` 258 | 259 | - Private Endpoint 260 | 261 | ```shell 262 | # you need create vnet/subnet webapp before execute this command 263 | az network private-endpoint create \ 264 | -n \ 265 | -g \ 266 | --vnet-name \ 267 | --subnet \ 268 | --connection-name \ 269 | --private-connection-resource-id /subscriptions/SubscriptionID/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/ \ 270 | --group-id sites 271 | ``` 272 | 273 | - Update redirectURI at aadConfig.ts and rebuild frontend app 274 | - Update redirectURI with following FQDN, which is webapp endpoint. 275 | 276 | ```shell 277 | az webapp config hostname list -g --webapp-name -o json | jq '.[0].name' 278 | ``` 279 | 280 | - Rebuild frontend 281 | 282 | ```shell 283 | cd app/frontend 284 | npm run build 285 | ``` 286 | 287 | - Before deployed webapp, you must change the environment variable with application settings of Azure App Service. 288 | 289 | ```shell 290 | az webapp config appsettings set --name -g --settings SCM_DO_BUILD_DURING_DEPLOYMENT="true" 291 | ``` 292 | 293 | 294 | - Deploy demo app to WebApp 295 | 296 | ```shell 297 | cd app/backend 298 | zip -r deploy.zip . 299 | az webapp deploy -g -n --src-path deploy.zip --type zip 300 | ``` 301 | 302 | - After deployed webapp, you must change the environment variables with application settings of Azure App Service. 303 | 304 | ```shell 305 | az webapp config appsettings set --name -g --settings OPENAI_API_KEY= AZURE_OPENAI_CHATGPT_DEPLOYMENT= AZURE_OPENAI_SERVICE= 306 | ``` 307 | 308 | 309 | 310 | ### 6. Configuration ⚙️ 311 | #### Collect application logs with Azure Log Analytics 312 | 313 | - Example of Log collection 314 | - Deploy Azure Log Analytics workspace 315 | 316 | ```shell 317 | export APP_SERIVCE= 318 | export LOCATION= 319 | export RESOURCE_GROUP= 320 | export WORKSPACE= 321 | export DIAGSETTINNG_NAME= 322 | 323 | az monitor log-analytics workspace create --name $WORKSPACE --resource-group $RESOURCE_GROUP --location $LOCATION 324 | ``` 325 | 326 | - Enable diagnostics setting 327 | 328 | ```shell 329 | export RESOURCE_ID=`az webapp show -g $RESOURCE_GROUP -n $APP_SERIVCE --query id --output tsv | tr -d '\r'` 330 | export WORKSPACE_ID=`az monitor log-analytics workspace show -g $RESOURCE_GROUP --workspace-name $WORKSPACE --query id --output tsv | tr -d '\r'` 331 | 332 | az monitor diagnostic-settings create \ 333 | --resource $RESOURCE_ID \ 334 | --workspace $WORKSPACE_ID \ 335 | -n $DIAGSETTINNG_NAME \ 336 | --logs '[{"category": "AppServiceAppLogs", "enabled": true},{"category": "AppServicePlatformLogs", "enabled": true},{"category": "AppServiceConsoleLogs", "enabled": true},{"category": "AppServiceAuditLogs", "enabled": true},{"category": "AppServiceHTTPLogs", "enabled": true}]' 337 | ``` 338 | 339 | 340 | #### (Optional) Store prompt log data to Azure Cosmos DB 341 | The [logging chat on Azure Cosmos DB](docs/en/logging_cosmosdb.md) section explains in detail on how chat messages can be logged into Azure Cosmos DB and used in deriving insights further downstream. 342 | 343 | 344 | --- 345 | ## 🙋🏾♂️Question and Feedback 346 | 347 | You can ask question and feedback about this repo on [GitHub Issues](https://github.com/Azure/openai-at-scale/issues). 348 | 349 | --- 350 | ## 📚 Resources 351 | 352 | - [ChatGPT + Enterprise data with Azure OpenAI and Cognitive Search](https://github.com/Azure-Samples/azure-search-openai-demo) 353 | - This repo is based on this sample code. 354 | 355 | --- 356 | ## 🤝 Contributing 357 | We are welcome your contribution from customers and internal Microsoft employees. Please see [CONTRIBUTING](./CONTRIBUTING.md). We appreciate all contributors to make this repo thrive! 358 | 359 | 360 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | For help and questions about using this product, please join the discussion on existing 10 | [issues](https://github.com/azure/azure-sdk-for-rust/issues) or create a new issue. 11 | 12 | ## Microsoft Support Policy 13 | 14 | Support for this project is limited to the resources listed above. 15 | -------------------------------------------------------------------------------- /app/backend/.env.sample: -------------------------------------------------------------------------------- 1 | # Azure OpenAI Service 2 | AZURE_OPENAI_SERVICE="" 3 | OPENAI_API_KEY="" 4 | AZURE_OPENAI_CHATGPT_DEPLOYMENT="" 5 | 6 | # (Optional) Azure Cosmos DB 7 | # AZURE_COSMOSDB_ENDPOINT="https://.documents.azure.com:443/" 8 | # AZURE_COSMOSDB_KEY="" 9 | # AZURE_COSMOSDB_DB="< your Azure Cosmos DB database name>" 10 | -------------------------------------------------------------------------------- /app/backend/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import openai 4 | from flask import Flask, request, jsonify 5 | from azure.identity import DefaultAzureCredential 6 | from approaches.chatreadretrieveread import ChatReadRetrieveReadApproach 7 | from dotenv import load_dotenv 8 | 9 | env_path = os.path.join(os.path.dirname(__file__), '.env') 10 | load_dotenv(env_path, verbose=True, override=True) 11 | 12 | # Replace these with your own values, either in environment variables or directly here 13 | AZURE_OPENAI_SERVICE = os.environ.get("AZURE_OPENAI_SERVICE") or "myopenai" 14 | AZURE_OPENAI_GPT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_GPT_DEPLOYMENT") or "davinci" 15 | AZURE_OPENAI_CHATGPT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_CHATGPT_DEPLOYMENT") or "chat" 16 | 17 | # Used by the OpenAI SDK 18 | openai.api_type = "azure" 19 | openai.api_base = f"https://{AZURE_OPENAI_SERVICE}.openai.azure.com" 20 | openai.api_version = "2023-03-15-preview" 21 | openai.api_key = os.getenv("OPENAI_API_KEY") 22 | 23 | chat_approaches = { 24 | "rrr": ChatReadRetrieveReadApproach(AZURE_OPENAI_CHATGPT_DEPLOYMENT, AZURE_OPENAI_GPT_DEPLOYMENT) 25 | } 26 | 27 | app = Flask(__name__) 28 | 29 | @app.route("/", defaults={"path": "index.html"}) 30 | @app.route("/") 31 | def static_file(path): 32 | print(path) 33 | return app.send_static_file(path) 34 | 35 | @app.route("/chat", methods=["POST"]) 36 | def chat(): 37 | 38 | # ensure_openai_token() 39 | approach = request.json["approach"] 40 | try: 41 | impl = chat_approaches.get(approach) 42 | if not impl: 43 | return jsonify({"error": "unknown approach"}), 400 44 | r = impl.run(request.json["history"], request.json.get("overrides") or {}, request.json.get("sessionConfig") or {}, request.json.get("userInfo") or {}, dict(request.headers) or {}) 45 | return jsonify(r) 46 | 47 | except Exception as e: 48 | logging.exception("Exception in /chat") 49 | return jsonify({"error": str(e)}), 500 50 | 51 | 52 | if __name__ == "__main__": 53 | app.run() 54 | -------------------------------------------------------------------------------- /app/backend/approaches/approach.py: -------------------------------------------------------------------------------- 1 | class Approach: 2 | def run(self, q: str, use_summaries: bool) -> any: 3 | raise NotImplementedError 4 | -------------------------------------------------------------------------------- /app/backend/approaches/chatreadretrieveread.py: -------------------------------------------------------------------------------- 1 | import openai 2 | import uuid 3 | from approaches.approach import Approach 4 | import chat_log.cosmosdb_logging as cosmosdb_logging 5 | 6 | class ChatReadRetrieveReadApproach(Approach): 7 | 8 | # def __init__(self, chatgpt_deployment: str, gpt_deployment: str, sourcepage_field: str, content_field: str): 9 | def __init__(self, chatgpt_deployment: str, gpt_deployment: str): 10 | self.chatgpt_deployment = chatgpt_deployment 11 | self.gpt_deployment = gpt_deployment 12 | 13 | def run(self, history: list[dict], overrides: dict, sessionConfig: dict, userInfo:dict, header: dict) -> any: 14 | request_uuid=uuid.uuid4() 15 | print("requestID:",request_uuid,",history:", history) 16 | print("requestID:",request_uuid,",override:", overrides) 17 | print("requestID:",request_uuid,",sessionConfig:", sessionConfig) 18 | print("requestID:",request_uuid,",userInfo:", userInfo) 19 | top_p = overrides.get("top") or 0.95 20 | temperature = overrides.get("temperature") or 0.7 21 | max_tokens = overrides.get("maxResponse") or 800 22 | promptSystemTemplate = overrides.get("prompt_system_template") or "You are an AI assistant that helps people find information." 23 | pastMessages = sessionConfig.get("pastMessages") or 10 24 | user_name= userInfo.get("username") or "anonymous user_name" 25 | user_id = userInfo.get("email") or "anonymous user_id" 26 | chat_session_id = header.get("Sessionid") or "anonymous-" + str(uuid.uuid4()) 27 | print("user:", {"name": user_name,"user_id":user_id} ) # For Azure Log Analytics 28 | print("parameters:", {"Max Response": max_tokens, "Temperature": temperature, "Top P": top_p, "Past message included": pastMessages}) 29 | 30 | # Step 31 | system_prompt_template = {} 32 | system_prompt_template["role"] = "system" 33 | system_prompt_template["content"] = promptSystemTemplate 34 | print("prompt:",[system_prompt_template]+self.get_chat_history_as_text(history, pastMessages)) # For Azure Log Analytics 35 | 36 | completion = openai.ChatCompletion.create( 37 | engine=self.chatgpt_deployment, 38 | messages = [system_prompt_template]+self.get_chat_history_as_text(history, pastMessages), 39 | temperature=temperature, 40 | max_tokens=max_tokens, 41 | top_p=top_p, 42 | frequency_penalty=0, 43 | presence_penalty=0, 44 | stop=None) 45 | print("completion: ", completion) # For Azure Log Analytics 46 | 47 | document_definition = { "id": str(uuid.uuid4()), 48 | "chat_session_id": chat_session_id, 49 | "user": {"name": user_name,"user_id":user_id}, 50 | 'message': {"id":completion.get("id") or "anonymous-id", 51 | "prompt":[system_prompt_template]+self.get_chat_history_as_text(history, pastMessages), 52 | "other_attr":[{"completion": completion}], 53 | "previous_message_id":"previous_message_id"}} 54 | 55 | #cosmosdb_logging.insert_chat_log(document_definition) # Store prompt log data into Azure Cosmos DB 56 | return {"answer": completion.choices[0].message["content"]} 57 | 58 | 59 | def get_chat_history_as_text(self, history, pastMessages) -> list: 60 | history_text = [] 61 | for h in history: 62 | user_text = {} 63 | user_text["role"] = "user" 64 | user_text["content"] = h["user"] 65 | 66 | if h.get("bot") is None: 67 | history_text = history_text + [user_text] 68 | else: 69 | bot_text = {} 70 | bot_text["role"] = "assistant" 71 | bot_text["content"] = h.get("bot") 72 | history_text = history_text + [user_text, bot_text] 73 | return history_text[-(pastMessages+1):] 74 | -------------------------------------------------------------------------------- /app/backend/chat_log/cosmosdb_logging.py: -------------------------------------------------------------------------------- 1 | import azure.cosmos.cosmos_client as cosmos_client 2 | import azure.cosmos.errors as errors 3 | import azure.cosmos.exceptions as cosmos_exception 4 | import os 5 | import json 6 | from dotenv import load_dotenv 7 | 8 | ## Read environment variables 9 | env_path = os.path.join(os.path.dirname(__file__), '../.env') 10 | load_dotenv(env_path, verbose=True, override=True) 11 | 12 | endpoint = os.environ.get("AZURE_COSMOSDB_ENDPOINT") or None 13 | key = os.environ.get("AZURE_COSMOSDB_KEY") or None 14 | database_name = os.environ.get("AZURE_COSMOSDB_DB") or None 15 | 16 | # Replace with your own values if you are changing the default document structure 17 | 18 | container_name = 'chat_log' 19 | partition_key = '/chat_session_id' 20 | 21 | class chat_log_result: 22 | def __init__(self,is_err,err_msg): 23 | self.is_err = is_err 24 | self.err_msg = err_msg 25 | 26 | 27 | def insert_chat_log(chat_message) -> chat_log_result: 28 | chat_log_inserted = False 29 | err_message = "" 30 | if endpoint is not None and key is not None and database_name is not None: 31 | try: 32 | client = cosmos_client.CosmosClient(endpoint, {'masterKey': key}) 33 | database = client.create_database_if_not_exists(database_name) 34 | container = database.create_container_if_not_exists(id=container_name,partition_key=partition_key,default_ttl=3600,offer_throughput=400) 35 | 36 | ## Ensure the message has partition key , chat_session_id and user_id 37 | try: 38 | if chat_message["chat_session_id"] is not None and len(str(chat_message["chat_session_id"])) > 0: 39 | document = container.create_item(body=chat_message) 40 | 41 | 42 | except cosmos_exception.CosmosHttpResponseError as err: 43 | print(err.message) 44 | chat_log_inserted = True 45 | err_message = err_message + err.message + " ::: " 46 | except: 47 | msg = "Missing partition key: chat_session_id or invalid message structure" + json.loads(chat_message) 48 | chat_log_inserted = True 49 | print(msg) 50 | err_message = err_message + msg + " ::: " 51 | 52 | 53 | except cosmos_exception.CosmosHttpResponseError as err: 54 | err_message = err_message + err.message + " ::: " 55 | chat_log_inserted = True 56 | print(err.message) 57 | 58 | else: 59 | msg = "Cannot initiate connection as one of the environment variables is empty - AZURE_COSMOSDB_ENDPOINT, AZURE_COSMOSDB_KEY ,AZURE_COSMOSDB_DB" 60 | chat_log_inserted = True 61 | print(msg) 62 | err_message = err_message + msg + " ::: " 63 | chat_log_res = chat_log_result(is_err=chat_log_inserted,err_msg=err_message) 64 | return chat_log_res 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /app/backend/chat_log/temp_dev.py: -------------------------------------------------------------------------------- 1 | import azure.cosmos.cosmos_client as cosmos_client 2 | 3 | import azure.cosmos.errors as errors 4 | import uuid 5 | import cosmosdb_logging 6 | 7 | ## Sample code to insert chat log data into Cosmos DB 8 | 9 | if __name__ == "__main__": 10 | chat_session_id = str(uuid.uuid4()) 11 | user_id = str(uuid.uuid4()) 12 | chat_id = str(uuid.uuid4()) 13 | previous_message_id = str(uuid.uuid4()) 14 | document_definition = { "id": str(uuid.uuid4()), 15 | "chat_session_id": chat_session_id, 16 | "user": {"name":'test',"user_id":user_id}, 17 | 'message': {"id":chat_id, 18 | "prompt":[{"role":"system","content":"test content"},{"role":"human","content":"test content works!"}], 19 | "other_attr":[{"completion": { 20 | "choices": [ 21 | { 22 | "finish_reason": "stop", 23 | "index": 0, 24 | "message": { 25 | "content": "", 26 | "role": "assistant" 27 | } 28 | } 29 | ]}},{"created":"datetime"},{"model": "gpt-35-turbo"},{"object": "chat.completion"},{"usage": { 30 | "completion_tokens": 95, 31 | "prompt_tokens": 664, 32 | "total_tokens": 759 33 | }}], 34 | "previous_message_id":previous_message_id}} 35 | #document = container.create_item(body=document_definition) 36 | 37 | result = cosmosdb_logging.insert_chat_log(document_definition) 38 | if result.is_err == True: 39 | print('chat log insert error: ' + result.err_msg) 40 | else: 41 | print('chat log inserted successfully') 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | # Create a container with partition key 52 | 53 | 54 | 55 | 56 | # Insert a JSON document with partition key 57 | 58 | 59 | 60 | 61 | ''' 62 | 63 | { 64 | : "", 65 | "user":{"name":"","user_id":""}, 66 | "message":{ 67 | "id": "unique ID identifying the message", 68 | "prompt":[{ 69 | "role": "system | human", 70 | "content": "message content" 71 | 72 | }], 73 | 74 | "other_attr": [{"completion": { 75 | "choices": [ 76 | { 77 | "finish_reason": "stop", 78 | "index": 0, 79 | "message": { 80 | "content": "", 81 | "role": "assistant" 82 | } 83 | } 84 | ]}},{"created":"datetime"},{"model": "gpt-35-turbo"},{"object": "chat.completion"},{"usage": { 85 | "completion_tokens": 95, 86 | "prompt_tokens": 664, 87 | "total_tokens": 759 88 | }}], 89 | "previous_message_id": "" 90 | 91 | } 92 | } 93 | 94 | ''' -------------------------------------------------------------------------------- /app/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-identity==1.13.0b3 2 | Flask==2.2.5 3 | openai==0.27.3 4 | azure-cosmos==4.3.1 5 | python-dotenv==1.0.0 6 | -------------------------------------------------------------------------------- /app/backend/static/assets/github-8c68b25d.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/backend/static/assets/index-51849b94.css: -------------------------------------------------------------------------------- 1 | *{box-sizing:border-box}html,body{height:100%;margin:0;padding:0}html{background:#ffffff;font-family:Segoe UI,-apple-system,BlinkMacSystemFont,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#root{height:100%}._layout_f6i7q_1{display:flex;flex-direction:column;height:100%}._header_f6i7q_7{background-color:#0078d4;height:44px}._headerTitle_f6i7q_11{display:flex;align-items:center;justify-content:left;text-decoration:none;color:#f2f2f2;width:1500px;float:left}._headerTitleText_f6i7q_20{display:flex;justify-content:center;text-decoration:none;color:#f2f2f2;width:150px;font-size:16px}._headerCommandBar_f6i7q_29{display:flex;align-items:center;justify-content:center;width:150px;height:40px}._headerContainer_f6i7q_37{display:flex;align-items:center;justify-content:space-between;width:100%;height:40px}._headerAvatar_f6i7q_45{display:flex;width:50px;align-items:center;justify-content:center}._container_1xd99_1{flex:1;display:flex;flex-direction:column;height:100%}._stack_1xd99_8{height:100%}._stackItem_1xd99_12{display:flex;height:100%;width:70%;margin-right:5px;flex-direction:row-reverse}._stackItemConfig_1xd99_20{display:flex;flex-direction:column;height:100%;margin-left:5px;background-color:#f2f2f2;padding:10px}._chatRoot_1xd99_30{flex:1;display:flex}._chatContainer_1xd99_35{flex:1;display:flex;flex-direction:column;align-items:center}._chatEmptyState_1xd99_43{flex-grow:1;display:flex;flex-direction:column;justify-content:center;align-items:center;max-height:1024px;padding-top:60px}._chatEmptyStateTitle_1xd99_53{font-size:4rem;font-weight:600;margin-top:0;margin-bottom:30px}._chatEmptyStateSubtitle_1xd99_60{font-weight:600;margin-bottom:10px}@media only screen and (max-height: 780px){._chatEmptyState_1xd99_43{padding-top:0}._chatEmptyStateTitle_1xd99_53{font-size:3rem;margin-bottom:0}}._chatMessageStream_1xd99_76{flex-grow:1;max-width:1028px;width:100%;overflow-y:auto;padding-left:24px;padding-right:24px;display:flex;flex-direction:column}._chatMessageGptMinWidth_1xd99_87{max-width:500px;margin-bottom:20px}._chatInput_1xd99_92{position:sticky;bottom:0;padding:12px 24px 24px;width:100%;max-width:1028px;background:#ffffff;display:flex;flex-direction:row}._chatAnalysisPanel_1xd99_107{flex:1;overflow-y:auto;max-height:89vh;margin-left:20px;margin-right:20px}._chatSettingsSeparator_1xd99_115{padding:15px}._chatSettingsLabel_1xd99_119{font-size:20px;font-weight:700;font-family:Segoe UI,-apple-system,BlinkMacSystemFont,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}._loadingLogo_1xd99_126{font-size:28px}._commandsContainer_1xd99_130{display:flex;flex-direction:column;justify-content:center;padding:10px}._commandButton_1xd99_137{margin-right:20px}._answerContainer_13phk_1{outline:transparent solid 1px;display:flex;flex-direction:row;align-items:center}._answerLogo_13phk_8{padding:24px;font-size:28px}._answerText_13phk_13{padding:16px 20px;background:rgb(249,249,249);border-radius:8px;box-shadow:0 2px 4px #00000024,0 0 2px #0000001f;font-size:16px;font-weight:400;line-height:22px;white-space:pre-line;max-width:60%;display:flex;min-width:500px}._answerText_13phk_13 table{border-collapse:collapse}._answerText_13phk_13 td,._answerText_13phk_13 th{border:1px solid;padding:5px}._selected_13phk_39{outline:2px solid rgba(115,118,225,1)}._citationLearnMore_13phk_43{margin-right:5px;font-weight:600;line-height:24px}._citation_13phk_43{font-weight:500;line-height:24px;text-align:center;border-radius:4px;padding:0 8px;background:#d1dbfa;color:#123bb6;text-decoration:none;cursor:pointer}._citation_13phk_43:hover{text-decoration:underline}._followupQuestionsList_13phk_65{margin-top:10px}._followupQuestionLearnMore_13phk_69{margin-right:5px;font-weight:600;line-height:24px}._followupQuestion_13phk_65{font-weight:600;line-height:24px;text-align:center;border-radius:4px;padding:0 8px;background:#e8ebfa;color:#000;font-style:italic;text-decoration:none;cursor:pointer}._supContainer_13phk_88{text-decoration:none;cursor:pointer}._supContainer_13phk_88:hover{text-decoration:underline}sup{position:relative;display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;vertical-align:top;top:-1;margin:0 2px;min-width:14px;height:14px;border-radius:3px;background:#d1dbfa;color:#123bb6;text-decoration-color:transparent;outline:transparent solid 1px;cursor:pointer}._retryButton_13phk_117{margin-top:10px;width:fit-content}._isLoading_13phk_122{margin-left:50px;width:50%}._questionInputContainer_16lqj_1{border-radius:8px;box-shadow:0 8px 16px #00000024,0 0 2px #0000001f;width:100%;padding:15px;background:white}._questionLogo_16lqj_9{padding:24px}._questionInputTextArea_16lqj_13{width:100%;line-height:40px}._questionInputTextAreaInput_16lqj_18{font-size:16px}._questionInputButtonsContainer_16lqj_22{display:flex;flex-direction:column;justify-content:center}._questionInputSendButton_16lqj_28{cursor:pointer}._questionInputSendButtonDisabled_16lqj_32{opacity:.4}._container_hy3wk_1{display:flex;flex-direction:row;align-items:center;justify-content:flex-end;margin-top:20px;margin-bottom:20px;margin-left:auto;max-width:80%}._logo_hy3wk_12{display:flex;flex-direction:column;padding:10px;width:120px;justify-content:center;align-items:center;overflow:hidden}._message_hy3wk_22{padding:20px;background:#e8ebfa;border-radius:8px;box-shadow:0 2px 4px #00000024,0 0 2px #0000001f;outline:transparent solid 1px;max-width:80%}._container_1opm5_1{display:flex;align-items:center;gap:6px;cursor:pointer}._disabled_1opm5_8{opacity:.4} 2 | -------------------------------------------------------------------------------- /app/backend/static/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/backend/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | OpenAI at Scale 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/backend/text.py: -------------------------------------------------------------------------------- 1 | def nonewlines(s: str) -> str: 2 | return s.replace('\n', ' ').replace('\r', ' ') 3 | -------------------------------------------------------------------------------- /app/frontend/.env.sample: -------------------------------------------------------------------------------- 1 | VITE_CLIENTID="" 2 | VITE_TENANTID="" 3 | -------------------------------------------------------------------------------- /app/frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "printWidth": 160, 4 | "arrowParens": "avoid", 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /app/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | OpenAI at Scale 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/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 | "watch": "tsc && vite build --watch" 10 | }, 11 | "dependencies": { 12 | "@azure/msal-browser": "^2.36.0", 13 | "@azure/msal-react": "^1.5.6", 14 | "@fluentui/example-data": "^8.4.7", 15 | "@fluentui/react": "^8.105.3", 16 | "@fluentui/react-components": "^9.18.6", 17 | "@fluentui/react-icons": "^2.0.195", 18 | "@microsoft/microsoft-graph-client": "^3.0.5", 19 | "@react-spring/web": "^9.7.1", 20 | "@types/md5": "^2.3.0", 21 | "@types/microsoft-graph": "^2.29.0", 22 | "@types/uuid": "^9.0.1", 23 | "dompurify": "^3.0.1", 24 | "md5": "^2.3.0", 25 | "react": "^18.2.0", 26 | "react-bootstrap": "^2.7.4", 27 | "react-dom": "^18.2.0", 28 | "react-router-dom": "^6.8.1", 29 | "uuid": "^9.0.0" 30 | }, 31 | "devDependencies": { 32 | "@types/dompurify": "^2.4.0", 33 | "@types/react": "^18.0.27", 34 | "@types/react-dom": "^18.0.10", 35 | "@vitejs/plugin-react": "^3.1.0", 36 | "prettier": "^2.8.3", 37 | "typescript": "^4.9.3", 38 | "vite": "^4.1.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/frontend/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/frontend/src/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import { AuthenticatedTemplate, UnauthenticatedTemplate } from "@azure/msal-react"; 2 | 3 | import { IPersonaSharedProps, Persona, PersonaSize } from "@fluentui/react/lib/Persona"; 4 | import { 5 | IStackStyles, 6 | IStackTokens, 7 | IStackItemStyles, 8 | Label, 9 | Stack, 10 | Callout, 11 | DirectionalHint, 12 | mergeStyleSets, 13 | Layer, 14 | PersonaInitialsColor 15 | } from "@fluentui/react"; 16 | import { useId } from "@fluentui/react-hooks"; 17 | import { useAppContext } from "./context/AppContext"; 18 | import { useBoolean } from "@fluentui/react-hooks"; 19 | 20 | export default function Avatar() { 21 | const app = useAppContext(); 22 | const examplePersona: IPersonaSharedProps = { 23 | imageUrl: app.user?.avatar || "", 24 | text: app.user?.displayName || "Anonymous", 25 | secondaryText: app.user?.email || "" 26 | }; 27 | 28 | const styles = mergeStyleSets({ 29 | button: { 30 | width: 130 31 | }, 32 | callout: { 33 | background: "white", 34 | width: 250, 35 | maxWidth: "100%" 36 | } 37 | }); 38 | // Tokens definition 39 | const outerStackTokens: IStackTokens = { childrenGap: 3 }; 40 | const innerStackTokens: IStackTokens = { 41 | childrenGap: 5 42 | }; 43 | const stackStyles: IStackStyles = { 44 | root: { 45 | height: 150 46 | } 47 | }; 48 | const stackAvatarStyles: IStackItemStyles = { 49 | root: { 50 | alignItems: "center", 51 | display: "flex", 52 | justifyContent: "center", 53 | backgroundColor: "white", 54 | height: "100%" 55 | } 56 | }; 57 | const stackSignFormStyles: IStackItemStyles = { 58 | root: { 59 | alignItems: "center", 60 | display: "flex", 61 | justifyContent: "center", 62 | backgroundColor: "rgb(248, 248, 248)", 63 | height: "50px", 64 | fontSize: "5px" 65 | } 66 | }; 67 | 68 | const [isCalloutVisible, { toggle: toggleIsCalloutVisible }] = useBoolean(false); 69 | const buttonId = useId("callout-button"); 70 | const labelId = useId("callout-label"); 71 | const descriptionId = useId("callout-description"); 72 | return ( 73 | <> 74 | 75 | 76 | 77 | 78 | 79 | 80 | {isCalloutVisible && ( 81 | 82 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | Sign Out 107 | 108 | 109 | Sign in 110 | 111 | 112 | 113 | 114 | 115 | 116 | )} 117 | > 118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /app/frontend/src/Bar.tsx: -------------------------------------------------------------------------------- 1 | import { Label, Stack, Callout, CommandBar, ICommandBarItemProps, DefaultPalette, mergeStyleSets, ThemeProvider, PartialTheme } from "@fluentui/react"; 2 | import { useBoolean } from "@fluentui/react-hooks"; 3 | import { useId } from "@fluentui/react-hooks"; 4 | import { FontIcon } from "@fluentui/react/lib/Icon"; 5 | import github from "./assets/github.svg"; 6 | 7 | const styles = mergeStyleSets({ 8 | commandBar: { 9 | backgroundColor: DefaultPalette.blue, 10 | alignItems: "center", 11 | justifyContent: "center", 12 | height: "40px%" 13 | }, 14 | button: { 15 | width: 130 16 | }, 17 | callout: { 18 | background: "white", 19 | width: 250, 20 | height: 200, 21 | maxWidth: "100%", 22 | padding: "10px" 23 | }, 24 | stack: { 25 | display: "flex", 26 | alignItems: "center", 27 | justifyContent: "left", 28 | padding: "10px", 29 | textDecoration: "none" 30 | } 31 | }); 32 | 33 | export default function Bar() { 34 | const buttonHelpId = useId("callout-button"); 35 | const buttonInfoId = useId("callout-button"); 36 | const [isHelpCalloutVisible, { toggle: toggleIsHelpCalloutVisible }] = useBoolean(false); 37 | const [isInfoCalloutVisible, { toggle: toggleIsInfoCalloutVisible }] = useBoolean(false); 38 | 39 | const _items: ICommandBarItemProps[] = [ 40 | { 41 | key: "help", 42 | text: "Help", 43 | iconProps: { 44 | iconName: "Help", 45 | styles: { 46 | root: { 47 | color: "white" 48 | } 49 | } 50 | }, 51 | style: { 52 | backgroundColor: "rgb(0, 120, 212)" 53 | }, 54 | iconOnly: true, 55 | id: buttonHelpId, 56 | onClick: toggleIsHelpCalloutVisible 57 | }, 58 | { 59 | key: "info", 60 | text: "Info", 61 | ariaLabel: "Info", 62 | iconOnly: true, 63 | iconProps: { 64 | iconName: "Info", 65 | styles: { 66 | root: { 67 | color: "white" 68 | } 69 | } 70 | }, 71 | style: { 72 | backgroundColor: "rgb(0, 120, 212)" 73 | }, 74 | id: buttonInfoId, 75 | onClick: toggleIsInfoCalloutVisible 76 | } 77 | ]; 78 | const darkTheme: PartialTheme = { 79 | semanticColors: { 80 | bodyBackground: DefaultPalette.blue, 81 | bodyText: "white" 82 | } 83 | }; 84 | return ( 85 | <> 86 | 87 | 88 | 89 | {isHelpCalloutVisible && ( 90 | 91 | 92 | 93 | 94 | 95 | Question & Feedback 96 | 97 | 98 | You can ask question and feedback about this application on GitHub Issues. 99 | 100 | 106 | Azure/openai-at-scale 107 | 108 | 109 | 110 | 111 | 112 | )} 113 | {isInfoCalloutVisible && ( 114 | 115 | 116 | 117 | 118 | 119 | Sample code 120 | 121 | 122 | Visit the GitHub repo to check the source code of this application. 123 | 124 | 130 | Azure/openai-at-scale 131 | 132 | 133 | 134 | 135 | 136 | )} 137 | > 138 | ); 139 | } 140 | -------------------------------------------------------------------------------- /app/frontend/src/GraphService.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | // 5 | import { Client, GraphRequestOptions, PageCollection, PageIterator } from "@microsoft/microsoft-graph-client"; 6 | import { AuthCodeMSALBrowserAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/authCodeMsalBrowser"; 7 | // import { endOfWeek, startOfWeek } from "date-fns"; 8 | // import { zonedTimeToUtc } from "date-fns-tz"; 9 | import { User, Event } from "microsoft-graph"; 10 | 11 | let graphClient: Client | undefined = undefined; 12 | 13 | function ensureClient(authProvider: AuthCodeMSALBrowserAuthenticationProvider) { 14 | if (!graphClient) { 15 | graphClient = Client.initWithMiddleware({ 16 | authProvider: authProvider 17 | }); 18 | } 19 | 20 | return graphClient; 21 | } 22 | 23 | export async function getUser(authProvider: AuthCodeMSALBrowserAuthenticationProvider): Promise { 24 | ensureClient(authProvider); 25 | 26 | // Return the /me API endpoint result as a User object 27 | const user: User = await graphClient! 28 | .api("/me") 29 | // Only retrieve the specific fields needed 30 | .select("displayName,mail,userPrincipalName,photo") 31 | .get(); 32 | 33 | return user; 34 | } 35 | 36 | export async function getProfilePhoto(authProvider: AuthCodeMSALBrowserAuthenticationProvider): Promise { 37 | ensureClient(authProvider); 38 | 39 | // Return the /me/photo API endpoint result as a ProfilePhoto object 40 | let blob = await graphClient!.api("/me/photo/$value").get(); 41 | const url = window.URL || window.webkitURL; 42 | return url.createObjectURL(blob); 43 | } 44 | -------------------------------------------------------------------------------- /app/frontend/src/aadConfig.ts: -------------------------------------------------------------------------------- 1 | export const aadConfig = { 2 | clientId: import.meta.env.VITE_CLIENTID, 3 | tenantId: import.meta.env.VITE_TENANTID, 4 | redirectUri: "http://localhost:5173", 5 | authorityBaseUrl: "https://login.microsoftonline.com/" 6 | }; 7 | -------------------------------------------------------------------------------- /app/frontend/src/api/api.ts: -------------------------------------------------------------------------------- 1 | import { AskResponse, ChatRequest } from "./models"; 2 | 3 | export async function chatApi(options: ChatRequest): Promise { 4 | console.log("options.approach:", options.approach); 5 | console.log("chatApi options: ", options.overrides?.promptSystemTemplate); 6 | console.log("sessionId: ", options.sessionId?.sessionId); 7 | const response = await fetch("/chat", { 8 | method: "POST", 9 | headers: { 10 | Authorization: "Bearer " + options.accessToken?.accessToken || "", 11 | SessionId: options.sessionId?.sessionId || "", 12 | "Content-Type": "application/json" 13 | }, 14 | body: JSON.stringify({ 15 | history: options.history, 16 | approach: options.approach, 17 | overrides: { 18 | top: options.overrides?.top, 19 | temperature: options.overrides?.temperature, 20 | prompt_system_template: options.overrides?.promptSystemTemplate, 21 | maxResponse: options.overrides?.maxResponse 22 | }, 23 | sessionConfig: { 24 | pastMessages: options.sessionConfig?.pastMessages 25 | }, 26 | userInfo: { 27 | username: options.userInfo?.username, 28 | email: options.userInfo?.email 29 | }, 30 | }) 31 | }); 32 | 33 | const parsedResponse: AskResponse = await response.json(); 34 | if (response.status > 299 || !response.ok) { 35 | throw Error(parsedResponse.error || "Unknown error"); 36 | } 37 | 38 | return parsedResponse; 39 | } 40 | -------------------------------------------------------------------------------- /app/frontend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./models"; 3 | -------------------------------------------------------------------------------- /app/frontend/src/api/models.ts: -------------------------------------------------------------------------------- 1 | export const enum Approaches { 2 | RetrieveThenRead = "rtr", 3 | ReadRetrieveRead = "rrr", 4 | ReadDecomposeAsk = "rda" 5 | } 6 | 7 | export type AskRequestOverrides = { 8 | promptSystemTemplate?: string; 9 | maxResponse?: number; 10 | temperature?: number; 11 | top?: number; 12 | }; 13 | 14 | export type SessionConfig = { 15 | pastMessages?: number; 16 | }; 17 | 18 | export type AskResponse = { 19 | answer: string | undefined; 20 | error?: string; 21 | }; 22 | 23 | export type ChatTurn = { 24 | user: string; 25 | bot: string | undefined; 26 | }; 27 | 28 | export type UserInfo = { 29 | username?: string; 30 | email?: string; 31 | }; 32 | 33 | export type AccessToken = { 34 | accessToken: string; 35 | }; 36 | 37 | export type SessionId = { 38 | sessionId: string; 39 | }; 40 | 41 | export type ChatRequest = { 42 | history: ChatTurn[]; 43 | approach: Approaches; 44 | overrides?: AskRequestOverrides; 45 | sessionConfig?: SessionConfig; 46 | userInfo?: UserInfo; 47 | accessToken?: AccessToken; 48 | sessionId?: SessionId; 49 | }; 50 | -------------------------------------------------------------------------------- /app/frontend/src/assets/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/frontend/src/components/Answer/Answer.module.css: -------------------------------------------------------------------------------- 1 | .answerContainer { 2 | outline: transparent solid 1px; 3 | display: flex; 4 | flex-direction: row; 5 | align-items: center; 6 | } 7 | 8 | .answerLogo { 9 | padding: 24px; 10 | font-size: 28px; 11 | } 12 | 13 | .answerText { 14 | padding: 20px; 15 | background: rgb(249, 249, 249); 16 | border-radius: 8px; 17 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); 18 | font-size: 16px; 19 | font-weight: 400; 20 | line-height: 22px; 21 | padding-top: 16px; 22 | padding-bottom: 16px; 23 | white-space: pre-line; 24 | max-width: 60%; 25 | display: flex; 26 | min-width: 500px; 27 | } 28 | 29 | .answerText table { 30 | border-collapse: collapse; 31 | } 32 | 33 | .answerText td, 34 | .answerText th { 35 | border: 1px solid; 36 | padding: 5px; 37 | } 38 | 39 | .selected { 40 | outline: 2px solid rgba(115, 118, 225, 1); 41 | } 42 | 43 | .citationLearnMore { 44 | margin-right: 5px; 45 | font-weight: 600; 46 | line-height: 24px; 47 | } 48 | 49 | .citation { 50 | font-weight: 500; 51 | line-height: 24px; 52 | text-align: center; 53 | border-radius: 4px; 54 | padding: 0px 8px; 55 | background: #d1dbfa; 56 | color: #123bb6; 57 | text-decoration: none; 58 | cursor: pointer; 59 | } 60 | 61 | .citation:hover { 62 | text-decoration: underline; 63 | } 64 | 65 | .followupQuestionsList { 66 | margin-top: 10px; 67 | } 68 | 69 | .followupQuestionLearnMore { 70 | margin-right: 5px; 71 | font-weight: 600; 72 | line-height: 24px; 73 | } 74 | 75 | .followupQuestion { 76 | font-weight: 600; 77 | line-height: 24px; 78 | text-align: center; 79 | border-radius: 4px; 80 | padding: 0px 8px; 81 | background: #e8ebfa; 82 | color: black; 83 | font-style: italic; 84 | text-decoration: none; 85 | cursor: pointer; 86 | } 87 | 88 | .supContainer { 89 | text-decoration: none; 90 | cursor: pointer; 91 | } 92 | 93 | .supContainer:hover { 94 | text-decoration: underline; 95 | } 96 | 97 | sup { 98 | position: relative; 99 | display: inline-flex; 100 | align-items: center; 101 | justify-content: center; 102 | font-size: 10px; 103 | font-weight: 600; 104 | vertical-align: top; 105 | top: -1; 106 | margin: 0px 2px; 107 | min-width: 14px; 108 | height: 14px; 109 | border-radius: 3px; 110 | background: #d1dbfa; 111 | color: #123bb6; 112 | text-decoration-color: transparent; 113 | outline: transparent solid 1px; 114 | cursor: pointer; 115 | } 116 | 117 | .retryButton { 118 | margin-top: 10px; 119 | width: fit-content; 120 | } 121 | 122 | .isLoading { 123 | margin-left: 50px; 124 | width: 50%; 125 | } 126 | -------------------------------------------------------------------------------- /app/frontend/src/components/Answer/Answer.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { Stack, IconButton } from "@fluentui/react"; 3 | import DOMPurify from "dompurify"; 4 | 5 | import styles from "./Answer.module.css"; 6 | 7 | import { AskResponse } from "../../api"; 8 | import { parseAnswerToHtml } from "./AnswerParser"; 9 | import { AnswerIcon } from "./AnswerIcon"; 10 | 11 | interface Props { 12 | answer: AskResponse; 13 | } 14 | 15 | export const Answer = ({ answer }: Props) => { 16 | if (answer == undefined) return null; 17 | if (answer.answer == undefined) return null; 18 | const generatedAnswer: string = answer.answer; 19 | const parsedAnswer = useMemo(() => parseAnswerToHtml(generatedAnswer), [answer]); 20 | const sanitizedAnswerHtml = DOMPurify.sanitize(parsedAnswer.answerHtml); 21 | 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /app/frontend/src/components/Answer/AnswerError.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, PrimaryButton } from "@fluentui/react"; 2 | import { ErrorCircle24Regular } from "@fluentui/react-icons"; 3 | 4 | import styles from "./Answer.module.css"; 5 | 6 | interface Props { 7 | error: string; 8 | onRetry: () => void; 9 | } 10 | 11 | export const AnswerError = ({ error, onRetry }: Props) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | {error} 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /app/frontend/src/components/Answer/AnswerIcon.tsx: -------------------------------------------------------------------------------- 1 | import { BotAdd24Regular } from "@fluentui/react-icons"; 2 | 3 | export const AnswerIcon = () => { 4 | return ; 5 | }; 6 | -------------------------------------------------------------------------------- /app/frontend/src/components/Answer/AnswerLoading.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "@fluentui/react"; 2 | import { animated, useSpring } from "@react-spring/web"; 3 | 4 | import styles from "./Answer.module.css"; 5 | import { AnswerIcon } from "./AnswerIcon"; 6 | import { ProgressIndicator } from "@fluentui/react/lib/ProgressIndicator"; 7 | 8 | export const AnswerLoading = () => { 9 | const animatedStyles = useSpring({ 10 | from: { opacity: 0 }, 11 | to: { opacity: 1 } 12 | }); 13 | const barHeight = 10; 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /app/frontend/src/components/Answer/AnswerParser.tsx: -------------------------------------------------------------------------------- 1 | type HtmlParsedAnswer = { 2 | answerHtml: string; 3 | }; 4 | 5 | export function parseAnswerToHtml(answer: string): HtmlParsedAnswer { 6 | const fragments = answer.split(/\[([^\]]+)\]/g); 7 | 8 | return { 9 | answerHtml: fragments.join("") 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /app/frontend/src/components/Answer/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Answer"; 2 | export * from "./AnswerLoading"; 3 | export * from "./AnswerError"; 4 | -------------------------------------------------------------------------------- /app/frontend/src/components/ClearChatButton/ClearChatButton.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | align-items: center; 4 | gap: 6px; 5 | cursor: pointer; 6 | } 7 | 8 | .disabled { 9 | opacity: 0.4; 10 | } 11 | -------------------------------------------------------------------------------- /app/frontend/src/components/ClearChatButton/ClearChatButton.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "@fluentui/react"; 2 | import { Broom28Filled } from "@fluentui/react-icons"; 3 | 4 | import styles from "./ClearChatButton.module.css"; 5 | 6 | interface Props { 7 | className?: string; 8 | onClick: () => void; 9 | disabled?: boolean; 10 | } 11 | 12 | export const ClearChatButton = ({ className, disabled, onClick }: Props) => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /app/frontend/src/components/ClearChatButton/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./ClearChatButton"; 2 | -------------------------------------------------------------------------------- /app/frontend/src/components/QuestionInput/QuestionInput.module.css: -------------------------------------------------------------------------------- 1 | .questionInputContainer { 2 | border-radius: 8px; 3 | box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); 4 | width: 100%; 5 | padding: 15px; 6 | background: white; 7 | } 8 | 9 | .questionLogo { 10 | padding: 24px; 11 | } 12 | 13 | .questionInputTextArea { 14 | width: 100%; 15 | line-height: 40px; 16 | } 17 | 18 | .questionInputTextAreaInput { 19 | font-size: 16px; 20 | } 21 | 22 | .questionInputButtonsContainer { 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: center; 26 | } 27 | 28 | .questionInputSendButton { 29 | cursor: pointer; 30 | } 31 | 32 | .questionInputSendButtonDisabled { 33 | opacity: 0.4; 34 | } 35 | -------------------------------------------------------------------------------- /app/frontend/src/components/QuestionInput/QuestionInput.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Stack, TextField } from "@fluentui/react"; 3 | import { Send28Filled } from "@fluentui/react-icons"; 4 | import { useAppContext } from "../../context/AppContext"; 5 | import styles from "./QuestionInput.module.css"; 6 | 7 | interface Props { 8 | onSend: (question: string) => void; 9 | disabled: boolean; 10 | placeholder?: string; 11 | clearOnSend?: boolean; 12 | } 13 | 14 | export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { 15 | const app = useAppContext(); 16 | const [question, setQuestion] = useState(""); 17 | const sendQuestion = () => { 18 | if (disabled || !question.trim()) { 19 | return; 20 | } 21 | 22 | onSend(question); 23 | 24 | if (clearOnSend) { 25 | setQuestion(""); 26 | } 27 | }; 28 | 29 | const onEnterPress = (ev: React.KeyboardEvent) => { 30 | if (ev.key !== "Enter" || ev.nativeEvent.isComposing) { 31 | return; 32 | } 33 | if (ev.key === "Enter" && !ev.shiftKey) { 34 | ev.preventDefault(); 35 | sendQuestion(); 36 | } 37 | }; 38 | 39 | const onQuestionChange = (_ev: React.FormEvent, newValue?: string) => { 40 | if (!newValue) { 41 | setQuestion(""); 42 | } else if (newValue.length <= 1000) { 43 | setQuestion(newValue); 44 | } 45 | }; 46 | 47 | const sendQuestionDisabled = disabled || !question.trim(); 48 | 49 | return ( 50 | 51 | 63 | 64 | 69 | 70 | 71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /app/frontend/src/components/QuestionInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./QuestionInput"; 2 | -------------------------------------------------------------------------------- /app/frontend/src/components/UserChatMessage/UserChatMessage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | justify-content: flex-end; 6 | margin-top: 20px; 7 | margin-bottom: 20px; 8 | margin-left: auto; 9 | max-width: 80%; 10 | } 11 | 12 | .logo { 13 | display: flex; 14 | flex-direction: column; 15 | padding: 10px; 16 | width: 120px; 17 | justify-content: center; 18 | align-items: center; 19 | overflow: hidden; 20 | } 21 | 22 | .message { 23 | padding: 20px; 24 | background: #e8ebfa; 25 | border-radius: 8px; 26 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); 27 | outline: transparent solid 1px; 28 | max-width: 80%; 29 | } 30 | -------------------------------------------------------------------------------- /app/frontend/src/components/UserChatMessage/UserChatMessage.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./UserChatMessage.module.css"; 2 | import { UserChatMessageIcon } from "./UserChatMessageIcon"; 3 | import { Stack } from "@fluentui/react"; 4 | import { useAppContext } from "../../context/AppContext"; 5 | 6 | interface Props { 7 | message: string; 8 | } 9 | 10 | export const UserChatMessage = ({ message }: Props) => { 11 | const app = useAppContext(); 12 | return ( 13 | 14 | {message} 15 | 16 | 17 | {app.user?.displayName || "Anonymous"} 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /app/frontend/src/components/UserChatMessage/UserChatMessageIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IPersonaSharedProps, Persona, PersonaSize, PersonaInitialsColor } from "@fluentui/react"; 2 | import { useAppContext } from "../../context/AppContext"; 3 | 4 | export const UserChatMessageIcon = () => { 5 | const app = useAppContext(); 6 | 7 | const examplePersona: IPersonaSharedProps = { 8 | imageUrl: app.user?.avatar 9 | }; 10 | 11 | return ; 12 | }; 13 | -------------------------------------------------------------------------------- /app/frontend/src/components/UserChatMessage/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./UserChatMessage"; 2 | -------------------------------------------------------------------------------- /app/frontend/src/context/AppContext.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | import React, { useContext, createContext, useState, MouseEventHandler, useEffect } from "react"; 5 | import { AuthCodeMSALBrowserAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/authCodeMsalBrowser"; 6 | import { InteractionType, PublicClientApplication } from "@azure/msal-browser"; 7 | import { useMsal } from "@azure/msal-react"; 8 | import { getUser, getProfilePhoto } from "../GraphService"; 9 | import { v4 as uuidv4 } from "uuid"; 10 | 11 | // 12 | export interface AppUser { 13 | displayName?: string; 14 | email?: string; 15 | avatar?: string; 16 | } 17 | 18 | export interface AppError { 19 | message: string; 20 | debug?: string; 21 | } 22 | 23 | export interface AccessToken { 24 | accessToken?: string; 25 | } 26 | 27 | export interface sessionInfo { 28 | sessionId?: string; 29 | } 30 | 31 | type AppContext = { 32 | user?: AppUser; 33 | error?: AppError; 34 | accessToken?: AccessToken; 35 | sessionId?: sessionInfo; 36 | signIn?: MouseEventHandler; 37 | signOut?: MouseEventHandler; 38 | displayError?: Function; 39 | clearError?: Function; 40 | authProvider?: AuthCodeMSALBrowserAuthenticationProvider; 41 | isAuthenticated?: boolean; 42 | }; 43 | 44 | const appContext = createContext({ 45 | user: undefined, 46 | error: undefined, 47 | accessToken: undefined, 48 | sessionId: undefined, 49 | signIn: undefined, 50 | signOut: undefined, 51 | displayError: undefined, 52 | clearError: undefined, 53 | authProvider: undefined 54 | }); 55 | 56 | export function useAppContext(): AppContext { 57 | return useContext(appContext); 58 | } 59 | 60 | interface ProvideAppContextProps { 61 | children: React.ReactNode; 62 | } 63 | 64 | export default function ProvideAppContext({ children }: ProvideAppContextProps) { 65 | const auth = useProvideAppContext(); 66 | return {children}; 67 | } 68 | // 69 | 70 | function useProvideAppContext() { 71 | const msal = useMsal(); 72 | const [user, setUser] = useState(undefined); 73 | const [error, setError] = useState(undefined); 74 | const [accessToken, setAccessToken] = useState(undefined); 75 | const [sessionId, setSessionId] = useState(undefined); 76 | const [isAuthenticated, setIsAuthenticated] = useState(false); 77 | 78 | const displayError = (message: string, debug?: string) => { 79 | setError({ message, debug }); 80 | }; 81 | 82 | const clearError = () => { 83 | setError(undefined); 84 | }; 85 | 86 | // 87 | // Used by the Graph SDK to authenticate API calls 88 | const authProvider = new AuthCodeMSALBrowserAuthenticationProvider(msal.instance as PublicClientApplication, { 89 | account: msal.instance.getActiveAccount()!, 90 | scopes: ["user.read"], 91 | interactionType: InteractionType.Popup 92 | }); 93 | // 94 | 95 | // 96 | useEffect(() => { 97 | const checkUser = async () => { 98 | if (!user) { 99 | try { 100 | // Check if user is already signed in 101 | const account = msal.instance.getActiveAccount(); 102 | if (account) { 103 | // Get the user from Microsoft Graph 104 | const user = await getUser(authProvider); 105 | console.log("user", user); 106 | const avatar = await getProfilePhoto(authProvider); 107 | setUser({ 108 | displayName: user.displayName || "", 109 | email: user.mail || "", 110 | avatar: avatar || "" 111 | }); 112 | msal.instance 113 | .acquireTokenSilent({ 114 | scopes: ["user.read"], 115 | account: account 116 | }) 117 | .then(function (accessTokenResponse) { 118 | setAccessToken({ 119 | accessToken: accessTokenResponse.accessToken 120 | }); 121 | console.log("token", accessTokenResponse.accessToken); 122 | console.log("session id", accessTokenResponse.idTokenClaims); 123 | }); 124 | 125 | //generate Session ID after user is logged in 126 | setSessionId({ 127 | sessionId: uuidv4() 128 | }); 129 | } 130 | } catch (err: any) { 131 | displayError(err.message); 132 | } 133 | } 134 | }; 135 | checkUser(); 136 | }); 137 | 138 | const signIn = async () => { 139 | await msal.instance.loginPopup({ 140 | scopes: ["user.read"], 141 | prompt: "select_account" 142 | }); 143 | 144 | // Get the user from Microsoft Graph 145 | const user = await getUser(authProvider); 146 | 147 | setUser({ 148 | displayName: user.displayName || "", 149 | email: user.mail || "" 150 | }); 151 | setIsAuthenticated(true); 152 | }; 153 | 154 | const signOut = async () => { 155 | await msal.instance.logoutPopup(); 156 | setUser(undefined); 157 | setIsAuthenticated(false); 158 | }; 159 | 160 | const configureSessionId = (sessionId: string) => { 161 | setSessionId({ sessionId }); 162 | }; 163 | 164 | return { 165 | user, 166 | error, 167 | accessToken, 168 | sessionId, 169 | signIn, 170 | signOut, 171 | displayError, 172 | clearError, 173 | authProvider, 174 | isAuthenticated 175 | }; 176 | } 177 | -------------------------------------------------------------------------------- /app/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | html { 13 | background: #ffffff; 14 | font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 15 | sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | 20 | #root { 21 | height: 100%; 22 | } 23 | -------------------------------------------------------------------------------- /app/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { HashRouter, Routes, Route } from "react-router-dom"; 4 | import { initializeIcons } from "@fluentui/react"; 5 | import { MsalProvider } from "@azure/msal-react"; 6 | import { PublicClientApplication, IPublicClientApplication, EventMessage, EventType, AuthenticationResult } from "@azure/msal-browser"; 7 | 8 | import "./index.css"; 9 | 10 | import Layout from "./pages/layout/Layout"; 11 | import NoPage from "./pages/NoPage"; 12 | import Chat from "./pages/chat/Chat"; 13 | import AppContext from "./context/AppContext"; 14 | import { aadConfig } from "./aadConfig"; 15 | initializeIcons(); 16 | 17 | const msalInstance = new PublicClientApplication({ 18 | auth: { 19 | clientId: aadConfig.clientId, 20 | redirectUri: aadConfig.redirectUri, 21 | authority: aadConfig.authorityBaseUrl + aadConfig.tenantId 22 | }, 23 | cache: { 24 | cacheLocation: "sessionStorage", 25 | storeAuthStateInCookie: true 26 | } 27 | }); 28 | 29 | const accounts = msalInstance.getAllAccounts(); 30 | if (accounts && accounts.length > 0) { 31 | msalInstance.setActiveAccount(accounts[0]); 32 | } 33 | 34 | msalInstance.addEventCallback((event: EventMessage) => { 35 | if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) { 36 | // Set the active account - this simplifies token acquisition 37 | const authResult = event.payload as AuthenticationResult; 38 | msalInstance.setActiveAccount(authResult.account); 39 | } 40 | }); 41 | 42 | type AppProps = { 43 | pca: IPublicClientApplication; 44 | }; 45 | 46 | export default function App({ pca }: AppProps): JSX.Element { 47 | return ( 48 | 49 | 50 | 51 | 52 | }> 53 | } /> 54 | } /> 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | 63 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 64 | 65 | 66 | 67 | ); 68 | -------------------------------------------------------------------------------- /app/frontend/src/pages/NoPage.tsx: -------------------------------------------------------------------------------- 1 | const NoPage = () => { 2 | return 404 Error; 3 | }; 4 | 5 | export default NoPage; 6 | -------------------------------------------------------------------------------- /app/frontend/src/pages/chat/Chat.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | flex: 1; 3 | display: flex; 4 | flex-direction: column; 5 | height: 100%; 6 | } 7 | 8 | .stack { 9 | height: 100%; 10 | } 11 | 12 | .stackItem { 13 | display: flex; 14 | height: 100%; 15 | width: 70%; 16 | margin-right: 5px; 17 | flex-direction: row-reverse; 18 | } 19 | 20 | .stackItemConfig { 21 | display: flex; 22 | flex-direction: column; 23 | height: 100%; 24 | margin-left: 5px; 25 | background-color: #f2f2f2; 26 | padding: 10px; 27 | /* box-shadow: rgba(0, 0, 0, 0.2) 3px 1px 1px 3px; */ 28 | } 29 | 30 | .chatRoot { 31 | flex: 1; 32 | display: flex; 33 | } 34 | 35 | .chatContainer { 36 | flex: 1; 37 | display: flex; 38 | flex-direction: column; 39 | align-items: center; 40 | /* width: 100%; */ 41 | } 42 | 43 | .chatEmptyState { 44 | flex-grow: 1; 45 | display: flex; 46 | flex-direction: column; 47 | justify-content: center; 48 | align-items: center; 49 | max-height: 1024px; 50 | padding-top: 60px; 51 | } 52 | 53 | .chatEmptyStateTitle { 54 | font-size: 4rem; 55 | font-weight: 600; 56 | margin-top: 0; 57 | margin-bottom: 30px; 58 | } 59 | 60 | .chatEmptyStateSubtitle { 61 | font-weight: 600; 62 | margin-bottom: 10px; 63 | } 64 | 65 | @media only screen and (max-height: 780px) { 66 | .chatEmptyState { 67 | padding-top: 0; 68 | } 69 | 70 | .chatEmptyStateTitle { 71 | font-size: 3rem; 72 | margin-bottom: 0px; 73 | } 74 | } 75 | 76 | .chatMessageStream { 77 | flex-grow: 1; 78 | max-width: 1028px; 79 | width: 100%; 80 | overflow-y: auto; 81 | padding-left: 24px; 82 | padding-right: 24px; 83 | display: flex; 84 | flex-direction: column; 85 | } 86 | 87 | .chatMessageGptMinWidth { 88 | max-width: 500px; 89 | margin-bottom: 20px; 90 | } 91 | 92 | .chatInput { 93 | position: sticky; 94 | bottom: 0; 95 | /* flex: 0 0 100px; */ 96 | padding-top: 12px; 97 | padding-bottom: 24px; 98 | padding-left: 24px; 99 | padding-right: 24px; 100 | width: 100%; 101 | max-width: 1028px; 102 | background: #ffffff; 103 | display: flex; 104 | flex-direction: row; 105 | } 106 | 107 | .chatAnalysisPanel { 108 | flex: 1; 109 | overflow-y: auto; 110 | max-height: 89vh; 111 | margin-left: 20px; 112 | margin-right: 20px; 113 | } 114 | 115 | .chatSettingsSeparator { 116 | padding: 15px; 117 | } 118 | 119 | .chatSettingsLabel { 120 | font-size: 20px; 121 | font-weight: bold; 122 | font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 123 | sans-serif; 124 | } 125 | 126 | .loadingLogo { 127 | font-size: 28px; 128 | } 129 | 130 | .commandsContainer { 131 | display: flex; 132 | flex-direction: column; 133 | justify-content: center; 134 | padding: 10px; 135 | } 136 | 137 | .commandButton { 138 | margin-right: 20px; 139 | } 140 | -------------------------------------------------------------------------------- /app/frontend/src/pages/chat/Chat.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState, useEffect } from "react"; 2 | import { TextField, Slider } from "@fluentui/react"; 3 | import { Stack } from "@fluentui/react"; 4 | import { Label } from "@fluentui/react-components"; 5 | import styles from "./Chat.module.css"; 6 | 7 | import { chatApi, Approaches, AskResponse, ChatRequest, ChatTurn } from "../../api"; 8 | import { Answer, AnswerError, AnswerLoading } from "../../components/Answer"; 9 | import { QuestionInput } from "../../components/QuestionInput"; 10 | import { UserChatMessage } from "../../components/UserChatMessage"; 11 | import { ClearChatButton } from "../../components/ClearChatButton"; 12 | import { useAppContext } from "../../context/AppContext"; 13 | 14 | const Chat = () => { 15 | //Parameters of model 16 | const [maxResponse, setMaxResponse] = useState(800); 17 | const [promptSystemTemplate, setPromptSystemTemplate] = useState(""); 18 | const [temperature, setTemperature] = useState(0.5); 19 | const [top, setTop] = useState(0.95); 20 | 21 | //Session settings 22 | const [pastMessages, setPastMessages] = useState(10); 23 | const lastQuestionRef = useRef(""); 24 | const [isLoading, setIsLoading] = useState(false); 25 | const [error, setError] = useState(); 26 | const [answers, setAnswers] = useState<[user: string, response: AskResponse][]>([]); 27 | 28 | //UserInfo 29 | const app = useAppContext(); 30 | 31 | const makeApiRequest = async (question: string) => { 32 | console.log("make api request ....", question); 33 | lastQuestionRef.current = question; 34 | const nullAnswer: AskResponse = { answer: undefined }; 35 | setAnswers([...answers, [question, nullAnswer]]); 36 | console.log("answers: ", answers); 37 | error && setError(undefined); 38 | setIsLoading(true); 39 | 40 | try { 41 | const history: ChatTurn[] = answers.map(a => ({ user: a[0], bot: a[1].answer })); 42 | const request: ChatRequest = { 43 | history: [...history, { user: question, bot: undefined }], 44 | approach: Approaches.ReadRetrieveRead, 45 | overrides: { 46 | promptSystemTemplate: promptSystemTemplate.length === 0 ? undefined : promptSystemTemplate, 47 | maxResponse: maxResponse, 48 | temperature: temperature, 49 | top: top 50 | }, 51 | sessionConfig: { 52 | pastMessages: pastMessages 53 | }, 54 | userInfo: { 55 | username: app.user?.displayName, 56 | email: app.user?.email 57 | }, 58 | accessToken: { 59 | accessToken: app.accessToken?.accessToken || "" 60 | }, 61 | sessionId: { 62 | sessionId: app.sessionId?.sessionId || "" 63 | } 64 | }; 65 | console.log("request: ", request); 66 | const result = await chatApi(request); 67 | console.log("answer: ", result); 68 | console.log([...answers, [question, result]]); 69 | setAnswers([...answers, [question, result]]); 70 | } catch (e) { 71 | setError(e); 72 | } finally { 73 | setIsLoading(false); 74 | } 75 | }; 76 | 77 | const clearChat = () => { 78 | lastQuestionRef.current = ""; 79 | error && setError(undefined); 80 | setAnswers([]); 81 | }; 82 | 83 | //setState for parameters setting 84 | const onPromptSystemTemplateChange = (_ev?: React.FormEvent, newValue?: string) => { 85 | setPromptSystemTemplate(newValue || ""); 86 | }; 87 | const maxResponseOnChange = (value: number) => setMaxResponse(value); 88 | const temperatureOnChange = (value: number) => setTemperature(value); 89 | const topOnChange = (value: number) => setTop(value); 90 | const pastMessagesOnChange = (value: number) => setPastMessages(value); 91 | 92 | return ( 93 | 94 | 95 | 96 | 97 | 98 | 99 | {answers.map((answer, index) => ( 100 | 101 | 102 | 103 | 104 | ))} 105 | {isLoading && ( 106 | <> 107 | 108 | 109 | 110 | > 111 | )} 112 | {error ? ( 113 | <> 114 | 115 | makeApiRequest(lastQuestionRef.current)} /> 116 | 117 | > 118 | ) : null} 119 | 120 | 121 | 122 | makeApiRequest(question)} 127 | /> 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | Parameters 136 | 137 | 145 | 156 | 167 | 178 | Session settings 179 | 190 | 191 | 192 | 193 | 194 | ); 195 | }; 196 | 197 | export default Chat; 198 | -------------------------------------------------------------------------------- /app/frontend/src/pages/layout/Layout.module.css: -------------------------------------------------------------------------------- 1 | .layout { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | } 6 | 7 | .header { 8 | background-color: rgb(0, 120, 212); 9 | height: 44px; 10 | } 11 | .headerTitle { 12 | display: flex; 13 | align-items: center; 14 | justify-content: left; 15 | text-decoration: none; 16 | color: #f2f2f2; 17 | width: 1500px; 18 | float: left; 19 | } 20 | .headerTitleText { 21 | display: flex; 22 | justify-content: center; 23 | text-decoration: none; 24 | color: #f2f2f2; 25 | width: 150px; 26 | font-size: 16px; 27 | } 28 | 29 | .headerCommandBar { 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | width: 150px; 34 | height: 40px; 35 | } 36 | 37 | .headerContainer { 38 | display: flex; 39 | align-items: center; 40 | justify-content: space-between; 41 | width: 100%; 42 | height: 40px; 43 | } 44 | 45 | .headerAvatar { 46 | display: flex; 47 | width: 50px; 48 | align-items: center; 49 | justify-content: center; 50 | } 51 | -------------------------------------------------------------------------------- /app/frontend/src/pages/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, Link } from "react-router-dom"; 2 | import { Stack } from "@fluentui/react"; 3 | 4 | import styles from "./Layout.module.css"; 5 | 6 | import Avatar from "../../Avatar"; 7 | import Bar from "../../Bar"; 8 | 9 | const Layout = () => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | OpenAI at Scale 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default Layout; 35 | -------------------------------------------------------------------------------- /app/frontend/src/requirements.txt: -------------------------------------------------------------------------------- 1 | azure-identity==1.13.0b3 2 | Flask>=2.2.2 3 | openai>=0.27.3 4 | -------------------------------------------------------------------------------- /app/frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /app/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "types": ["vite/client"] 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /app/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | build: { 8 | outDir: "../backend/static", 9 | emptyOutDir: true, 10 | sourcemap: true 11 | }, 12 | server: { 13 | proxy: { 14 | "/ask": "http://localhost:5000", 15 | "/chat": "http://localhost:5000" 16 | } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /docs/en/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/openai-at-scale/cec5ce5b1f27f44c4056384ec8113e535c3b3575/docs/en/.gitkeep -------------------------------------------------------------------------------- /docs/en/logging_cosmosdb.md: -------------------------------------------------------------------------------- 1 | ## Logging prompt log with Azure Cosmos DB 2 | Large scale systems that are adopting Azure OpenAI APIs to incorporate Large Language Models such as chatgpt. Logging the user interactions in terms of prompts and responses is very critical. This data can be used for downstream analytis in determining patterns of conversations, identifying risks and effectiveness of the model in providing accurate responses. 3 | 4 | ### Azure Cosmos DB 5 | We have decided to store the prompt log in Azure Cosmos DB as it can scale to a large number of concurrent transactions and has the provision of an Analytical store that enables executing analytical queries at scale with controlled costs. 6 | 7 | ### Typical downstream Analytics 8 | Following are examples of some analytical use-cases in addition to the monitoring and logging use-cases that can be built. This repository will have a set of pre-designed dashboard implementing the below use-cases and can be extended to others depending on specific requirements of the implementations. 9 | 10 | 1. Sentiment analytics on conversations 11 | 2. Common topics discussed 12 | 3. Conversation stats in a session 13 | 4. Concurrent sessions stats 14 | 15 | ## Document Structure 16 | Following is the JSON document structure that will store the prompt log data. 17 | 18 | { 19 | "chat_session_id": "", 20 | "user":{"name":"","user_id":""}, 21 | "message":{ 22 | "id": "unique ID identifying the message", 23 | "prompt":[{ 24 | "role": "system | human", 25 | "content": "message content" 26 | 27 | }], 28 | 29 | "other_attr": [{"completion": { 30 | "choices": [ 31 | { 32 | "finish_reason": "stop", 33 | "index": 0, 34 | "message": { 35 | "content": "", 36 | "role": "assistant" 37 | } 38 | } 39 | ]}},{"created":"datetime"},{"model": "gpt-35-turbo"},{"object": "chat.completion"},{"usage": { 40 | "completion_tokens": 95, 41 | "prompt_tokens": 664, 42 | "total_tokens": 759 43 | }}], 44 | "previous_message_id": "" 45 | 46 | } 47 | } 48 | 49 | ### Partitioning 50 | Recommended partitioning key for storing prompt log data is "chat_session_id". This will ensure that all prompts related to the same session are in the same partitions, and basic logging queries will avoid cross-partitions queries. 51 | 52 | Following structure automatically created in the analytical store will enable further analytics (WIP). 53 | 54 | Coming Soon 55 | 1. Setting up Azure Cosmos DB for prompt logging 56 | 2. Inserting prompt log data into Azure Cosmos DB 57 | 3. Dashboards for Analytics 58 | -------------------------------------------------------------------------------- /docs/images/appcomponents.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:53c2ce4faf67b1de817eabb1e025642455d9051acc348d25f79d64ffa028b8dc 3 | size 55860 4 | -------------------------------------------------------------------------------- /docs/images/chatscreen.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f69914a2073c2c1a717c7c377be247ce0c89103311996750b55ede260c8abfbe 3 | size 330115 4 | -------------------------------------------------------------------------------- /docs/images/network-control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/openai-at-scale/cec5ce5b1f27f44c4056384ec8113e535c3b3575/docs/images/network-control.png -------------------------------------------------------------------------------- /docs/jp/ai-security-draft.md: -------------------------------------------------------------------------------- 1 | # Security Part 2 | 3 | サイバー セキュリティの目的は、組織に対して発生するサイバー セキュリティ上のリスクを把握し、許容可能なレベルに管理することです。 4 | 5 | サイバーセキュリティは人、組織のプロセス、システムの実装を包括的にカバーする必要がありますが、サイバーセキュリティのエリアでは様々な組織によって管理、実証されている基準やフレームワークが存在し、適切なものを選択することで、適切なコストで抜け漏れの少ないサイバーセキュリティを実現することができます。 6 | 7 | ここでは [Cyber Security Framework](https://www.nist.gov/cyberframework) に従って、フレームワークで提供される機能の一部を拾いながら AI/ML システムで考慮すべきリスクと対処の方針を解説します。 8 | 9 | ## Cyber Security Framework 10 | 11 | [Cyber Security Framework](https://www.nist.gov/cyberframework) は [NIST](https://www.nist.gov/) によって管理されている、組織がサイバー セキュリティを継続的かつ体系的に管理するためのフレームワークで、組織のセキュリティ機能を識別、防御、検知、対応、復旧の 5 つに分類します。 12 | 13 |  14 | 15 | - 識別 (Identify) : ビジネス環境、資産、脅威を識別し、リスクを把握する。 16 | - 防御 (Protection): 特定されたリスクに対して適切な保護策を検討し、実施する。 17 | - 検知 (Detect) : サイバー セキュリティ イベントの発生を検出する。 18 | - 対応 (Respond) : 検知されたサイバーセキュリティ インシデントに対処する。 19 | - 復旧 (Recover) : サイバー セキュリティ インシデントによって阻害された機能やサービスを復旧する。 20 | 21 | フレームワークの機能は識別から開始され、まず組織の資産と資産に対する脅威が識別されるため、新しいテクノロジーを導入する際にもリスクを検討することができます。また、識別された脅威に対してセキュリティ対策を検討することになるため、リスク ベースのアプローチとなり、実際に組織にとって必要なセキュリティ対策が実装されやすいという特徴があります。 22 | 23 | フレームワークは CIS CRC、ISO 27001 (ISMS)、NIST SP800-53 などの重要なセキュリティ基準を参照しています。これによりフレームワークの信頼性を説明することが可能になり、またこれらの基準とフレームワークを併用することを容易にしています。フレームワークのカテゴリー自体は包括的でありながらも項目の数はコンパクトであり、小規模な組織でも管理しやすいものになっています。 24 | 25 | ## 識別 (Identify) 26 | 27 | 識別では組織のビジネス環境や所有する資産、内部および外部の脅威、関連するサプライチェーンなどを識別します。リスクの可能性は脅威の存在に対して存在するため、脅威を把握することで管理されていないリスクを減らすことができます。 28 | 29 | ### 脅威の特定 30 | 31 | サイバー セキュリティの目的は、組織に対して発生するサイバー セキュリティ上のリスクを把握し、許容可能なレベルに管理することです。リスクの可能性は脅威に対して存在するため、脅威を把握することで管理されていないリスクを減らすことができます。 32 | 33 | この脅威を識別する取り組みは脅威モデリングと呼ばれますが、AI/ML システムでは従来の脅威に加えてさらに深く議論すべき課題があると考えられています。 34 | 35 | #### 従来の脅威 36 | 37 | 組織のAI/ML システムシステムは、既存のインフラストラクチャの一部として動作するため、従来のシステムに対して発生する脅威は、AI/ML システムにおいても考慮されている必要があります。 38 | 39 | システムが満たすべきセキュリティ要件は機密性、完全性、可用性という 3 つの要素として整理されます。 40 | 41 | サイバー セキュリティの基本 3 要素 42 | 43 | - 機密性 (Confidentiality) : 権限のある主体だけが情報を読み取ることができること 44 | - 完全性 (Integrity) : 権限のある主体だけが情報を変更することができること 45 | - 可用性 (Availability) : 情報が必要なときに利用できること 46 | 47 | より詳細な議論のために、この他に真正性(Authenticity)、信頼性(Reliability)、責任追跡性(Accountability) 、否認防止(non-repudiation)という 4 要素が追加されることもあります。 48 | 49 | これらのセキュリティ要素を侵害するものがサイバー セキュリティ上の脅威です。特定のシステムに対して想定される脅威を識別する取り組みを脅威モデリングと呼びます。サイバー セキュリティ上の脅威はデータの入出力に対して発生するため、システムのコンポーネント間のデータフローを記述し、識別されたデータの入出力に対して脅威の分析を行います。 50 | 組織が管理していないコンポーネントから組織が管理しているコンポ―ネントに対するデータの入出力があったり、組織の管理しているコンポーネントでもセキュリティ レベルの異なるコンポーネント間のやりとりは信頼の境界を跨ぐデータの入出力になるため、注意深く脅威を識別する必要があります。 51 | 52 | [脅威モデリングの概要](https://learn.microsoft.com/ja-jp/training/modules/tm-introduction-to-threat-modeling/) 53 | 54 | また、攻撃者が使用する既知のテクニックは TTP というナレッジベースで管理されており、広く利用されているものとしては [MITRE ATT&CK®](https://attack.mitre.org/) があります。攻撃のフェーズに応じたテクニックが整理されており、脅威シナリオを想定したり、技術的な実装を検討する際に役立てることができます。 55 | 56 | #### 障害モード (AI/ML システムの脅威) 57 | 58 | AI/ML システムで考慮すべき新しいリスクは [機械学習の障害モード](https://learn.microsoft.com/ja-jp/security/engineering/failure-modes-in-machine-learning) にまとめられています。 59 | 60 | このドキュメントではAI/ML システムに発生する問題を意図的な障害と意図的ではない障害の2 つに分類し整理を行っています。 61 | 62 | 意図的な障害はアクティブな敵対者によって引き起こされる障害で、機密性、完全性、可用性を脅かす可能性のある脅威と考えることができます。Azure Open AI で既存のモデルを使用する場合、学習データやモデルの管理はプラットフォーム側の管理になりますが、ファインチューニングを行う際の学習データや、クエリの問い合わせを行う際に他のデータベースと連携するような場合には、これらの脅威によって発生する新たなリスクを考慮する必要があります。 63 | 64 | 意図的ではない障害は ML が意図した動作を行わないことによって発生します。従来のシステムでも意図しない動作が行われることはありましたが、これは意図に対して適切ではないプログラムが与えられることが原因であり、プログラムのバグとして意図した動作を行うように修正することが可能でした。 65 | 66 | AI/ML システムでは一般的に入力に対する出力を完全に予測したり、問題の原因を完全に特定することが困難です。このため、AI/ML システムに依存するシステムでは意図しない動作が発生することを従来のシステムより高い確率で見込まなければなりません。このため、従来のシステムで好まれているリスクを移転、回避するというアプローチの他にも、リスクの低減と保有を強く意識するシナリオが増えます。 67 | 68 | 意図的な障害を脅威として解釈し、整理して対応する際の指針が次のドキュメントで解説されています。 69 | 70 | [AI/ML システムと依存関係の脅威のモデル化](https://learn.microsoft.com/ja-jp/security/engineering/threat-modeling-aiml) 71 | 72 | AI/ML システムを脅威モデリングする際に新しく導入すべき観点は次の 3 つです。 73 | 74 | - セキュリティ境界ではトレーニング データを含める必要がある 75 | 関連する脅威: 76 | - 敵対的摂動 (すべてのバリエーション) 77 | - データのポイズニング 78 | 79 | - オンラインまたは物理的な領域でお客様に損害を与える可能性のある、モデルまたは製品・サービスの行動を特定する必要がある 80 | 関連する脅威: 81 | - メンバーシップの推論 82 | - モデルの反転 83 | - モデルの盗難 84 | 85 | - AI/ML システムが依存するライブラリやデータ、AI/ML を使用するサードパーティを特定する必要がある 86 | 関連する脅威: 87 | - ニューラル ネットワークの再プログラミング 88 | - 物理ドメインにおける敵対的な例 89 | - 悪意ある ML プロバイダーによるトレーニング データの復旧 90 | - ML サプライ チェーンへの攻撃 91 | - モデルに対するバックドア攻撃 92 | - ML 固有の依存関係の侵害 93 | 94 | ## 防御の実装 95 | 96 | 防御では認証と認可に基づく適切なアクセス制御、ネットワークのセグメンテーション、暗号化など、脅威に対するセキュリティ対策を導入します。脅威に対処ができていない状態は脆弱性と呼ばれます。アプリケーションは様々なコンポーネントによって構成されるため、防御にはそれぞれのコンポーネントに対して考えられる脅威と、それぞれのコンポーネントで構成可能な実装のオプションに関する知識が必要になります。 97 | 98 | ### セキュリティ態勢管理 99 | 100 | Azure ではプラットフォームの機能としてクラウドセキュリティ態勢管理 (CSPM) 機能が提供されます。リソースのセキュリティ設定はプラットフォームで自動的に計測され、脆弱な構成を発見することができます。セキュリティ状態はセキュア スコアとして数値で表示されるため、セキュリティ運用や開発プロセスで KPI のひとつとして利用することができます。 101 | 102 |  103 | 104 | 105 | 106 | ### Microsfot Cloud Security Benchmark 107 | 108 | CSPM 機能が基準とするセキュリティ設定は [Microsoft Cloud Security Benchmark](https://learn.microsoft.com/ja-jp/security/benchmark/azure/) に基づいています。Microsoft Cloud Security Benchmark は組織が実装すべきセキュリティ コントロールを次の 12 種類に分類し、ガイダンスを提供しています。 109 | 110 | - ネットワーク 111 | - ID 管理 112 | - 特権アクセス 113 | - データ保護 114 | - アセット管理 115 | - ログと脅威検出 116 | - インシデント対応 117 | - 体制と脆弱性の管理 118 | - エンドポイントのセキュリティ 119 | - バックアップと回復 120 | - DevOps セキュリティ 121 | - ガバナンスと戦略 122 | 123 | ドキュメントではセキュリティ ベースラインという項目で、 Azure の各リソースで考慮すべきセキュリティ コントロールと、実装のガイダンスを提供しています。このガイダンスのなかでプラットフォームが自動的に測定できるものが CSPM 機能によって自動的に測定され、セキュアスコアが計算されます。 124 | 125 | セキュリティ ベースラインは継続的に更新されていますが、AI/ML システムのコンポーネントでは個別のセキュリティ ベースラインが利用できない場合があります。その際には Microsoft Cloud Security Benchmark を参照し、コンポーネントが考慮すべきセキュリティ コントロールを特定し、ガイドラインを参照することで、他のコンポーネントと整合するセキュリティレベルを保つことができます。 126 | 127 | ### AI/ML システムに対する保護 128 | 129 | - セキュリティ境界ではトレーニング データを含める必要がある 130 | 関連する脅威: 131 | - 敵対的摂動 (すべてのバリエーション) 132 | **[検知]での対応** 133 | - ~~データのポイズニング ~~ 134 | 135 | - オンラインまたは物理的な領域でお客様に損害を与える可能性のある、モデルまたは製品・サービスの行動を特定する必要がある 136 | 関連する脅威: 137 | - ~~メンバーシップの推論~~ 138 | **[検知]での対応** 139 | - ~~モデルの反転~~ 140 | - ~~モデルの盗難~~ 141 | 142 | - AI/ML システムが依存するライブラリやデータ、AI/ML を使用するサードパーティを特定する必要がある 143 | 関連する脅威: 144 | - ~~ニューラル ネットワークの再プログラミング~~ 145 | - 物理ドメインにおける敵対的な例 146 | - ~~悪意ある ML プロバイダーによるトレーニング データの復旧~~ 147 | - ML サプライ チェーンへの攻撃 148 | 脆弱性評価ソリューションを使用する 149 | - モデルに対するバックドア攻撃 150 | **[検知]での対応** 151 | - ML 固有の依存関係の侵害 152 | 脆弱性評価ソリューションを使用する 153 | 154 | ### ログとバックアップ 155 | 156 | Cyber Security Framework の防御機能にはバックアップと監査ログの管理が含まれています。AI/ML システムは通常の手続き型言語で開発されたシステムに比べてコントロールが難しく、意図しない動作が、より高い頻度で発生することを織り込む必要があります。問題が発生した際に調査を行うことができること、また複数のモデルやデータを併用する場合に、意図しない結果がどののモデルやデータから発生したものかを特定する必要がある可能性を考慮してください。 157 | 158 | 最初に検討すべきログとバックアップ: 159 | 160 | - ユーザー ディレクトリの認証ログ 161 | - Azure の監査ログ (Azure Activity) 162 | - AI/ML システムに対するリクエストとレスポンスのログ 163 | - 問題発生時の環境を再現するためのデータ 164 | - モデルのバージョン 165 | - アプリケーション コード 166 | - モデルを生成したデータ 167 | 168 | ## 検知の実装 169 | 170 | 脅威を検知するために必要なログを収集し、検知を行うための分析を実施する 171 | 172 | AI/ML システムの脅威はアクセス制御を越えて発生するため、完全に防御することはできないので検知を成熟させていく必要がある。 173 | 174 | 意図的な障害については(半数くらい?)検出することができる。ファインチューニングをする場合はユーザーの責任になる資産が発生するため力を入れるべき。 175 | 例えばモデルの移転やメンバーシップ推論攻撃をしようとすると、似たようなクエリが(複数のユーザーを跨ぐ場合もある)連続して発生する。 176 | プロンプト インジェクションも似たような傾向になることが想像できる。 177 | 178 | - ログの実装 179 | - 標準的なログ 180 | - AI のログ 181 | 182 | 意図的でない障害は「ユーザーからの通知」で見つけることができる可能性があるので、報告用の UI が用意されていると良い。 183 | 184 | Audit Logs 185 | Request and Response Logs 186 | Trace Logs 187 | AllMetrics 188 | 189 | - 想定する脅威 190 | 191 | - セキュリティ境界ではトレーニング データを含める必要がある 192 | 関連する脅威: 193 | - 敵対的摂動 (すべてのバリエーション) 194 | **[検知]での対応** 195 | 196 | - オンラインまたは物理的な領域でお客様に損害を与える可能性のある、モデルまたは製品・サービスの行動を特定する必要がある 197 | 関連する脅威: 198 | - メンバーシップの推論 199 | **[検知]での対応** 200 | 201 | - AI/ML システムが依存するライブラリやデータ、AI/ML を使用するサードパーティを特定する必要がある。 202 | 関連する脅威: 203 | 204 | - モデルに対するバックドア攻撃 205 | **[検知]での対応** 206 | 207 | ## 対応の実装 208 | 209 | - 対応プロセスを準備しておくことは大事 210 | - 障害モードを検知した場合 211 | - サービスの停止も考慮する 212 | 213 | 問題のある出力は出力がユーザーにわたる前にブロックしなければならないのか 214 | 検知と修正は対応後でも良いのか 215 | -------------------------------------------------------------------------------- /docs/jp/openai-ai-scale.md: -------------------------------------------------------------------------------- 1 | # 1. OpenAI at Scale 概要 📝 2 | ## 目的とアジェンダ 3 | OpenAI at Scale は [FastTrack for Azure](https://azure.microsoft.com/ja-jp/pricing/offers/azure-fasttrack/#overview) が提供する計 3 日間のハンズオンセッションです。Azure OpenAI Service が提供する ChatGPT を利用したサンドボックス的な簡易アプリケーションを構築します。 4 | 5 | **目的** 6 | - エンタープライズ対応した ChatGPT アプリケーションを Azure 上に構築する 7 | - アプリーケーションで利用する Azure サービスの基本を理解する 8 | 9 | **アジェンダ** 10 | 11 | #### Day1 12 | | | Section | Item | Description | Reference | 13 | |---|--|--|--|--| 14 | | 1 | OpenAI at Scale 概要| | | | 15 | | | |目的とアジェンダ| OpenAI at Scale の目的とアジェンダを説明します。 | | 16 | | | |FastTrack for Azure| FastTrack for Azure のプログラムの概要を説明します。 | **プログラム紹介ページ** : [FastTrack for Azure](https://azure.microsoft.com/ja-jp/pricing/offers/azure-fasttrack/) **セミナー** : [プログラムのご紹介(FTA Intro)セッション](https://developer.microsoft.com/en-us/reactor/events/17982/) | 17 | | | |アーキテクチャ| 本ハンズオンで構築するアプリケーションのアーキテクチャを説明します。 | | 18 | | | | クライアント開発環境の確認 | 開発者ごとのクライアント端末における開発環境を確認します。 | | 19 | | | | Azure 環境の確認 | Azure OpenAI Service の申請状況、サブスクリプション上での権限の確認をします。 | | 20 | | 2 | Azure サービス構築と設定| | | | 21 | | | |Azure Active Directory| アプリケーションを作成します。 | **製品資料** : [Azure Active Directory のアプリケーションオブジェクトとサービスプリンシパルオブジェクト](https://learn.microsoft.com/ja-jp/azure/active-directory/develop/app-objects-and-service-principals) **リソース作成手順** : [チュートリアル: Microsoft ID プラットフォームにシングルページ アプリケーションを登録する](https://learn.microsoft.com/ja-jp/azure/active-directory/develop/single-page-app-tutorial-01-register-app)| 22 | | | |Azure OpenAI Service| Azure OpenAI Service のリソースを作成し、モデルをデプロイします。 | **製品資料** : [Web](https://azure.microsoft.com/ja-jp/products/cognitive-services/openai-service) / [Document](https://learn.microsoft.com/ja-JP/azure/cognitive-services/openai/overview) **リソース作成手順** : [リソースの作成 & モデルデプロイ](https://learn.microsoft.com/ja-JP/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal) | 23 | | | |Azure App Service| Azure App Service のリソースを作成します。 | **製品資料** : [Web](https://azure.microsoft.com/ja-jp/products/app-service) / [Document](https://learn.microsoft.com/ja-JP/azure/app-service/) **リソース作成資料** : [クイックスタート : Python をデプロイする](https://learn.microsoft.com/ja-jp/azure/app-service/quickstart-python?tabs=flask%2Cwindows%2Cvscode-aztools%2Cvscode-deploy%2Cdeploy-instructions-azportal%2Cterminal-bash%2Cdeploy-instructions-zip-azcli) | 24 | | | |Azure Log Analytics| Azure Log Analytics のリソースを作成します。 | **製品資料** : [Web](https://azure.microsoft.com/ja-jp/products/monitor) / [Document](https://learn.microsoft.com/ja-JP/azure/azure-monitor/) **リソース作成資料** : [チュートリアル : リソースログを収集して表示する](https://learn.microsoft.com/ja-jp/azure/azure-monitor/essentials/tutorial-resource-logs) | 25 | 26 | #### Day2 27 | | | Section | Item | Description | Reference | 28 | |---|--|--|--|--| 29 | | 2' | Azure サービス構築と設定 | 前日の続き | | | 30 | | 3 | クライアント開発環境の構築 | クライアント端末へソフトウェアインストール| Docker、VSCode、Python、Node.js、Azure CLI をインストールします。| | 31 | | 4 | ChatGPT アプリケーション構築| | | | 32 | | | | ローカル環境へのデプロイ| ローカル環境でアプリケーションを構築し、動作確認をします。 | | 33 | | | | Azure 環境へのデプロイ| Azure 環境でアプリケーションをデプロイし、動作確認をします。 | | 34 | | | | 設定 | ログの取得などの追加設定を行い、動作確認をします。 | | 35 | 36 | #### Day3 37 | | | Section | Item | Description | Reference | 38 | |---|--|--|--|--| 39 | | 4' | ChatGPT アプリ構築 |前日の続き| 40 | | 5 | 今後の拡張案 | | アプリケーションを改良するポイントを説明します。 | | 41 | | 6 | 実装のベストプラクティス| | LLM をベースとしたアプリケーション開発におけるベストプラクティスや留意点について説明します。 | | 42 | | 7 | その他 | | | 43 | | | | Q&A | | 44 | | | | 参考情報 | | 45 | 46 | 47 | ## FastTrack for Azure 48 | FastTrack for Azure は、Azure の迅速に & 確実な構築を支援するカスタマーサクセスプログラムです。Azure 製品部門に所属する Engineer と Program Manager が担当します。Azure をご利用いただくお客様やパートナー様がご利用になれます。 49 | 50 | なお、6月21日(水) 13:00 より Program Manager による FastTrack for Azure 紹介セッションが予定されています。ぜひ参加ください。 51 | :point_right: [プログラムのご紹介(FTA Intro)セッション](https://developer.microsoft.com/en-us/reactor/events/17982/) 52 | 53 | ## アーキテクチャ 54 | 本セッションで構築するアプリケーションの全体像は以下の通りです。 55 | 56 | **開発環境の要件** 57 | 58 | 59 | 60 | ## クライアント開発環境の確認 61 | クライアント端末における開発環境の要件を[こちら](https://github.com/Azure/openai-at-scale/wiki/%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%81%AE%E8%A6%81%E4%BB%B6)よりご確認ください。 62 | 63 | 64 | ## Azure 環境の確認 65 | Azure サブスクリプションにおける要件は[こちら](https://github.com/Azure/openai-at-scale/wiki/Azure-%E3%82%B5%E3%83%96%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%A6%81%E4%BB%B6)よりご確認ください。 66 | 67 | 68 | 69 | --- 70 | # 2. Azure サービス構築と設定 🛠️ 71 | ## Azure Active Directory アプリケーション 🔑 72 | **製品概要** 73 | - [Azure Active Directory とは](https://learn.microsoft.com/ja-jp/azure/active-directory/fundamentals/active-directory-whatis) 74 | - [Azure Active Directory のアプリケーションオブジェクトとサービスプリンシパルオブジェクト](https://learn.microsoft.com/ja-jp/azure/active-directory/develop/app-objects-and-service-principals) 75 | 76 | **構築と設定** 77 | 1. [チュートリアル: Microsoft ID プラットフォームにシングルページ アプリケーションを登録する](https://learn.microsoft.com/ja-jp/azure/active-directory/develop/single-page-app-tutorial-01-register-app) の内容に従って、Azure Active Directory アプリケーションを作成します。 78 | - **`Single-page application (SPA)`** をプラットフォームとして選択します。 79 | - リダイレクト URI は、ローカル開発用に **`http://localhost:5000`** と **`http://localhost:5173`** を設定しておきます。 80 | 1. 下記の情報を控えておきます。 81 | - クライアント ID 82 | - テナント ID 83 | 84 | ## Azure OpenAI Service 🧠 85 | **製品概要** 86 | - [Azure OpenAI Service とは](https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/overview) 87 | - [Azure OpenAI Service モデル](https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/concepts/models) 88 | 89 | 90 | **構築と設定** 91 | Azure OpenAI Service のリソースを作成する方法はいくつかございますが、Azure CLI に慣れていない方は Azure Portal から作成することをおすすめします。 92 | 93 | 始める前に、Azure OpenAI Service のリージョンを選択する必要があります。利用可能なリージョンは [こちら](https://azure.microsoft.com/ja-JP/explore/global-infrastructure/products-by-region/?regions=all&products=cognitive-services) から確認できます。 94 | 95 | 1. [Azure OpenAI を使用してリソースを作成し、モデルをデプロイする](https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal) の内容に従って、Azure OpenAI Serivce のリソースを作成します。 96 | - モデル : ChatGPT (gpt-35-turbo) 97 | - 既存のリソースがある場合はそちらをご利用いただいて構いません。 98 | - ⚠ Azure OpenAI Servce のリソース作成や、モデルのデプロイに時間がかかる場合があります。 99 | 100 | 1. 下記の情報を取得します。 101 | - エンドポイント名 102 | - キー 103 | - モデルデプロイ名 104 | 105 | 106 | 107 | ## Azure App Service 🌐 108 | 109 | **製品概要** 110 | - [App Service について](https://learn.microsoft.com/ja-jp/azure/app-service/overview) 111 | 112 | **構築と設定** 113 | 114 | App Service は、まず初めに、App Service Plan を作り、その上に Web App として、アプリをデプロイする流れとなります。ここでは [クイックスタート:Python Web アプリを Azure App Service にデプロイする](https://learn.microsoft.com/ja-jp/azure/app-service/quickstart-python?tabs=flask%2Cwindows%2Cazure-cli%2Cvscode-deploy%2Cdeploy-instructions-azportal%2Cterminal-bash%2Cdeploy-instructions-zip-azcli) 115 | を参考に、`Azure Portal` を用いて App Service Plan の作成と Web App のデプロイを行います。 116 | 117 | > コマンドライン (azure-cli) を用いても同様の操作が可能です。上記クイックスタートのドキュメントにも記載がありますが、`az webapp up` コマンドを用いると、リソースグループの作成、App Service Plan の作成、Web App のデプロイまで一括で行うことができます。 118 | 119 | ## Azure Log Analytics 🖌️ 120 | 121 | **製品概要** 122 | 123 | [Azure Monitor ログの概要](https://learn.microsoft.com/ja-jp/azure/azure-monitor/logs/data-platform-logs) 124 | 125 | **構築と設定** 126 | 127 | Azure Log Analytics は Azure 上で様々なデータソースからログを収集し、分析を行うための Azure Monitor の機能です。ここでは Log Analytics ワークスペースを作成し、リソースのログを収集します。 128 | 129 | - 次のドキュメントを参考に、Log Analytics ワークスペースを作成します。 130 | 131 | - [Log Analytics ワークスペースを作成する](https://learn.microsoft.com/ja-jp/azure/azure-monitor/logs/quick-create-workspace?tabs=azure-portal) 132 | 133 | 134 | - 多くの Azure リソースは診断設定を持っており、様々なタイプのログを Log Analytics ワークスペースに取り込むことができます。 135 | 136 | - [Azure Monitor の診断設定](https://learn.microsoft.com/ja-jp/azure/azure-monitor/essentials/diagnostic-settings?tabs=portal) 137 | 138 | Log Analytics ワークスペースを作成し、App Service のログを取り込むためのコマンドの例は次のリンクを参照してください。 139 | 140 | - [Azure Log Analytics によるアプリケーションログの収集](#azure-log-analytics-によるアプリケーションログの収集) 141 | 142 | **ログの分析** 143 | - Log Analytics ワークスペースに集約されたログは Kusto Query Language (KQL) という言語を使用して検索や可視化を行うことができます。基本的な使い方は[こちらのチュートリアル](https://learn.microsoft.com/ja-jp/azure/azure-monitor/logs/log-analytics-tutorial)を参照してください。 144 | 145 | - 言語の詳細なリファレンスは以下のドキュメントを参考にしてください。 146 | - [Kusto クエリ言語の概要](https://learn.microsoft.com/ja-jp/azure/data-explorer/kusto/query/) 147 | 148 | 149 | ## (任意) Azure Cosmos DB の作成 🪐 150 | **製品概要** 151 | 152 | [Azure Cosmos DB の概要](https://learn.microsoft.com/ja-jp/azure/cosmos-db/introduction) 153 | 154 | **構築と設定** 155 | 156 | Azure ドキュメント [こちら](https://docs.microsoft.com/ja-jp/azure/cosmos-db/create-cosmosdb-resources-portal) を参考に Azure Cosmos DB を作成します。また、Azure Cosmos DB に Analytical Store を有効にする必要があります。詳細は [こちら](https://docs.microsoft.com/ja-jp/azure/cosmos-db/analytical-store-introduction) を参照してください。 157 | 158 | - **`Core (SQL)`** を API として選択します。 159 | - コンテナ名は **`chat_log`** とし、パーティションキーは **`/chat_session_id`** とします。 160 | 161 | 162 | 163 | --- 164 | # 3. クライアント開発環境の構築 🖥️ 165 | 開発で必要なソフトウェアをクライアント端末にインストールします。 166 | 167 | **GitHub Codespaces を利用する場合** 168 | 1. https://github.com/Azure/openai-at-scale にアクセス 169 | 1. Codespaces の起動 170 | - 4-core 以上を推奨 171 | 1. Python、Node.js バージョン確認 172 | ```shell 173 | python --version 174 | node --version 175 | ``` 176 | 1. Azure CLI による認証確認 177 | ```shell 178 | az login --use-device 179 | ``` 180 | 181 | **Docker が利用可能な場合** 182 | 1. Docker Desktop (もしくは代替ソフトウェア) の起動 183 | 1. クライアント端末へのコードのダウンロード 184 | ```shell 185 | git clone https://github.com/Azure/openai-at-scale 186 | ``` 187 | 1. Visual Studio Code Dev Container の起動 188 | 1. Python、Node.js バージョン確認 189 | ```shell 190 | python --version 191 | node --version 192 | ``` 193 | 1. Azure CLI による認証確認 194 | ```shell 195 | az login --use-device 196 | ``` 197 | 198 | 199 | **Docker が利用不可の場合** 200 | 1. クライアント端末へのコードのダウンロード 201 | ```shell 202 | git clone https://github.com/Azure/openai-at-scale 203 | ``` 204 | 1. Visual Studio Codeの起動 205 | 206 | 1. Python、Node.js、Azure CLI のインストール 207 | 208 | 1. Python、Node.js バージョン確認 209 | ```shell 210 | python --version 211 | node --version 212 | ``` 213 | 1. Azure CLI による認証確認 214 | ```shell 215 | az login --use-device 216 | ``` 217 | 218 | --- 219 | # 4. ChatGPT アプリケーション構築 🤖 220 | ## ローカル環境へのデプロイ 221 | ### 環境変数の設定 222 | 223 | アプリケーションを起動する前に、環境変数を設定するために、`.env.sample` ファイルを参考に `.env` ファイルを作成します。 224 | 225 | - `app/frontend/.env` 226 | - Azure Active Directory の SDK で利用されます. 227 | 228 | ```shell 229 | # Azure Active Directory application 230 | VITE_CLIENTID="" 231 | VITE_TENANTID="" 232 | ``` 233 | 234 | - `app/fronend/src/aadConfig.ts` 235 | - Azure Active Directory による認証で用いられます。 236 | 237 | 238 | ```typescript 239 | export const aadConfig = { 240 | clientId: import.meta.env.VITE_CLIENTID, 241 | tenantId: import.meta.env.VITE_TENANTID, 242 | redirectUri: "http://localhost:5173", 243 | authorityBaseUrl: "https://login.microsoftonline.com/" 244 | }; 245 | ``` 246 | 247 | >⚠ 今は変更する必要はありませんが、以降の手順でアプリケーションを起動した際は、Web アプリケーションの環境 (URL、ポート) に応じて redirectUri を変更する必要があります。 248 | >- GitHub Codespaces を用いている場合は、ポート転送で Web アプリケーションにアクセスします。そのため、redirectUri は `http://localhost:5173` ではなく、Codespaces 環境の URL に変更します。ポート番号 5173 の場合は、`https://xxxx-5173.preview.app.github.dev/` のようになります。URL は Visual Studio Code のポートタブから確認できます。 249 | >- また、Azure Active Directory アプリケーションのリダイレクト URI の設定にもこの URL を適宜追加します。 250 | 251 | 252 | 253 | - `app/backend/.env` 254 | - Azure OpenAI Service と Azure Cosmos DB への接続に利用されます。 255 | 256 | ```shell 257 | # Azure OpenAI Service 258 | AZURE_OPENAI_SERVICE="" 259 | OPENAI_API_KEY="" 260 | AZURE_OPENAI_CHATGPT_DEPLOYMENT="" 261 | 262 | 263 | # (Optional) Azure Cosmos DB 264 | # AZURE_COSMOSDB_ENDPOINT="https://.documents.azure.com:443/" 265 | # AZURE_COSMOSDB_KEY="" 266 | # AZURE_COSMOSDB_DB="< your Azure Cosmos DB database name>" 267 | ``` 268 | > ⚠ 本番環境においては [Azure Key Vault](https://azure.microsoft.com/ja-jp/products/key-vault) を用いて環境変数を設定することを推奨します。 269 | 270 | 271 | Azure CLI を用いて環境変数を取得するコマンド例 272 | 273 | ```shell 274 | export RESOURCE_GROUP= 275 | export AZURE_OPENAI_SERVICE= 276 | export AZURE_OPENAI_CHATGPT_DEPLOYMENT= 277 | export OPENAI_API_KEY=`az cognitiveservices account keys list \ 278 | -n $AZURE_OPENAI_SERVICE \ 279 | -g $RESOURCE_GROUP \ 280 | -o json \ 281 | | jq -r .key1` 282 | ``` 283 | 284 | 285 | 286 | ### Python 環境 287 | 288 | Python は Flask アプリケーションを稼働させるために必要です。 289 | 290 | #### Python ライブラリのインストール 291 | 292 | ```shell 293 | cd app/backend 294 | python -m venv ./backend_env 295 | source ./backend_env/bin/activate #bash 296 | pip install -r requirements.txt 297 | ``` 298 | 299 | #### バックエンドアプリケーションの開始 (Flask) 300 | 301 | ```shell 302 | cd app/backend 303 | flask run --debug #hot reload 304 | #python ./app.py 305 | ``` 306 | 307 | ### Node.js 環境 308 | 309 | Node.js is は React アプリケーションを稼働させるために必要です。 310 | 311 | #### Node.js パッケージのインストール 312 | 313 | ```shell 314 | cd app/frontend 315 | npm install 316 | ``` 317 | 318 | #### フロントエンドアプリケーションの開始 (React) 319 | 開発用途 320 | 321 | ```shell 322 | npm run dev 323 | ``` 324 | 325 | 本番用途 326 | 327 | ```shell 328 | npm run build 329 | ``` 330 | 331 | > このコマンドは app/backend/static フォルダにデプロイされるアプリケーションファイルのサイズを最適化し削減します。 332 | 333 | ### 動作確認 334 | - ローカル環境で Chat 機能や認証機能が使えることを確認します。エラーが発生した場合は、修正してください。 335 | - 本リポジトリでの実装では認証機能の利用は任意です。ユーザ認証しない場合は Anonymous というユーザでアプリケーションをご利用になれます。 336 | 337 | 338 | 339 | ## Azure へのデプロイ ☁️ 340 | 341 | ### Azure App Service へのデプロイ 342 | 343 | - `az webapp up` コマンドを用いたアプリケーションのデプロイ 344 | - aadConfig.ts ファイルの redirectURI を Web アプリケーションの FQDN に更新します。 345 | > ⚠ Web アプリケーションの名前はグローバルで一意である必要があります。 346 | ```typescript 347 | export const aadConfig = { 348 | clientId: import.meta.env.VITE_CLIENTID, 349 | tenantId: import.meta.env.VITE_TENANTID, 350 | redirectUri: "https://.azurewebsites.net/", //Edit here 351 | authorityBaseUrl: "https://login.microsoftonline.com/" 352 | }; 353 | ``` 354 | 355 | - フロントエンドアプリを構築します。 356 | ```shell 357 | cd app/frontend 358 | npm run build 359 | ``` 360 | > ⚠ `npm run build` を実行することで、フロントエンドファイルが app/backend/static に配置されます。 361 | 362 | - Azure Active Directory アプリケーションのリダイレクト URI に Web アプリケーションの FQDN (`http://.azurewebsites.net/`) を追加します。 363 | 364 | - ここでは簡単のために、`az webapp up` コマンドを用いて、Azure App Service と Web アプリケーションを同時にデプロイします。まず dryrun オプションを用いて、`The webapp '' doesn't exist` と表示されるか確認します。 365 | > ⚠ エラーが発生した場合、Web アプリケーションの名前がグローバルで一意になっていない可能性があります。その場合は、新たに Web アプリケーションの名前を決めていただき、本セクションの最初からやり直してください。 366 | ```shell 367 | cd app/backend 368 | az webapp up -n --dryrun 369 | #az webapp up --runtime "python:3.10" --sku B1 -n -p -g --dryrun 370 | ``` 371 | 372 | - 問題がなければ、`az webapp up` コマンドを実行します。 373 | ```shell 374 | cd app/backend 375 | az webapp up --runtime "python:3.10" --sku B1 -n -p -g 376 | ``` 377 | 378 | - Web アプリケーションをデプロイした後、`Azure Portal` の Azure App Service のアプリケーション設定で以下の環境変数を変更します。 379 | - OPENAI_API_KEY 380 | - AZURE_OPENAI_CHATGPT_DEPLOYMENT 381 | - AZURE_OPENAI_SERVICE 382 | 383 | - アプリケーションを開き、動作確認をします。 384 | 385 | 386 | (任意) コマンドベースで Azure App Service Plan と Web アプリケーションをデプロイする方法 387 | 388 | 389 | - ⚠ 上記の手順でもアプリケーションをデプロイできますが、詳細な Azure App Service Plan や Web アプリケーションの設定を変更することはできません。また手動で行なっている部分も多く、自動化することができません。コマンドベースで詳細な設定を行いたい場合は、以下の手順を実行してください。 390 | - Azure App Service Plan リソースの作成します。 391 | 392 | ```shell 393 | az appservice plan create -g --is-linux -n --sku --location eastus 394 | ``` 395 | 396 | - 作成した Azure App Service Plan 上に Web アプリケーションのリソースを作成します。 397 | 398 | ```shell 399 | az webapp create -g -n -p -r "python:3.10" 400 | ``` 401 | 402 | ⚡️ 任意: システムにプライベートエンドポイントやVNETの統合を追加する必要がある場合は、以下のオプションを使用して追加できます。 403 | 404 | - VNET Integration 405 | 406 | ```shell 407 | # you need create vnet/subnet before execute this command 408 | az webapp create -g -n -p -r "python:3.10" --vnet --subnet 409 | ``` 410 | 411 | - Private Endpoint 412 | 413 | ```shell 414 | # you need create vnet/subnet webapp before execute this command 415 | az network private-endpoint create \ 416 | -n \ 417 | -g \ 418 | --vnet-name \ 419 | --subnet \ 420 | --connection-name \ 421 | --private-connection-resource-id /subscriptions/SubscriptionID/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/ \ 422 | --group-id sites 423 | ``` 424 | 425 | - aadConfig.ts ファイルの redirectURI を更新し、フロントエンドアプリを再構築します 426 | - 以下で出力される FQDN を使用して、redirectURI を更新してください。これは Webアプリケーションのエンドポイントです。 427 | 428 | ```shell 429 | az webapp config hostname list -g --webapp-name -o json | jq '.[0].name' 430 | ``` 431 | 432 | - フロントの再ビルド 433 | 434 | ```shell 435 | cd app/frontend 436 | npm run build 437 | ``` 438 | 439 | - Web アプリケーションのデプロイ前に、Azure App Service のアプリケーション設定で環境変数を変更する必要があります。 440 | 441 | ```shell 442 | az webapp config appsettings set --name -g --settings SCM_DO_BUILD_DURING_DEPLOYMENT="true" 443 | ``` 444 | 445 | - Web アプリケーションのデプロイ 446 | 447 | ```shell 448 | cd app/backend 449 | zip -r deploy.zip . 450 | az webapp deploy -g -n --src-path deploy.zip --type zip 451 | ``` 452 | 453 | - Web アプリケーションをデプロイした後、Azure App Service のアプリケーション設定で環境変数を変更する必要があります。 454 | 455 | ```shell 456 | az webapp config appsettings set --name -g --settings OPENAI_API_KEY= AZURE_OPENAI_CHATGPT_DEPLOYMENT= AZURE_OPENAI_SERVICE= 457 | ``` 458 | 459 | 460 | 461 | 462 | ## 設定 ⚙️ 463 | ### Azure Log Analytics によるアプリケーションログの収集 464 | 465 | - ログ収集の例 466 | - Azure Log Analytics workspace のデプロイ 467 | 468 | ```shell 469 | export APP_SERIVCE= 470 | export LOCATION= 471 | export RESOURCE_GROUP= 472 | export WORKSPACE= 473 | export DIAGSETTINNG_NAME= 474 | 475 | az monitor log-analytics workspace create --name $WORKSPACE --resource-group $RESOURCE_GROUP --location $LOCATION 476 | ``` 477 | 478 | - 診断設定の有効化 479 | 480 | ```shell 481 | export RESOURCE_ID=`az webapp show -g $RESOURCE_GROUP -n $APP_SERIVCE --query id --output tsv | tr -d '\r'` 482 | export WORKSPACE_ID=`az monitor log-analytics workspace show -g $RESOURCE_GROUP --workspace-name $WORKSPACE --query id --output tsv | tr -d '\r'` 483 | 484 | az monitor diagnostic-settings create \ 485 | --resource $RESOURCE_ID \ 486 | --workspace $WORKSPACE_ID \ 487 | -n $DIAGSETTINNG_NAME \ 488 | --logs '[{"category": "AppServiceAppLogs", "enabled": true},{"category": "AppServicePlatformLogs", "enabled": true},{"category": "AppServiceConsoleLogs", "enabled": true},{"category": "AppServiceAuditLogs", "enabled": true},{"category": "AppServiceHTTPLogs", "enabled": true}]' 489 | ``` 490 | 491 | 492 | ### (オプション) プロンプトログの Azure Cosmos DB への格納 493 | [Logging chat on Azure Cosmos DB](../en/logging_cosmosdb.md) のドキュメント (英語) では、チャットメッセージを Azure Cosmos DB にログ出力し、さらにダウンストリームで洞察を導出する方法について詳しく説明しています。 494 | 495 | 496 | 497 | --- 498 | # 5. 今後の拡張案 💡 499 | 500 | 本ハンズオンで構築したアプリケーションは非常にシンプルで Sandbox としての利用用途を想定しています。今後より本格的にビジネスで活用していくためには、改良が必要になります。 501 | 502 | ## 標準的なセキュア化 503 | 504 | クラウドサービスを利用する際に考慮る必要がある全体的なセキュリティ コントロールや、各リソースのセキュリティ設定の推奨を確認するためには [Microsoft Cloud Security Benchmark](https://learn.microsoft.com/ja-jp/security/benchmark/azure/overview) を参照します。ここでは一般的に行われるセキュリティ対策として、Azure OpenAI Service のキーの保護と、ネットワーク制御の一部を紹介します。 505 | 506 | 507 | 508 | ### シークレットの保護 509 | 510 | Azure OpenAI Serviceにアクセスするキーはセキュアに取り扱う必要があります。キーを Key Vault に保存することで、複数のアプリケーションでキーを利用する際の運用を簡単にし、アプリケーションへの権限とキーに対するアクセスの権限を分けることができます。 511 | 512 | 513 | 手順の概要 514 | 515 | ※ この手順の実行には `Microsoft.Authorization/roleAssignments/write` および `Microsoft.Authorization/roleAssignments/delete` のアクセス許可 (ユーザー アクセス管理者や所有者など) が必要です。 516 | 517 | - App Service で Managed ID を有効化します。 518 | - Key Vault を作成します。 519 | - Key Vault の RBAC でシークレットの操作を行うユーザーに [キー コンテナー シークレット責任者] を割り当てます。 520 | - シークレットに Azure OpenAI Service のキーを保存します。 521 | - Key Vault の RBAC で App Service の Managed ID に [キー コンテナー シークレット ユーザー] を割り当てます。 522 | - App Service のアプリケーション設定で次の環境変数を追加します。 523 | **名前 : OPENAI_API_KEY、値 : @Microsoft.KeyVault(SecretUri=<シークレットのURI>)** 524 | [参考:App Service と Azure Functions の Key Vault 参照を使用する](https://learn.microsoft.com/ja-jp/azure/app-service/app-service-key-vault-references?tabs=azure-cli) 525 | 526 | 527 | 528 | ### ネットワークアクセスの制御 529 | 530 | 今回のハンズオンの環境は複数の PaaS サービスを使用しています。PaaS サービスはパブリック インターネットからアクセスすることが可能ですが、ほとんどのサービスではネットワーク アクセスの経路を制限するための設定を持っています。以下はパブリック インターネットからのアクセスを許可された経路だけに制限し、リソース間のアクセスをプライベート ネットワークで行うための構成です。 531 | 532 | - 仮想ネットワークの構成 533 | リソース間のネットワークアクセスをプライベート ネットワーク経由にするために仮想ネットワークを作成します。各 PaaS リソースのプライベート エンドポイントを作成するため、リソースの種類ごとにサブネットを作成します。 534 | 535 | - Application Gateway の作成 536 | アプリケーションの負荷分散とアクセス制御、Web Application Firewall を使用して既知の攻撃の検出とブロックを行うために Application Gateway を作成します。 537 | 538 | 手順の概要 539 | 540 | - App Service をバックエンドプールに追加し、Application Gateway 経由でアプリケーションにアクセスできるようにします。 541 | - Application Gateway へのアクセスは許可された IP からのみの通信に制限します。 542 | - Application Gateway の診断ログを Log Analytics ワークスペースに送信します。 543 | 544 | 545 | 546 | - App Service の閉域化 547 | App Service はアプリケーションに対するインバウンド接続と、App Service からのアウトバウンド接続は異なる設定を持つため、それぞれにプライベート通信のための接続を行います。 548 | 549 | 550 | 手順の概要 551 | 552 | - App Service でプライベート エンドポイントを作成します。 553 | - 「パブリック アクセスを許可する」を無効化し、パブリック インターネットからのアクセスが無効になることを確認します。 554 | - App Service で VNET 統合を有効化し、アウトバウンド接続が仮想ネットワークを経由する設定にします。 555 | 556 | ※ この構成により、パブリック インターネットからのアプリケーション コードのアップロードも制限されるようになります。アプリケーションを更新する場合には仮想ネットワークを経由するアクセスとするか、一時的にパブリックアクセスを許可する必要があります。 557 | 558 | 559 | 560 | - Key Vault の閉域化 561 | 562 | Key Vault に対するアクセスは Managed ID と RBAC によるセキュアな認証と認可が行われますが、ネットワーク制御を行うことで攻撃面を小さくすることができます。 563 | 564 | 565 | 手順の概要 566 | 567 | - Key Vault でプライベートエンドポイントを作成します。 568 | - 「パブリック アクセスの無効化」を選択し、パブリック インターネットからのアクセスが無効になることを確認します。 569 | - Key Vault の診断ログを Log Analytics ワークスペースに送信します。 570 | 571 | ※ App Service の VNET 統合が既に有効化されているため、App Service からのアクセスは仮想ネットワーク経由で行われます。 572 | 573 | 574 | 575 | - Azure OpenAI Service の閉域化 576 | 577 | Azure OpenAI Service にプライベート エンドポイントを構成し、全てのサービス間の通信がプライベート ネットワークを経由するようにします。 578 | 579 | 580 | 手順の概要 581 | 582 | - Azure OpenAI Service でプライベート エンドポイントを作成します。 583 | - 許可するアクセス元を [無効] に設定し、パブリック インターネットからのアクセスが無効になることを確認します。 584 | 585 | ※ この構成により、パブリック インターネット経由でアクセスするローカルコンピューター上のアプリケーションからのアクセスや、Azure OpenAI Studio からのアクセスが制限されます。現在この構成の有効化 / 無効化には数時間程度かかる場合があるため注意してください。 586 | 587 | 588 | 589 | ## API Management 590 | API Management(以下、APIM) には、[こちら](https://learn.microsoft.com/ja-jp/azure/api-management/api-management-key-concepts) に紹介されているように、API ゲートウェイとしての機能、管理プレーンとして API を構成するための機能、API を開発者として利用するための、開発者ポータルなどがあります。特に今回のように、frontend のための backend API を公開するケースにおいては、APIM のゲートウェイ機能のAPI呼び入れ、JWTトークン、使用量のレートリミット、ロギングなどの機能が役にたつでしょう。 591 | 592 | ## デプロイの選択肢 593 | 今回の環境では、Azure のアプリケーション PaaS として運用が容易である、Azure App Service を使いましたが、その他のオプションについては、[Container Apps と他の Azure コンテナー オプションの比較](https://learn.microsoft.com/ja-jp/azure/container-apps/compare-options) と [Azure でアプリケーションをホストする](https://learn.microsoft.com/ja-jp/azure/developer/intro/hosting-apps-on-azure) のドキュメントを参考に選択すると良いでしょう。また、アプリケーションをどこにホストするか迷った時には、[Azure コンピューティング サービスを選択する](https://learn.microsoft.com/ja-jp/azure/architecture/guide/technology-choices/compute-decision-tree) のフローチャートも参考にしていただけたらと思います。 594 | 595 | 596 | 597 | --- 598 | # 6. 実装のベストプラクティス 📚 599 | ChatGPT などの LLM をベースとしたアプリケーション開発におけるベストプラクティスや留意点を説明します。 600 | 601 | ## Fine Tuning vs Prompt Engineering 602 | OpenAI 社が提供するモデルは汎用的なものであるため、ファインチューニングやプロンプトエンジニアリングによって工夫する必要があります。ファインチューニングは特定のタスクに適応しやすくなり高い精度が期待できる一方で、学習のための大量データが必要で、計算リソースのコストも高くなります。一方でプロンプトエンジニアリングは、プロンプトを加工するのみでタスクに適応することができる可能性がありますが、プロンプト長の制限や、プロンプトの組み合わせによるパフォーマンスの懸念があります。 603 | 604 | - [プロンプトエンジニアリングの概要](https://learn.microsoft.com/ja-JP/azure/cognitive-services/openai/concepts/prompt-engineering) 605 | 606 | ## User Experience と責任のある AI 607 | 大規模言語モデルは応答時間が長いことがあります。簡単な入力に関してはルールベースで定型文を返すことでパフォーマンスを向上したり、ストリーム処理で結果を返すなどの UI/UX の工夫が必要になります。 608 | 609 | また、大規模言語モデルは halluciation と呼ばれる現象が発生する可能性があります。そのため、モデルのアウトプットをモニタリングしたり制御することが必要になるケースがあります。 610 | 611 | 更には、透明性を保つための緩和策を考えることも重要で、フィードバックの機能を取り入れたり、モデルの出力をユーザーが確認できて必要に応じて編集できるなどの人間を中心としたシステム設計が必要になってきます。 612 | 613 | **参考情報** 614 | - [Azure OpenAI Service - Transparency Note](https://learn.microsoft.com/en-us/legal/cognitive-services/openai/transparency-note?context=%2Fazure%2Fcognitive-services%2Fopenai%2Fcontext%2Fcontext&tabs=text) 615 | - [HAX Toolkit](https://www.microsoft.com/en-us/haxtoolkit/toolkit-overview/) 616 | 617 | 618 | 619 | --- 620 | # 7. その他 ❓ 621 | ## Q&A 622 | コードに関する質問は GitHub の Issue よりお問い合わせください。 623 | - [Issue - Azure/openai-at-scale](https://github.com/Azure/openai-at-scale/issues) 624 | 625 | ## 参考情報 626 | --------------------------------------------------------------------------------
19 | {error} 20 | 21 | 22 |