├── .devcontainer └── devcontainer.json ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── issue-template.md │ └── weekly-updates.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── azure-dev-validation.yaml │ └── python-test.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE.md ├── README.md ├── app ├── backend.ps1 ├── backend │ ├── app.py │ ├── approaches │ │ ├── __init__.py │ │ ├── approach.py │ │ ├── chatreadretrieveread.py │ │ ├── chatreadretrieveread_cosmosdb.py │ │ ├── readpluginsretrieve.py │ │ ├── readretrieveread.py │ │ └── retrievethenread.py │ ├── core │ │ ├── __init__.py │ │ ├── messagebuilder.py │ │ └── modelhelper.py │ ├── data │ │ └── restaurantinfo.csv │ ├── gunicorn.conf.py │ ├── langchainadapters.py │ ├── lookuptool.py │ ├── main.py │ ├── requirements.txt │ └── text.py ├── frontend │ ├── .npmrc │ ├── .prettierrc.json │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── api │ │ │ ├── api.ts │ │ │ ├── index.ts │ │ │ └── models.ts │ │ ├── assets │ │ │ ├── github.svg │ │ │ └── search.svg │ │ ├── components │ │ │ ├── AnalysisPanel │ │ │ │ ├── AnalysisPanel.module.css │ │ │ │ ├── AnalysisPanel.tsx │ │ │ │ ├── AnalysisPanelTabs.tsx │ │ │ │ └── index.tsx │ │ │ ├── Answer │ │ │ │ ├── Answer.module.css │ │ │ │ ├── Answer.tsx │ │ │ │ ├── AnswerError.tsx │ │ │ │ ├── AnswerIcon.tsx │ │ │ │ ├── AnswerLoading.tsx │ │ │ │ ├── AnswerParser.tsx │ │ │ │ └── index.ts │ │ │ ├── ClearChatButton │ │ │ │ ├── ClearChatButton.module.css │ │ │ │ ├── ClearChatButton.tsx │ │ │ │ └── index.tsx │ │ │ ├── Example │ │ │ │ ├── Example.module.css │ │ │ │ ├── Example.tsx │ │ │ │ ├── ExampleList.tsx │ │ │ │ └── index.tsx │ │ │ ├── QuestionInput │ │ │ │ ├── QuestionInput.module.css │ │ │ │ ├── QuestionInput.tsx │ │ │ │ └── index.ts │ │ │ ├── SettingsButton │ │ │ │ ├── SettingsButton.module.css │ │ │ │ ├── SettingsButton.tsx │ │ │ │ └── index.tsx │ │ │ ├── SupportingContent │ │ │ │ ├── SupportingContent.module.css │ │ │ │ ├── SupportingContent.tsx │ │ │ │ ├── SupportingContentParser.ts │ │ │ │ └── index.ts │ │ │ └── UserChatMessage │ │ │ │ ├── UserChatMessage.module.css │ │ │ │ ├── UserChatMessage.tsx │ │ │ │ └── index.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── pages │ │ │ ├── NoPage.tsx │ │ │ ├── chat │ │ │ │ ├── Chat.module.css │ │ │ │ └── Chat.tsx │ │ │ ├── layout │ │ │ │ ├── Layout.module.css │ │ │ │ └── Layout.tsx │ │ │ └── oneshot │ │ │ │ ├── OneShot.module.css │ │ │ │ └── OneShot.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── start.ps1 └── start.sh ├── assets └── endpoint.png ├── azure.yaml ├── data ├── 三浦義澄 - Wikipedia.pdf ├── 源実朝 - Wikipedia.pdf ├── 源範頼 - Wikipedia.pdf ├── 源義経 - Wikipedia.pdf ├── 源頼家 - Wikipedia.pdf └── 源頼朝 - Wikipedia.pdf ├── docs ├── appcomponents.png ├── building-on-codespaces.png ├── chatscreen.png ├── checking-your-ui.png ├── codespaces-config.png ├── dotfiles.png ├── how-to-get-url.png ├── run-codespaces.png ├── transaction-tracing.png └── view-creation-log.png ├── infra ├── abbreviations.json ├── core │ ├── ai │ │ └── cognitiveservices.bicep │ ├── host │ │ ├── appservice.bicep │ │ └── appserviceplan.bicep │ ├── monitor │ │ ├── applicationinsights.bicep │ │ └── monitoring.bicep │ ├── search │ │ └── search-services.bicep │ ├── security │ │ └── role.bicep │ └── storage │ │ └── storage-account.bicep ├── main.bicep └── main.parameters.json ├── locustfile.py ├── plugins ├── BushoCafeReservationPluginsDemo.ipynb ├── LICENSE ├── README.md ├── cafe-review-plugin │ ├── .well-known │ │ └── ai-plugin.json │ ├── logo.png │ ├── main.py │ ├── openapi.yaml │ └── requirements.txt └── restaurant-reservation-plugin │ ├── .well-known │ └── ai-plugin.json │ ├── logo.png │ ├── main.py │ ├── openapi.yaml │ └── requirements.txt ├── pyproject.toml ├── requirements-dev.txt ├── scripts ├── prepdocs.ps1 ├── prepdocs.py ├── prepdocs.sh ├── prepdocs_removeall.ps1 ├── prepdocs_removeall.sh ├── requirements.txt ├── roles.ps1 ├── roles.sh └── setup-sql-cosmosdb.sh └── tests ├── conftest.py ├── snapshots └── test_app │ ├── test_ask_rtr_hybrid │ └── result.json │ ├── test_ask_rtr_text │ └── result.json │ ├── test_ask_rtr_text_semanticcaptions │ └── result.json │ ├── test_ask_rtr_text_semanticranker │ └── result.json │ ├── test_chat_hybrid │ └── result.json │ ├── test_chat_stream_text │ └── result.jsonlines │ ├── test_chat_text │ └── result.json │ ├── test_chat_text_semanticcaptions │ └── result.json │ ├── test_chat_text_semanticranker │ └── result.json │ └── test_chat_vector │ └── result.json ├── test_app.py ├── test_messagebuilder.py ├── test_modelhelper.py └── test_prepdocs.py /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure Developer CLI", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.10", 4 | "features": { 5 | "ghcr.io/devcontainers/features/node:1": { 6 | "version": "16", 7 | "nodeGypDependencies": false 8 | }, 9 | "ghcr.io/devcontainers/features/powershell:1.1.0": {}, 10 | "ghcr.io/devcontainers/features/azure-cli:1.0.8": {}, 11 | "ghcr.io/azure/azure-dev/azd:latest": {} 12 | }, 13 | "customizations": { 14 | "vscode": { 15 | "extensions": [ 16 | "ms-azuretools.azure-dev", 17 | "ms-azuretools.vscode-bicep", 18 | "ms-python.python" 19 | ] 20 | } 21 | }, 22 | "forwardPorts": [ 23 | 50505 24 | ], 25 | "postCreateCommand": "cd app;./start.sh", 26 | "remoteUser": "vscode", 27 | "hostRequirements": { 28 | "memory": "8gb" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf -------------------------------------------------------------------------------- /.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/issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: issue 3 | about: 4 | title: 5 | labels: 6 | assignees: 7 | --- 8 | ## Why 9 | 10 | ## What 11 | 12 | > Please provide us with the following information: 13 | > --------------------------------------------------------------- 14 | 15 | ### This issue is for a: (mark with an `x`) 16 | ``` 17 | - [ ] bug report -> please search issues before submitting 18 | - [ ] feature request 19 | - [ ] documentation issue or request 20 | - [ ] regression (a behavior that used to work and stopped in a new release) 21 | ``` 22 | 23 | ### Minimal steps to reproduce 24 | > 25 | 26 | ### Any log messages given by the failure 27 | > 28 | 29 | ### Expected/desired behavior 30 | > 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/weekly-updates.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'weekly updates' 3 | about: '' 4 | title: 'YYYY-MM-DD Weekly Update' 5 | labels: 'weekly updates' 6 | assignees: 'koudaiii' 7 | --- 8 | ## Why 9 | 10 | エンジニアの仕事がサポートチームやビジネスチームへの共有が少ないために連携不足が続き、顧客満足度や売上に影響しているのを解決したい。 11 | 12 | - ある日突然仕様が変わったり、ヘルプページが追いついていない 13 | - トラブルの場合にお客さまから状況確認して社内で確認するといった後手後手のフローになっている 14 | - ビジネスチームも新機能であったり、修正されたことを知らないため、プロダクトを知り尽くした踏み込んだコミュニケーションがしにくい 15 | - 便利な社内ツールを知らない 16 | 17 | 今後できるものやリリースしたものなどをアピールする場を作る。 18 | 19 | ## What 20 | 21 | 次の例のようにコメントで自身の成果をアピールしましょう! 22 | 社内環境のようなものは [internal] というタイトルを付けて投稿しましょう 😄 23 | 案外隣のエンジニアチームがなにやっているかわからないものです。隣の人にアピールするつもりでどんどん書いていきましょう! 24 | 25 | ```markdown 26 | [internal] Codespaces に対応しました! 27 | これまで開発環境を作るのが大変だったのが、 Codespaces を使うと簡単に開発環境がセットアップできて開発と確認ができます。 28 | データや Cognitive Search 等はすべて本番環境を使っています。 29 | 30 | ref: #62 codespaces だけで開発できるようにする 31 | ``` 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Why 2 | 3 | 4 | * ... 5 | 6 | ## What 7 | 8 | ### Does this introduce a breaking change? 9 | 10 | ``` 11 | [ ] Yes 12 | [ ] No 13 | ``` 14 | 15 | ### Pull Request Type 16 | What kind of change does this Pull Request introduce? 17 | 18 | 19 | ``` 20 | [ ] Bugfix 21 | [ ] Feature 22 | [ ] Code style update (formatting, local variables) 23 | [ ] Refactoring (no functional changes, no api changes) 24 | [ ] Documentation content changes 25 | [ ] Other... Please describe: 26 | ``` 27 | 28 | ### How to Test 29 | * Get the code 30 | 31 | ``` 32 | git clone [repo-address] 33 | cd [repo-name] 34 | git checkout [branch-name] 35 | npm install 36 | ``` 37 | 38 | * Test the code 39 | 40 | ``` 41 | ``` 42 | 43 | * What to Check 44 | Verify that the following are valid 45 | * ... 46 | * 47 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev-validation.yaml: -------------------------------------------------------------------------------- 1 | name: Validate AZD template 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Build Bicep for linting 17 | uses: azure/CLI@v1 18 | with: 19 | inlineScript: az config set bicep.use_binary_from_path=false && az bicep build -f infra/main.bicep --stdout 20 | 21 | - name: Run Microsoft Security DevOps Analysis 22 | uses: microsoft/security-devops-action@preview 23 | id: msdo 24 | continue-on-error: true 25 | with: 26 | tools: templateanalyzer 27 | 28 | - name: Upload alerts to Security tab 29 | uses: github/codeql-action/upload-sarif@v2 30 | if: github.repository == 'Azure-Samples/azure-search-openai-demo' 31 | with: 32 | sarif_file: ${{ steps.msdo.outputs.sarifFile }} 33 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yaml: -------------------------------------------------------------------------------- 1 | name: Python check 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test_package: 11 | name: Test ${{ matrix.os }} Python ${{ matrix.python_version }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: ["ubuntu-20.04"] 17 | python_version: ["3.9", "3.10", "3.11"] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Setup python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python_version }} 24 | architecture: x64 25 | - name: Setup node 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: 18 29 | - name: Build frontend 30 | run: | 31 | cd ./app/frontend 32 | npm install 33 | npm run build 34 | - name: Install dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install -r requirements-dev.txt 38 | - name: Lint with ruff 39 | run: ruff . 40 | - name: Run Python tests 41 | run: python3 -m pytest 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Azure az webapp deployment details 2 | .azure 3 | *_env 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # pytype static type analyzer 139 | .pytype/ 140 | 141 | # Cython debug symbols 142 | cython_debug/ 143 | 144 | # NPM 145 | npm-debug.log* 146 | node_modules 147 | static/ 148 | 149 | backend_env 150 | .DS_Store 151 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: '^tests/snapshots/' 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.4.0 5 | hooks: 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - repo: https://github.com/astral-sh/ruff-pre-commit 10 | rev: v0.0.282 11 | hooks: 12 | - id: ruff 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "ms-azuretools.azure-dev" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Quart", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "quart", 12 | "cwd": "${workspaceFolder}/app/backend", 13 | "env": { 14 | "QUART_APP": "main:app", 15 | "QUART_ENV": "development", 16 | "QUART_DEBUG": "0" 17 | }, 18 | "args": [ 19 | "run", 20 | "--no-reload", 21 | "-p 50505" 22 | ], 23 | "console": "integratedTerminal", 24 | "justMyCode": false, 25 | "envFile": "${input:dotEnvFilePath}", 26 | }, 27 | { 28 | "name": "Frontend: watch", 29 | "type": "node", 30 | "request": "launch", 31 | "cwd": "${workspaceFolder}/app/frontend", 32 | "runtimeExecutable": "npm", 33 | "runtimeArgs": [ 34 | "run-script", 35 | "watch" 36 | ], 37 | "console": "integratedTerminal", 38 | }, 39 | { 40 | "name": "Frontend: build", 41 | "type": "node", 42 | "request": "launch", 43 | "cwd": "${workspaceFolder}/app/frontend", 44 | "runtimeExecutable": "npm", 45 | "runtimeArgs": [ 46 | "run-script", 47 | "build" 48 | ], 49 | "console": "integratedTerminal", 50 | } 51 | ], 52 | "inputs": [ 53 | { 54 | "id": "dotEnvFilePath", 55 | "type": "command", 56 | "command": "azure-dev.commands.getDotEnvFilePath" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "editor.formatOnSave": true 9 | }, 10 | "[typescriptreact]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "editor.formatOnSave": true 13 | }, 14 | "[css]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode", 16 | "editor.formatOnSave": true 17 | }, 18 | "search.exclude": { 19 | "**/node_modules": true, 20 | "static": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Start App", 6 | "type": "dotenv", 7 | "targetTasks": [ 8 | "Start App (Script)" 9 | ], 10 | "file": "${input:dotEnvFilePath}" 11 | }, 12 | { 13 | "label": "Start App (Script)", 14 | "type": "shell", 15 | "command": "${workspaceFolder}/app/start.sh", 16 | "windows": { 17 | "command": "pwsh ${workspaceFolder}/app/start.ps1" 18 | }, 19 | "presentation": { 20 | "reveal": "silent" 21 | }, 22 | "options": { 23 | "cwd": "${workspaceFolder}/app" 24 | }, 25 | "problemMatcher": [] 26 | } 27 | ], 28 | "inputs": [ 29 | { 30 | "id": "dotEnvFilePath", 31 | "type": "command", 32 | "command": "azure-dev.commands.getDotEnvFilePath" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 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 | - [Running Tests](#tests) 20 | - [Code Style](#style) 21 | 22 | ## Code of Conduct 23 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 24 | 25 | ## Found an Issue? 26 | If you find a bug in the source code or a mistake in the documentation, you can help us by 27 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 28 | [submit a Pull Request](#submit-pr) with a fix. 29 | 30 | ## Want a Feature? 31 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 32 | Repository. If you would like to *implement* a new feature, please submit an issue with 33 | a proposal for your work first, to be sure that we can use it. 34 | 35 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 36 | 37 | ## Submission Guidelines 38 | 39 | ### Submitting an Issue 40 | Before you submit an issue, search the archive, maybe your question was already answered. 41 | 42 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 43 | Help us to maximize the effort we can spend fixing issues and adding new 44 | features, by not reporting duplicate issues. Providing the following information will increase the 45 | chances of your issue being dealt with quickly: 46 | 47 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 48 | * **Version** - what version is affected (e.g. 0.1.2) 49 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 50 | * **Browsers and Operating System** - is this a problem with all browsers? 51 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 52 | * **Related Issues** - has a similar issue been reported before? 53 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 54 | causing the problem (line of code or commit) 55 | 56 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 57 | 58 | ### Submitting a Pull Request (PR) 59 | Before you submit your Pull Request (PR) consider the following guidelines: 60 | 61 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 62 | that relates to your submission. You don't want to duplicate effort. 63 | * Make your changes in a new git fork 64 | * Follow [Code style conventions](#style) 65 | * [Run the tests](#tests) (and write new ones, if needed) 66 | * Commit your changes using a descriptive commit message 67 | * Push your fork to GitHub 68 | * In GitHub, create a pull request to the `main` branch of the repository 69 | * Ask a maintainer to review your PR and address any comments they might have 70 | 71 | ## Running tests 72 | 73 | Install the development dependencies: 74 | 75 | ``` 76 | python3 -m pip install -r requirements-dev.txt 77 | ``` 78 | 79 | Install the pre-commit hooks: 80 | 81 | ``` 82 | pre-commit install 83 | ``` 84 | 85 | Run the tests: 86 | 87 | ``` 88 | python3 -m pytest 89 | ``` 90 | 91 | ## Code Style 92 | 93 | This codebase includes several languages: TypeScript, Python, Bicep, Powershell, and Bash. 94 | Code should follow the standard conventions of each language. 95 | 96 | For Python, you can enforce the conventions using `ruff` and `black`. 97 | 98 | Install the development dependencies: 99 | 100 | ``` 101 | python3 -m pip install -r requirements-dev.txt 102 | ``` 103 | 104 | Run `ruff` to lint a file: 105 | 106 | ``` 107 | python3 -m ruff 108 | ``` 109 | 110 | Run `black` to format a file: 111 | 112 | ``` 113 | python3 -m black 114 | ``` 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Azure Samples 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 -------------------------------------------------------------------------------- /app/backend.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "" 2 | Write-Host "Loading azd .env file from current environment" 3 | Write-Host "" 4 | 5 | foreach ($line in (& azd env get-values)) { 6 | if ($line -match "([^=]+)=(.*)") { 7 | $key = $matches[1] 8 | $value = $matches[2] -replace '^"|"$' 9 | Set-Item -Path "env:\$key" -Value $value 10 | } 11 | } 12 | 13 | if ($LASTEXITCODE -ne 0) { 14 | Write-Host "Failed to load environment variables from azd environment" 15 | exit $LASTEXITCODE 16 | } 17 | 18 | 19 | Write-Host 'Creating python virtual environment "backend/backend_env"' 20 | $pythonCmd = Get-Command python -ErrorAction SilentlyContinue 21 | if (-not $pythonCmd) { 22 | # fallback to python3 if python not found 23 | $pythonCmd = Get-Command python3 -ErrorAction SilentlyContinue 24 | } 25 | 26 | 27 | Set-Location backend 28 | $venvPythonPath = "./backend_env/scripts/python.exe" 29 | if (Test-Path -Path "/usr") { 30 | # fallback to Linux venv path 31 | $venvPythonPath = "./backend_env/bin/python" 32 | } 33 | 34 | 35 | 36 | Write-Host "" 37 | Write-Host "Starting backend" 38 | Write-Host "" 39 | Set-Location ../backend 40 | 41 | Start-Process -FilePath $venvPythonPath -ArgumentList "-m quart --app main:app run --port 50505 --reload" -Wait -NoNewWindow 42 | 43 | if ($LASTEXITCODE -ne 0) { 44 | Write-Host "Failed to start backend" 45 | exit $LASTEXITCODE 46 | } 47 | -------------------------------------------------------------------------------- /app/backend/app.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | import logging 4 | import mimetypes 5 | import os 6 | import time 7 | from typing import AsyncGenerator 8 | 9 | import aiohttp 10 | import openai 11 | 12 | #Cosmos DB 13 | from azure.identity.aio import DefaultAzureCredential 14 | from azure.monitor.opentelemetry import configure_azure_monitor 15 | from azure.search.documents.aio import SearchClient 16 | from azure.storage.blob.aio import BlobServiceClient 17 | from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor 18 | from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware 19 | from quart import ( 20 | Blueprint, 21 | Quart, 22 | abort, 23 | current_app, 24 | jsonify, 25 | make_response, 26 | request, 27 | send_file, 28 | send_from_directory, 29 | ) 30 | 31 | from approaches.chatreadretrieveread import ChatReadRetrieveReadApproach 32 | from approaches.readpluginsretrieve import ReadPluginsRetrieve 33 | from approaches.readretrieveread import ReadRetrieveReadApproach 34 | from approaches.retrievethenread import RetrieveThenReadApproach 35 | 36 | # Replace these with your own values, either in environment variables or directly here 37 | AZURE_STORAGE_ACCOUNT = os.getenv("AZURE_STORAGE_ACCOUNT", "mystorageaccount") 38 | AZURE_STORAGE_CONTAINER = os.getenv("AZURE_STORAGE_CONTAINER", "content") 39 | AZURE_SEARCH_SERVICE = os.getenv("AZURE_SEARCH_SERVICE", "gptkb") 40 | AZURE_SEARCH_INDEX = os.getenv("AZURE_SEARCH_INDEX", "gptkbindex") 41 | AZURE_OPENAI_SERVICE = os.getenv("AZURE_OPENAI_SERVICE", "myopenai") 42 | AZURE_OPENAI_CHATGPT_MODEL = os.getenv("AZURE_OPENAI_CHATGPT_MODEL", "gpt-35-turbo-16k") 43 | AZURE_OPENAI_GPT_DEPLOYMENT = os.getenv("AZURE_OPENAI_GPT_DEPLOYMENT", "davinci") 44 | AZURE_OPENAI_CHATGPT_DEPLOYMENT = os.getenv("AZURE_OPENAI_CHATGPT_DEPLOYMENT", "chat16k") 45 | AZURE_OPENAI_EMB_DEPLOYMENT = os.getenv("AZURE_OPENAI_EMB_DEPLOYMENT", "embedding") 46 | 47 | KB_FIELDS_CONTENT = os.getenv("KB_FIELDS_CONTENT", "content") 48 | KB_FIELDS_CATEGORY = os.getenv("KB_FIELDS_CATEGORY", "category") 49 | KB_FIELDS_SOURCEPAGE = os.getenv("KB_FIELDS_SOURCEPAGE", "sourcepage") 50 | 51 | CONFIG_OPENAI_TOKEN = "openai_token" 52 | CONFIG_CREDENTIAL = "azure_credential" 53 | CONFIG_ASK_APPROACHES = "ask_approaches" 54 | CONFIG_CHAT_APPROACHES = "chat_approaches" 55 | CONFIG_BLOB_CLIENT = "blob_client" 56 | 57 | APPLICATIONINSIGHTS_CONNECTION_STRING = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") 58 | 59 | bp = Blueprint("routes", __name__, static_folder='static') 60 | 61 | @bp.route("/") 62 | async def index(): 63 | return await bp.send_static_file("index.html") 64 | 65 | @bp.route("/favicon.ico") 66 | async def favicon(): 67 | return await bp.send_static_file("favicon.ico") 68 | 69 | @bp.route("/assets/") 70 | async def assets(path): 71 | return await send_from_directory("static/assets", path) 72 | 73 | # Serve content files from blob storage from within the app to keep the example self-contained. 74 | # *** NOTE *** this assumes that the content files are public, or at least that all users of the app 75 | # can access all the files. This is also slow and memory hungry. 76 | 77 | # この例を自己完結的なものにするために、アプリ内からblobストレージにコンテンツファイルを配信する。 78 | # *** NOTE *** この例では、コンテンツファイルが公開されているか、少なくともアプリの全ユーザーが 79 | # すべてのファイルにアクセスできることを前提としています。また, これは低速でメモリを消費します. 80 | @bp.route("/content/") 81 | async def content_file(path): 82 | logging.info("content_file: " + path) 83 | blob_container = current_app.config[CONFIG_BLOB_CLIENT].get_container_client(AZURE_STORAGE_CONTAINER) 84 | logging.info("blob_container: " + blob_container.get_blob_client(path).url) 85 | blob = await blob_container.get_blob_client(path).download_blob() 86 | if not blob.properties or not blob.properties.has_key("content_settings"): 87 | abort(404) 88 | mime_type = blob.properties["content_settings"]["content_type"] 89 | if mime_type == "application/octet-stream": 90 | mime_type = mimetypes.guess_type(path)[0] or "application/octet-stream" 91 | blob_file = io.BytesIO() 92 | await blob.readinto(blob_file) 93 | blob_file.seek(0) 94 | return await send_file(blob_file, mimetype=mime_type, as_attachment=False, attachment_filename=path) 95 | 96 | @bp.route("/ask", methods=["POST"]) 97 | async def ask(): 98 | if not request.is_json: 99 | return jsonify({"error": "request must be json"}), 415 100 | request_json = await request.get_json() 101 | approach = request_json["approach"] 102 | try: 103 | impl = current_app.config[CONFIG_ASK_APPROACHES].get(approach) 104 | if not impl: 105 | return jsonify({"error": "unknown approach"}), 400 106 | #plugin の場合、非同期だと失敗するので同期で実行 107 | if approach == "rpr": 108 | r = impl.run(request_json["question"], request_json.get("overrides") or {}) 109 | logging.info("ReadPluginsRetrieve is completed.") 110 | else: 111 | r = await impl.run(request_json["question"], request_json.get("overrides") or {}) 112 | return jsonify(r) 113 | except Exception as e: 114 | logging.exception("Exception in /ask") 115 | return jsonify({"error": str(e)}), 500 116 | 117 | @bp.route("/chat", methods=["POST"]) 118 | async def chat(): 119 | if not request.is_json: 120 | return jsonify({"error": "request must be json"}), 415 121 | request_json = await request.get_json() 122 | approach = request_json["approach"] 123 | try: 124 | impl = current_app.config[CONFIG_CHAT_APPROACHES].get(approach) 125 | if not impl: 126 | return jsonify({"error": "unknown approach"}), 400 127 | # Workaround for: https://github.com/openai/openai-python/issues/371 128 | async with aiohttp.ClientSession() as s: 129 | openai.aiosession.set(s) 130 | r = await impl.run_without_streaming(request_json["history"], request_json.get("overrides", {})) 131 | return jsonify(r) 132 | except Exception as e: 133 | logging.exception("Exception in /chat") 134 | return jsonify({"error": str(e)}), 500 135 | 136 | 137 | async def format_as_ndjson(r: AsyncGenerator[dict, None]) -> AsyncGenerator[str, None]: 138 | async for event in r: 139 | yield json.dumps(event, ensure_ascii=False) + "\n" 140 | 141 | @bp.route("/chat_stream", methods=["POST"]) 142 | async def chat_stream(): 143 | if not request.is_json: 144 | return jsonify({"error": "request must be json"}), 415 145 | request_json = await request.get_json() 146 | approach = request_json["approach"] 147 | try: 148 | impl = current_app.config[CONFIG_CHAT_APPROACHES].get(approach) 149 | if not impl: 150 | return jsonify({"error": "unknown approach"}), 400 151 | response_generator = impl.run_with_streaming(request_json["history"], request_json.get("overrides", {})) 152 | response = await make_response(format_as_ndjson(response_generator)) 153 | response.timeout = None # type: ignore 154 | return response 155 | except Exception as e: 156 | logging.exception("Exception in /chat") 157 | return jsonify({"error": str(e)}), 500 158 | 159 | 160 | @bp.before_request 161 | async def ensure_openai_token(): 162 | openai_token = current_app.config[CONFIG_OPENAI_TOKEN] 163 | if openai_token.expires_on < time.time() + 60: 164 | openai_token = await current_app.config[CONFIG_CREDENTIAL].get_token("https://cognitiveservices.azure.com/.default") 165 | current_app.config[CONFIG_OPENAI_TOKEN] = openai_token 166 | openai.api_key = openai_token.token 167 | 168 | @bp.before_app_serving 169 | async def setup_clients(): 170 | 171 | # Use the current user identity to authenticate with Azure OpenAI, Cognitive Search and Blob Storage (no secrets needed, 172 | # just use 'az login' locally, and managed identity when deployed on Azure). If you need to use keys, use separate AzureKeyCredential instances with the 173 | # keys for each service 174 | # If you encounter a blocking error during a DefaultAzureCredential resolution, you can exclude the problematic credential by using a parameter (ex. exclude_shared_token_cache_credential=True) 175 | 176 | # Azure OpenAI、Cognitive Search、Blob Storageでの認証には、現在のユーザーIDを使用します(シークレットは必要ありません、 177 | # ローカルでは 'az login' を使用し、Azure 上にデプロイされている場合はマネージド ID を使用するだけです)。キーを使用する必要がある場合は、各サービスのキーを持つ個別の AzureKeyCredential インスタンスを使用します。 178 | # DefaultAzureCredentialの解決中にブロックエラーが発生した場合、パラメータを使用することで問題のあるクレデンシャルを除外することができます(ex.exclude_shared_token_cache_credential=True) 179 | azure_credential = DefaultAzureCredential(exclude_shared_token_cache_credential = True) 180 | # Set up clients for Cognitive Search and Storage 181 | search_client = SearchClient( 182 | endpoint=f"https://{AZURE_SEARCH_SERVICE}.search.windows.net", 183 | index_name=AZURE_SEARCH_INDEX, 184 | credential=azure_credential) 185 | blob_client = BlobServiceClient( 186 | account_url=f"https://{AZURE_STORAGE_ACCOUNT}.blob.core.windows.net", 187 | credential=azure_credential) 188 | 189 | # Set up a Cosmos DB client to store the chat history 190 | # endpoint = 'https://.documents.azure.com:443/' 191 | # key = '' 192 | # cosmos_container = [] 193 | # try: 194 | # cosmos_client = CosmosClient(url=endpoint, credential=key) 195 | # database = cosmos_client.create_database_if_not_exists(id="ChatGPT") 196 | # partitionKeyPath = PartitionKey(path="/id") 197 | # cosmos_container = database.create_container_if_not_exists( 198 | # id="ChatLogs", partition_key=partitionKeyPath 199 | # ) 200 | # except Exception as e: 201 | # logging.exception(e) 202 | # pass 203 | 204 | # Used by the OpenAI SDK 205 | openai.api_base = f"https://{AZURE_OPENAI_SERVICE}.openai.azure.com" 206 | openai.api_version = "2023-07-01-preview" 207 | openai.api_type = "azure_ad" 208 | openai_token = await azure_credential.get_token( 209 | "https://cognitiveservices.azure.com/.default" 210 | ) 211 | openai.api_key = openai_token.token 212 | 213 | # Store on app.config for later use inside requests 214 | current_app.config[CONFIG_OPENAI_TOKEN] = openai_token 215 | current_app.config[CONFIG_CREDENTIAL] = azure_credential 216 | current_app.config[CONFIG_BLOB_CLIENT] = blob_client 217 | # Various approaches to integrate GPT and external knowledge, most applications will use a single one of these patterns 218 | # or some derivative, here we include several for exploration purposes 219 | # GPTと外部の知識を統合するための様々なアプローチ。ほとんどのアプリケーションは、これらのパターンのうちの1つ、あるいは派生したものを使うでしょう。 220 | # このサンプルでは ReadDecomposeAsk 機能は ChatGPT プラグイン機能に代替しました。 221 | current_app.config[CONFIG_ASK_APPROACHES] = { 222 | "rtr": RetrieveThenReadApproach( 223 | search_client, 224 | AZURE_OPENAI_CHATGPT_DEPLOYMENT, 225 | AZURE_OPENAI_CHATGPT_MODEL, 226 | AZURE_OPENAI_EMB_DEPLOYMENT, 227 | KB_FIELDS_SOURCEPAGE, 228 | KB_FIELDS_CONTENT 229 | ), 230 | "rrr": ReadRetrieveReadApproach( 231 | search_client, 232 | AZURE_OPENAI_CHATGPT_DEPLOYMENT, 233 | AZURE_OPENAI_EMB_DEPLOYMENT, 234 | KB_FIELDS_SOURCEPAGE, 235 | KB_FIELDS_CONTENT 236 | ), 237 | "rpr": ReadPluginsRetrieve( 238 | AZURE_OPENAI_CHATGPT_DEPLOYMENT 239 | ) 240 | } 241 | current_app.config[CONFIG_CHAT_APPROACHES] = { 242 | "rrr": ChatReadRetrieveReadApproach( 243 | search_client, 244 | AZURE_OPENAI_CHATGPT_DEPLOYMENT, 245 | AZURE_OPENAI_CHATGPT_MODEL, 246 | AZURE_OPENAI_EMB_DEPLOYMENT, 247 | KB_FIELDS_SOURCEPAGE, 248 | KB_FIELDS_CONTENT, 249 | ) 250 | # "rrr": ChatReadRetrieveReadApproachCosmosDB ( 251 | # search_client, 252 | # cosmos_container, 253 | # AZURE_OPENAI_CHATGPT_DEPLOYMENT, 254 | # AZURE_OPENAI_CHATGPT_MODEL, 255 | # AZURE_OPENAI_EMB_DEPLOYMENT, 256 | # KB_FIELDS_SOURCEPAGE, 257 | # KB_FIELDS_CONTENT, 258 | # ) 259 | } 260 | 261 | 262 | def create_app(): 263 | if APPLICATIONINSIGHTS_CONNECTION_STRING: 264 | configure_azure_monitor() 265 | AioHttpClientInstrumentor().instrument() 266 | app = Quart(__name__) 267 | app.register_blueprint(bp) 268 | app.asgi_app = OpenTelemetryMiddleware(app.asgi_app) 269 | 270 | return app 271 | -------------------------------------------------------------------------------- /app/backend/approaches/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nohanaga/azure-search-openai-demo/405dc611ad1e0e755a1e964121e6f6c5dcab2869/app/backend/approaches/__init__.py -------------------------------------------------------------------------------- /app/backend/approaches/approach.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | 4 | 5 | class AskApproach(ABC): 6 | @abstractmethod 7 | async def run(self, q: str, overrides: dict[str, Any]) -> dict[str, Any]: 8 | ... 9 | -------------------------------------------------------------------------------- /app/backend/approaches/readpluginsretrieve.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any 3 | 4 | import openai 5 | from langchain.agents import AgentType, initialize_agent, load_tools 6 | from langchain.agents.mrkl import prompt 7 | from langchain.callbacks.manager import CallbackManager 8 | from langchain.chat_models import AzureChatOpenAI 9 | from langchain.tools import AIPluginTool 10 | 11 | from approaches.approach import AskApproach 12 | from langchainadapters import HtmlCallbackHandler 13 | from requests.exceptions import ConnectionError 14 | 15 | class ReadPluginsRetrieve(AskApproach): 16 | def __init__(self, openai_deployment: str): 17 | self.openai_deployment = openai_deployment 18 | 19 | def run(self, q: str, overrides: dict[str, Any]) -> Any: 20 | try: 21 | cb_handler = HtmlCallbackHandler() 22 | cb_manager = CallbackManager(handlers=[cb_handler]) 23 | 24 | #llm = ChatOpenAI(model_name="gpt-4-0613", temperature=0) 25 | llm = AzureChatOpenAI(deployment_name=self.openai_deployment, 26 | temperature=0.0, 27 | openai_api_base=openai.api_base, 28 | openai_api_version=openai.api_version, 29 | openai_api_type=openai.api_type, 30 | openai_api_key=openai.api_key) 31 | tools = load_tools(["requests_all"]) 32 | plugin_urls = ["http://localhost:5005/.well-known/ai-plugin.json", "http://localhost:5006/.well-known/ai-plugin.json"] 33 | 34 | tools += [AIPluginTool.from_plugin_url(url) for url in plugin_urls] 35 | 36 | SUFFIX = """ 37 | Answer should be in Japanese. Use http instead of https for endpoint. 38 | If there is no year in the reservation, use the year 2023. 39 | """ 40 | 41 | # Responsible AI MetaPrompt 42 | #**IMPORTANT** 43 | #If a restaurant reservation is available, must check with the user before making a reservation if yes.' 44 | agent_chain = initialize_agent(tools, 45 | llm, 46 | agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, 47 | #agent=AgentType.OPENAI_FUNCTIONS, 48 | verbose=True, 49 | agent_kwargs=dict(suffix=SUFFIX + prompt.SUFFIX), 50 | handle_parsing_errors=True, 51 | callback_manager = cb_manager, 52 | max_iterations=5, 53 | early_stopping_method="generate") 54 | 55 | result = agent_chain.run(q) 56 | except ConnectionError as e: 57 | logging.exception(e) 58 | result = "すみません、わかりません。(ConnectionError)" 59 | except Exception as e: 60 | logging.exception(e) 61 | result = "すみません、わかりません。(Error)" 62 | 63 | return {"data_points": [], "answer": result, "thoughts": cb_handler.get_and_reset_log()} 64 | -------------------------------------------------------------------------------- /app/backend/approaches/readretrieveread.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import logging 3 | from typing import Any 4 | 5 | import openai 6 | from azure.search.documents.aio import SearchClient 7 | from azure.search.documents.models import QueryType 8 | from langchain.agents import ( 9 | AgentType, 10 | Tool, 11 | initialize_agent, 12 | ) 13 | from langchain.agents.mrkl import prompt 14 | from langchain.callbacks.manager import CallbackManager 15 | from langchain.chat_models import AzureChatOpenAI 16 | from langchain.tools import BaseTool 17 | 18 | from approaches.approach import AskApproach 19 | from langchainadapters import HtmlCallbackHandler 20 | from text import nonewlines 21 | 22 | class ReadRetrieveReadApproach(AskApproach): 23 | """ 24 | 質問に対して、どのような情報が欠けているのかを確認するために、質問を繰り返し評価し、すべての情報が揃ったところで、回答を作成することを試みる。 25 | 各反復は 2 つの部分で構成されています。 26 | 1. GPT を使用して、さらに情報が必要かどうかを確認する。 27 | 2.より多くのデータが必要な場合は、要求された「ツール」を使ってデータを取得する。 28 | GPT への最後の呼び出しが、実際の質問に答える。 29 | これは、MRKL の論文[1]にインスパイアされ、Langchain の実装を使ってここで適用されている。 30 | 31 | [1] E. Karpas, et al. arXiv:2205.00445 32 | """ 33 | 34 | def __init__(self, search_client: SearchClient, openai_deployment: str, embedding_deployment: str, sourcepage_field: str, content_field: str): 35 | self.search_client = search_client 36 | self.openai_deployment = openai_deployment 37 | self.embedding_deployment = embedding_deployment 38 | self.sourcepage_field = sourcepage_field 39 | self.content_field = content_field 40 | 41 | async def retrieve(self, query_text: str, overrides: dict[str, Any]) -> Any: 42 | has_text = overrides.get("retrieval_mode") in ["text", "hybrid", None] 43 | has_vector = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] 44 | use_semantic_captions = True if overrides.get("semantic_captions") and has_text else False 45 | top = overrides.get("top") or 3 46 | exclude_category = overrides.get("exclude_category") or None 47 | filter = "category ne '{}'".format(exclude_category.replace("'", "''")) if exclude_category else None 48 | # If retrieval mode includes vectors, compute an embedding for the query 49 | if has_vector: 50 | query_vector = (await openai.Embedding.acreate(engine=self.embedding_deployment, input=query_text))["data"][0]["embedding"] 51 | else: 52 | query_vector = None 53 | 54 | # Only keep the text query if the retrieval mode uses text, otherwise drop it 55 | if not has_text: 56 | query_text = "" 57 | 58 | # Use semantic ranker if requested and if retrieval mode is text or hybrid (vectors + text) 59 | if overrides.get("semantic_ranker") and has_text: 60 | r = await self.search_client.search(query_text, 61 | filter=filter, 62 | query_type=QueryType.SEMANTIC, 63 | query_language="ja-jp", 64 | query_speller="none", 65 | semantic_configuration_name="default", 66 | top = top, 67 | query_caption="extractive|highlight-false" if use_semantic_captions else None, 68 | vector=query_vector, 69 | top_k=50 if query_vector else None, 70 | vector_fields="embedding" if query_vector else None) 71 | else: 72 | r = await self.search_client.search(query_text, 73 | filter=filter, 74 | top=top, 75 | vector=query_vector, 76 | top_k=50 if query_vector else None, 77 | vector_fields="embedding" if query_vector else None) 78 | if use_semantic_captions: 79 | results = [doc[self.sourcepage_field] + ":" + nonewlines(" -.- ".join([c.text for c in doc['@search.captions']])) for doc in r] 80 | else: 81 | results = [doc[self.sourcepage_field] + ":" + nonewlines(doc[self.content_field]) async for doc in r] 82 | content = "\n".join(results) 83 | return results, content 84 | 85 | async def run(self, q: str, overrides: dict[str, Any]) -> dict[str, Any]: 86 | 87 | retrieve_results = None 88 | async def retrieve_and_store(q: str) -> Any: 89 | nonlocal retrieve_results 90 | retrieve_results, content = await self.retrieve(q, overrides) 91 | return content 92 | 93 | # Use to capture thought process during iterations 94 | cb_handler = HtmlCallbackHandler() 95 | cb_manager = CallbackManager(handlers=[cb_handler]) 96 | 97 | # Tool dataclass 法と Subclassing the BaseTool class 法の異なる記法を示しています 98 | tools = [ 99 | Tool(name="PeopleSearchTool", 100 | func=retrieve_and_store, 101 | coroutine=retrieve_and_store, 102 | description="日本の歴史の人物情報の検索に便利です。ユーザーの質問から検索クエリーを生成して検索します。クエリーは文字列のみを受け付けます" 103 | ), 104 | CafeSearchTool() 105 | ] 106 | 107 | #llm = ChatOpenAI(model_name="gpt-4-0613", temperature=0) 108 | llm = AzureChatOpenAI(deployment_name=self.openai_deployment, 109 | temperature=overrides.get("temperature") or 0.3, 110 | openai_api_base=openai.api_base, 111 | openai_api_version=openai.api_version, 112 | openai_api_type=openai.api_type, 113 | openai_api_key=openai.api_key) 114 | SUFFIX = """ 115 | Answer should be in Japanese. 116 | """ 117 | agent_chain = initialize_agent(tools, 118 | llm, 119 | agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, 120 | #agent=AgentType.OPENAI_FUNCTIONS, 121 | verbose=True, 122 | agent_kwargs=dict(suffix=SUFFIX + prompt.SUFFIX), 123 | callback_manager = cb_manager, 124 | handle_parsing_errors=True, 125 | max_iterations=5, 126 | early_stopping_method="generate") 127 | #最大反復回数を制限する max_iterations, early_stopping_method 128 | #https://python.langchain.com/docs/modules/agents/how_to/max_iterations 129 | #解析エラーを処理する handle_parsing_errors 130 | #https://python.langchain.com/docs/modules/agents/how_to/handle_parsing_errors 131 | 132 | result = await agent_chain.arun(q) 133 | # Remove references to tool names that might be confused with a citation 134 | #result = result.replace("[CognitiveSearch]", "").replace("[Employee]", "") 135 | return {"data_points": retrieve_results or [], "answer": result, "thoughts": cb_handler.get_and_reset_log()} 136 | 137 | # 検索を行うカスタムツールを定義。CSV からルックアップを行う例です。 138 | # Subclassing the BaseTool class 139 | # https://python.langchain.com/docs/modules/agents/tools/custom_tools 140 | class CafeSearchTool(BaseTool): 141 | data: dict[str, str] = {} 142 | name = "CafeSearchTool" 143 | description = "武将のゆかりのカフェを検索するのに便利です。カフェの検索クエリには、武将の**名前のみ**を入力してください。" 144 | 145 | # Use the tool synchronously. 146 | def _run(self, query: str) -> str: 147 | """Use the tool.""" 148 | return query 149 | 150 | # Use the tool asynchronously. 151 | async def _arun(self, query: str) -> str: 152 | filename = "data/restaurantinfo.csv" 153 | key_field = "name" 154 | try: 155 | with open(filename, newline='', encoding='utf-8') as csvfile: 156 | reader = csv.DictReader(csvfile) 157 | for row in reader: 158 | self.data[row[key_field]] = "\n".join([f"{i}:{row[i]}" for i in row]) 159 | 160 | except Exception as e: 161 | logging.exception("File read error:", e) 162 | 163 | return self.data.get(query, "") -------------------------------------------------------------------------------- /app/backend/approaches/retrievethenread.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import openai 4 | from azure.search.documents.aio import SearchClient 5 | from azure.search.documents.models import QueryType 6 | 7 | from approaches.approach import AskApproach 8 | from core.messagebuilder import MessageBuilder 9 | from text import nonewlines 10 | 11 | 12 | class RetrieveThenReadApproach(AskApproach): 13 | """ 14 | Simple retrieve-then-read implementation, using the Cognitive Search and OpenAI APIs directly. It first retrieves 15 | top documents from search, then constructs a prompt with them, and then uses OpenAI to generate an completion 16 | (answer) with that prompt. 17 | Cognitive Search と Azure OpenAI の API を直接使用した、シンプルな retrieve-then-read の実装です。 18 | まず検索から上位のドキュメントを取得し、それを使ってプロンプトを作成し、OpenAI を使ってそのプロンプトを使った補完(回答)を生成します。 19 | """ 20 | 21 | system_chat_template = \ 22 | "あなたは日本の歴史に関する質問をサポートする教師アシスタントです。" + \ 23 | "質問者が「私」で質問しても、「あなた」を使って質問者を指すようにする。" + \ 24 | "次の質問に、以下の出典で提供されたデータのみを使用して答えてください。" + \ 25 | "表形式の情報については、htmlテーブルとして返してください。マークダウン形式で返さないでください。" + \ 26 | "各出典元には、名前の後にコロンと実際の情報があり、回答で使用する各事実には必ず出典名を記載します。" + \ 27 | "以下の出典の中から答えられない場合は、「わかりません」と答えてください。" 28 | #shots/sample conversation 29 | question = """ 30 | 'Question: '源頼朝の具体的な功績を教えてください' 31 | 32 | Sources: 33 | info1.txt: 「本領安堵」「新恩給付」という豪族たちの最大の願望を実現し、坂東豪族の支持を集めた。 34 | info2.pdf: 1185年に設置されたこの守護地頭は源頼朝の代表的な政治政策です。 35 | info3.pdf: 源頼朝は、御家人の所領の保証、敵方の没収所領の給付を行いました。 36 | info4.pdf: 平氏追討を名目にした軍事的支配権の行使を通じて、鎌倉政権を確立しました。 37 | """ 38 | answer = "源頼朝は、御家人の所領の保証、敵方の没収所領の給付を行い、「本領安堵」「新恩給付」という豪族たちの最大の願望を実現し、坂東豪族の支持を集めた。[info1.txt][info3.pdf] また、平氏追討を名目にした軍事的支配権の行使を通じて、鎌倉政権を確立し、[info4.txt] 守護地頭という重要な政策を確立しました。[info2.txt]" 39 | 40 | def __init__(self, search_client: SearchClient, openai_deployment: str, chatgpt_model: str, embedding_deployment: str, sourcepage_field: str, content_field: str): 41 | self.search_client = search_client 42 | self.openai_deployment = openai_deployment 43 | self.chatgpt_model = chatgpt_model 44 | self.embedding_deployment = embedding_deployment 45 | self.sourcepage_field = sourcepage_field 46 | self.content_field = content_field 47 | 48 | async def run(self, q: str, overrides: dict[str, Any]) -> dict[str, Any]: 49 | has_text = overrides.get("retrieval_mode") in ["text", "hybrid", None] 50 | has_vector = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] 51 | use_semantic_captions = True if overrides.get("semantic_captions") and has_text else False 52 | top = overrides.get("top") or 3 53 | exclude_category = overrides.get("exclude_category") or None 54 | filter = "category ne '{}'".format(exclude_category.replace("'", "''")) if exclude_category else None 55 | 56 | # If retrieval mode includes vectors, compute an embedding for the query 57 | if has_vector: 58 | query_vector = (await openai.Embedding.acreate(engine=self.embedding_deployment, input=q))["data"][0]["embedding"] 59 | else: 60 | query_vector = None 61 | 62 | # Only keep the text query if the retrieval mode uses text, otherwise drop it 63 | query_text = q if has_text else "" 64 | 65 | # Use semantic ranker if requested and if retrieval mode is text or hybrid (vectors + text) 66 | if overrides.get("semantic_ranker") and has_text: 67 | r = await self.search_client.search(query_text, 68 | filter=filter, 69 | query_type=QueryType.SEMANTIC, 70 | query_language="ja-jp", 71 | query_speller="none", 72 | semantic_configuration_name="default", 73 | top=top, 74 | query_caption="extractive|highlight-false" if use_semantic_captions else None, 75 | vector=query_vector, 76 | top_k=50 if query_vector else None, 77 | vector_fields="embedding" if query_vector else None) 78 | else: 79 | r = await self.search_client.search(query_text, 80 | filter=filter, 81 | top=top, 82 | vector=query_vector, 83 | top_k=50 if query_vector else None, 84 | vector_fields="embedding" if query_vector else None) 85 | if use_semantic_captions: 86 | results = [doc[self.sourcepage_field] + ": " + nonewlines(" . ".join([c.text for c in doc['@search.captions']])) async for doc in r] 87 | else: 88 | results = [doc[self.sourcepage_field] + ": " + nonewlines(doc[self.content_field]) async for doc in r] 89 | content = "\n".join(results) 90 | 91 | message_builder = MessageBuilder(overrides.get("prompt_template") or self.system_chat_template, self.chatgpt_model) 92 | 93 | # add user question 94 | user_content = q + "\n" + f"Sources:\n {content}" 95 | message_builder.append_message('user', user_content) 96 | 97 | # Add shots/samples. This helps model to mimic response and make sure they match rules laid out in system message. 98 | message_builder.append_message('assistant', self.answer) 99 | message_builder.append_message('user', self.question) 100 | 101 | messages = message_builder.messages 102 | chat_completion = await openai.ChatCompletion.acreate( 103 | deployment_id=self.openai_deployment, 104 | model=self.chatgpt_model, 105 | messages=messages, 106 | temperature=overrides.get("temperature") or 0.3, 107 | max_tokens=1024, 108 | n=1) 109 | 110 | return {"data_points": results, "answer": chat_completion.choices[0].message.content, "thoughts": f"Question:
{query_text}

Prompt:
" + '\n\n'.join([str(message) for message in messages])} 111 | -------------------------------------------------------------------------------- /app/backend/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nohanaga/azure-search-openai-demo/405dc611ad1e0e755a1e964121e6f6c5dcab2869/app/backend/core/__init__.py -------------------------------------------------------------------------------- /app/backend/core/messagebuilder.py: -------------------------------------------------------------------------------- 1 | from .modelhelper import num_tokens_from_messages 2 | 3 | 4 | class MessageBuilder: 5 | """ 6 | A class for building and managing messages in a chat conversation. 7 | Attributes: 8 | message (list): A list of dictionaries representing chat messages. 9 | model (str): The name of the ChatGPT model. 10 | token_count (int): The total number of tokens in the conversation. 11 | Methods: 12 | __init__(self, system_content: str, chatgpt_model: str): Initializes the MessageBuilder instance. 13 | append_message(self, role: str, content: str, index: int = 1): Appends a new message to the conversation. 14 | """ 15 | 16 | def __init__(self, system_content: str, chatgpt_model: str): 17 | self.messages = [{'role': 'system', 'content': system_content}] 18 | self.model = chatgpt_model 19 | self.token_length = num_tokens_from_messages( 20 | self.messages[-1], self.model) 21 | 22 | def append_message(self, role: str, content: str, index: int = 1): 23 | self.messages.insert(index, {'role': role, 'content': content}) 24 | self.token_length += num_tokens_from_messages( 25 | self.messages[index], self.model) 26 | -------------------------------------------------------------------------------- /app/backend/core/modelhelper.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import tiktoken 4 | 5 | MODELS_2_TOKEN_LIMITS = { 6 | "gpt-35-turbo": 4000, 7 | "gpt-3.5-turbo": 4000, 8 | "gpt-35-turbo-16k": 16000, 9 | "gpt-3.5-turbo-16k": 16000, 10 | "gpt-4": 8100, 11 | "gpt-4-32k": 32000 12 | } 13 | 14 | AOAI_2_OAI = { 15 | "gpt-35-turbo": "gpt-3.5-turbo", 16 | "gpt-35-turbo-16k": "gpt-3.5-turbo-16k" 17 | } 18 | 19 | 20 | def get_token_limit(model_id: str) -> int: 21 | if model_id not in MODELS_2_TOKEN_LIMITS: 22 | raise ValueError("Expected model gpt-35-turbo and above") 23 | return MODELS_2_TOKEN_LIMITS[model_id] 24 | 25 | 26 | def num_tokens_from_messages(message: dict[str, str], model: str) -> int: 27 | """ 28 | Calculate the number of tokens required to encode a message. 29 | Args: 30 | message (dict): The message to encode, represented as a dictionary. 31 | model (str): The name of the model to use for encoding. 32 | Returns: 33 | int: The total number of tokens required to encode the message. 34 | Example: 35 | message = {'role': 'user', 'content': 'Hello, how are you?'} 36 | model = 'gpt-3.5-turbo' 37 | num_tokens_from_messages(message, model) 38 | output: 11 39 | """ 40 | encoding = tiktoken.encoding_for_model(get_oai_chatmodel_tiktok(model)) 41 | num_tokens = 2 # For "role" and "content" keys 42 | for key, value in message.items(): 43 | num_tokens += len(encoding.encode(value)) 44 | return num_tokens 45 | 46 | 47 | def get_oai_chatmodel_tiktok(aoaimodel: str) -> str: 48 | message = "Expected Azure OpenAI ChatGPT model name" 49 | if aoaimodel == "" or aoaimodel is None: 50 | raise ValueError(message) 51 | if aoaimodel not in AOAI_2_OAI and aoaimodel not in MODELS_2_TOKEN_LIMITS: 52 | raise ValueError(message) 53 | return AOAI_2_OAI.get(aoaimodel) or aoaimodel 54 | -------------------------------------------------------------------------------- /app/backend/data/restaurantinfo.csv: -------------------------------------------------------------------------------- 1 | name,category,restaurant,ratings,location 2 | 源範頼,カフェ,"喫茶かば庵",3.5,修善寺 3 | 源頼家,カフェ,"Cafe Genji13",3.4,修善寺 4 | 源頼朝,カフェ,"鎌倉武衛ミュージアムカフェ",3.6,鎌倉 5 | 源義経,カフェ,"カフェ金色堂",3.2,奥州平泉 -------------------------------------------------------------------------------- /app/backend/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | max_requests = 1000 4 | max_requests_jitter = 50 5 | log_file = "-" 6 | bind = "0.0.0.0" 7 | 8 | timeout = 230 9 | # https://learn.microsoft.com/en-us/troubleshoot/azure/app-service/web-apps-performance-faqs#why-does-my-request-time-out-after-230-seconds 10 | 11 | num_cpus = multiprocessing.cpu_count() 12 | workers = (num_cpus * 2) + 1 13 | worker_class = "uvicorn.workers.UvicornWorker" 14 | -------------------------------------------------------------------------------- /app/backend/langchainadapters.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Optional, Union 2 | 3 | from langchain.callbacks.base import BaseCallbackHandler 4 | from langchain.schema import AgentAction, AgentFinish, LLMResult 5 | 6 | 7 | def ch(text: Union[str, object]) -> str: 8 | s = text if isinstance(text, str) else str(text) 9 | return s.replace("<", "<").replace(">", ">").replace("\r", "").replace("\n", "
") 10 | 11 | class HtmlCallbackHandler (BaseCallbackHandler): 12 | html: str = "" 13 | 14 | def get_and_reset_log(self) -> str: 15 | result = self.html 16 | self.html = "" 17 | return result 18 | 19 | def on_llm_start( 20 | self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any 21 | ) -> None: 22 | """Print out the prompts.""" 23 | self.html += "LLM prompts:
" + "
".join(ch(prompts)) + "
" 24 | 25 | def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None: 26 | """Do nothing.""" 27 | pass 28 | 29 | def on_llm_error(self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any) -> None: 30 | self.html += f"LLM error: {ch(error)}
" 31 | 32 | def on_chain_start( 33 | self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any 34 | ) -> None: 35 | """Print out that we are entering a chain.""" 36 | class_name = "unknown" 37 | if "name" in serialized: 38 | class_name = serialized["name"] 39 | self.html += f"Entering chain: {ch(class_name)}
" 40 | 41 | def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> None: 42 | """Print out that we finished a chain.""" 43 | self.html += "Finished chain
" 44 | 45 | def on_chain_error(self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any) -> None: 46 | self.html += f"Chain error: {ch(error)}
" 47 | 48 | def on_tool_start( 49 | self, 50 | serialized: Dict[str, Any], 51 | input_str: str, 52 | color: Optional[str] = None, 53 | **kwargs: Any, 54 | ) -> None: 55 | """Print out the log in specified color.""" 56 | pass 57 | 58 | def on_tool_end( 59 | self, 60 | output: str, 61 | color: Optional[str] = None, 62 | observation_prefix: Optional[str] = None, 63 | llm_prefix: Optional[str] = None, 64 | **kwargs: Any, 65 | ) -> None: 66 | """If not the final action, print out observation.""" 67 | self.html += f"{ch(observation_prefix)}
{ch(output)}
{ch(llm_prefix)}
" 68 | 69 | def on_tool_error(self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any) -> None: 70 | self.html += f"Tool error: {ch(error)}
" 71 | 72 | def on_text( 73 | self, 74 | text: str, 75 | color: Optional[str] = None, 76 | **kwargs: Optional[str], 77 | ) -> None: 78 | """Run when agent ends.""" 79 | self.html += f"{ch(text)}
" 80 | 81 | def on_agent_action( 82 | self, 83 | action: AgentAction, 84 | color: Optional[str] = None, 85 | **kwargs: Any) -> Any: 86 | self.html += f"{ch(action.log)}
" 87 | 88 | def on_agent_finish( 89 | self, finish: AgentFinish, color: Optional[str] = None, **kwargs: Any 90 | ) -> None: 91 | """Run on agent end.""" 92 | self.html += f"{ch(finish.log)}
" 93 | -------------------------------------------------------------------------------- /app/backend/lookuptool.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pathlib import Path 3 | from typing import Union 4 | 5 | from langchain.agents import Tool 6 | from langchain.callbacks.manager import Callbacks 7 | 8 | 9 | class CsvLookupTool(Tool): 10 | data: dict[str, str] = {} 11 | 12 | def __init__(self, filename: Union[str, Path], key_field: str, name: str = "lookup", 13 | description: str = "useful to look up details given an input key as opposite to searching data with an unstructured question", 14 | callbacks: Callbacks = None): 15 | super().__init__(name, self.lookup, description, callbacks=callbacks) 16 | with open(filename, newline='', encoding='utf-8') as csvfile: 17 | reader = csv.DictReader(csvfile) 18 | for row in reader: 19 | self.data[row[key_field]] = "\n".join([f"{i}:{row[i]}" for i in row]) 20 | 21 | def lookup(self, key: str) -> str: 22 | return self.data.get(key, "") 23 | -------------------------------------------------------------------------------- /app/backend/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from app import create_app 4 | 5 | format = '%(asctime)s [%(levelname)s]:%(message)s' 6 | logging.basicConfig(format=format, encoding='utf-8', level=logging.ERROR) 7 | 8 | logger = logging.getLogger('azure') 9 | logger.setLevel(logging.ERROR) 10 | 11 | app = create_app() 12 | -------------------------------------------------------------------------------- /app/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | aiofiles==23.2.1 8 | # via quart 9 | aiohttp==3.8.6 10 | # via 11 | # -r requirements.in 12 | # langchain 13 | # openai 14 | aiosignal==1.3.1 15 | # via aiohttp 16 | annotated-types==0.6.0 17 | # via pydantic 18 | anyio==4.0.0 19 | # via watchfiles 20 | asgiref==3.7.2 21 | # via opentelemetry-instrumentation-asgi 22 | async-timeout==4.0.3 23 | # via 24 | # aiohttp 25 | # langchain 26 | attrs==23.1.0 27 | # via aiohttp 28 | azure-common==1.1.28 29 | # via azure-search-documents 30 | azure-core==1.29.4 31 | # via 32 | # azure-core-tracing-opentelemetry 33 | # azure-cosmos 34 | # azure-identity 35 | # azure-monitor-opentelemetry 36 | # azure-monitor-opentelemetry-exporter 37 | # azure-search-documents 38 | # azure-storage-blob 39 | # msrest 40 | azure-core-tracing-opentelemetry==1.0.0b11 41 | # via azure-monitor-opentelemetry 42 | azure-cosmos==4.5.1 43 | # via -r requirements.in 44 | azure-identity==1.14.1 45 | # via -r requirements.in 46 | azure-monitor-opentelemetry==1.0.0 47 | # via -r requirements.in 48 | azure-monitor-opentelemetry-exporter==1.0.0b17 49 | # via azure-monitor-opentelemetry 50 | azure-search-documents==11.4.0b6 51 | # via -r requirements.in 52 | azure-storage-blob==12.18.3 53 | # via -r requirements.in 54 | blinker==1.6.3 55 | # via 56 | # flask 57 | # quart 58 | certifi==2023.7.22 59 | # via 60 | # msrest 61 | # requests 62 | cffi 63 | # via 64 | # -r requirements.in 65 | # cryptography 66 | charset-normalizer==3.3.0 67 | # via 68 | # aiohttp 69 | # requests 70 | click==8.1.7 71 | # via 72 | # flask 73 | # quart 74 | # uvicorn 75 | colorama==0.4.6 76 | # via 77 | # click 78 | # tqdm 79 | # uvicorn 80 | cryptography==41.0.4 81 | # via 82 | # azure-identity 83 | # azure-storage-blob 84 | # msal 85 | # pyjwt 86 | dataclasses-json==0.5.14 87 | # via langchain 88 | deprecated==1.2.14 89 | # via opentelemetry-api 90 | et-xmlfile==1.1.0 91 | # via openpyxl 92 | exceptiongroup==1.1.3 93 | # via anyio 94 | fixedint==0.1.6 95 | # via azure-monitor-opentelemetry-exporter 96 | flask==3.0.0 97 | # via quart 98 | frozenlist==1.4.0 99 | # via 100 | # aiohttp 101 | # aiosignal 102 | greenlet==3.0.0 103 | # via sqlalchemy 104 | h11==0.14.0 105 | # via 106 | # hypercorn 107 | # uvicorn 108 | # wsproto 109 | h2==4.1.0 110 | # via hypercorn 111 | hpack==4.0.0 112 | # via h2 113 | httptools==0.6.0 114 | # via uvicorn 115 | hypercorn==0.14.4 116 | # via quart 117 | hyperframe==6.0.1 118 | # via h2 119 | idna==3.4 120 | # via 121 | # anyio 122 | # requests 123 | # yarl 124 | importlib-metadata==6.8.0 125 | # via opentelemetry-api 126 | isodate==0.6.1 127 | # via 128 | # azure-search-documents 129 | # azure-storage-blob 130 | # msrest 131 | itsdangerous==2.1.2 132 | # via 133 | # flask 134 | # quart 135 | jinja2==3.1.2 136 | # via 137 | # flask 138 | # quart 139 | langchain==0.0.268 140 | # via -r requirements.in 141 | langsmith==0.0.43 142 | # via langchain 143 | markupsafe==2.1.3 144 | # via 145 | # jinja2 146 | # quart 147 | # werkzeug 148 | marshmallow==3.20.1 149 | # via dataclasses-json 150 | msal==1.24.1 151 | # via 152 | # -r requirements.in 153 | # azure-identity 154 | # msal-extensions 155 | msal-extensions==1.0.0 156 | # via 157 | # -r requirements.in 158 | # azure-identity 159 | msrest==0.7.1 160 | # via azure-monitor-opentelemetry-exporter 161 | multidict==6.0.4 162 | # via 163 | # aiohttp 164 | # yarl 165 | mypy-extensions==1.0.0 166 | # via typing-inspect 167 | numexpr==2.8.7 168 | # via langchain 169 | numpy==1.26.0 170 | # via 171 | # langchain 172 | # numexpr 173 | # openai 174 | # pandas 175 | # pandas-stubs 176 | oauthlib==3.2.2 177 | # via requests-oauthlib 178 | openai[datalib]==0.28.1 179 | # via -r requirements.in 180 | openpyxl==3.1.2 181 | # via openai 182 | opentelemetry-api==1.20.0 183 | # via 184 | # azure-core-tracing-opentelemetry 185 | # azure-monitor-opentelemetry-exporter 186 | # opentelemetry-instrumentation 187 | # opentelemetry-instrumentation-aiohttp-client 188 | # opentelemetry-instrumentation-asgi 189 | # opentelemetry-instrumentation-dbapi 190 | # opentelemetry-instrumentation-django 191 | # opentelemetry-instrumentation-fastapi 192 | # opentelemetry-instrumentation-flask 193 | # opentelemetry-instrumentation-psycopg2 194 | # opentelemetry-instrumentation-requests 195 | # opentelemetry-instrumentation-urllib 196 | # opentelemetry-instrumentation-urllib3 197 | # opentelemetry-instrumentation-wsgi 198 | # opentelemetry-sdk 199 | opentelemetry-instrumentation==0.41b0 200 | # via 201 | # opentelemetry-instrumentation-aiohttp-client 202 | # opentelemetry-instrumentation-asgi 203 | # opentelemetry-instrumentation-dbapi 204 | # opentelemetry-instrumentation-django 205 | # opentelemetry-instrumentation-fastapi 206 | # opentelemetry-instrumentation-flask 207 | # opentelemetry-instrumentation-psycopg2 208 | # opentelemetry-instrumentation-requests 209 | # opentelemetry-instrumentation-urllib 210 | # opentelemetry-instrumentation-urllib3 211 | # opentelemetry-instrumentation-wsgi 212 | opentelemetry-instrumentation-aiohttp-client==0.41b0 213 | # via -r requirements.in 214 | opentelemetry-instrumentation-asgi==0.41b0 215 | # via 216 | # -r requirements.in 217 | # opentelemetry-instrumentation-fastapi 218 | opentelemetry-instrumentation-dbapi==0.41b0 219 | # via opentelemetry-instrumentation-psycopg2 220 | opentelemetry-instrumentation-django==0.41b0 221 | # via azure-monitor-opentelemetry 222 | opentelemetry-instrumentation-fastapi==0.41b0 223 | # via azure-monitor-opentelemetry 224 | opentelemetry-instrumentation-flask==0.41b0 225 | # via azure-monitor-opentelemetry 226 | opentelemetry-instrumentation-psycopg2==0.41b0 227 | # via azure-monitor-opentelemetry 228 | opentelemetry-instrumentation-requests==0.41b0 229 | # via 230 | # -r requirements.in 231 | # azure-monitor-opentelemetry 232 | opentelemetry-instrumentation-urllib==0.41b0 233 | # via azure-monitor-opentelemetry 234 | opentelemetry-instrumentation-urllib3==0.41b0 235 | # via azure-monitor-opentelemetry 236 | opentelemetry-instrumentation-wsgi==0.41b0 237 | # via 238 | # opentelemetry-instrumentation-django 239 | # opentelemetry-instrumentation-flask 240 | opentelemetry-resource-detector-azure==0.1.0 241 | # via azure-monitor-opentelemetry 242 | opentelemetry-sdk==1.20.0 243 | # via 244 | # azure-monitor-opentelemetry-exporter 245 | # opentelemetry-resource-detector-azure 246 | opentelemetry-semantic-conventions==0.41b0 247 | # via 248 | # opentelemetry-instrumentation-aiohttp-client 249 | # opentelemetry-instrumentation-asgi 250 | # opentelemetry-instrumentation-dbapi 251 | # opentelemetry-instrumentation-django 252 | # opentelemetry-instrumentation-fastapi 253 | # opentelemetry-instrumentation-flask 254 | # opentelemetry-instrumentation-requests 255 | # opentelemetry-instrumentation-urllib 256 | # opentelemetry-instrumentation-urllib3 257 | # opentelemetry-instrumentation-wsgi 258 | # opentelemetry-sdk 259 | opentelemetry-util-http==0.41b0 260 | # via 261 | # opentelemetry-instrumentation-aiohttp-client 262 | # opentelemetry-instrumentation-asgi 263 | # opentelemetry-instrumentation-django 264 | # opentelemetry-instrumentation-fastapi 265 | # opentelemetry-instrumentation-flask 266 | # opentelemetry-instrumentation-requests 267 | # opentelemetry-instrumentation-urllib 268 | # opentelemetry-instrumentation-urllib3 269 | # opentelemetry-instrumentation-wsgi 270 | packaging==23.2 271 | # via 272 | # marshmallow 273 | # opentelemetry-instrumentation-flask 274 | pandas==2.1.1 275 | # via openai 276 | pandas-stubs==2.1.1.230928 277 | # via openai 278 | portalocker==2.8.2 279 | # via msal-extensions 280 | priority==2.0.0 281 | # via hypercorn 282 | pycparser==2.21 283 | # via cffi 284 | pydantic==2.4.2 285 | # via 286 | # langchain 287 | # langsmith 288 | pydantic-core==2.10.1 289 | # via pydantic 290 | pyjwt[crypto]==2.8.0 291 | # via msal 292 | python-dateutil==2.8.2 293 | # via pandas 294 | python-dotenv==1.0.0 295 | # via uvicorn 296 | pytz==2023.3.post1 297 | # via pandas 298 | #pywin32==306 299 | # via portalocker 300 | pyyaml==6.0.1 301 | # via 302 | # langchain 303 | # uvicorn 304 | quart==0.19.3 305 | # via 306 | # -r requirements.in 307 | # quart-cors 308 | quart-cors==0.7.0 309 | # via -r requirements.in 310 | regex==2023.10.3 311 | # via tiktoken 312 | requests==2.31.0 313 | # via 314 | # azure-core 315 | # langchain 316 | # langsmith 317 | # msal 318 | # msrest 319 | # openai 320 | # requests-oauthlib 321 | # tiktoken 322 | requests-oauthlib==1.3.1 323 | # via msrest 324 | six==1.16.0 325 | # via 326 | # azure-core 327 | # isodate 328 | # python-dateutil 329 | sniffio==1.3.0 330 | # via anyio 331 | sqlalchemy==2.0.21 332 | # via langchain 333 | tenacity==8.2.3 334 | # via langchain 335 | tiktoken==0.5.1 336 | # via -r requirements.in 337 | tomli==2.0.1 338 | # via hypercorn 339 | tqdm==4.66.1 340 | # via openai 341 | types-pytz==2023.3.1.1 342 | # via pandas-stubs 343 | typing-extensions==4.8.0 344 | # via 345 | # asgiref 346 | # azure-core 347 | # azure-storage-blob 348 | # opentelemetry-sdk 349 | # pydantic 350 | # pydantic-core 351 | # sqlalchemy 352 | # typing-inspect 353 | # uvicorn 354 | typing-inspect==0.9.0 355 | # via dataclasses-json 356 | tzdata==2023.3 357 | # via pandas 358 | urllib3==2.0.6 359 | # via requests 360 | uvicorn[standard]==0.23.2 361 | # via -r requirements.in 362 | watchfiles==0.20.0 363 | # via uvicorn 364 | websockets==11.0.3 365 | # via uvicorn 366 | werkzeug==3.0.0 367 | # via 368 | # flask 369 | # quart 370 | wrapt==1.15.0 371 | # via 372 | # deprecated 373 | # opentelemetry-instrumentation 374 | # opentelemetry-instrumentation-aiohttp-client 375 | # opentelemetry-instrumentation-dbapi 376 | # opentelemetry-instrumentation-urllib3 377 | wsproto==1.2.0 378 | # via hypercorn 379 | yarl==1.9.2 380 | # via aiohttp 381 | zipp==3.17.0 382 | # via importlib-metadata 383 | 384 | # The following packages are considered to be unsafe in a requirements file: 385 | # setuptools 386 | -------------------------------------------------------------------------------- /app/backend/text.py: -------------------------------------------------------------------------------- 1 | def nonewlines(s: str) -> str: 2 | return s.replace('\n', ' ').replace('\r', ' ').replace('[', '【').replace(']', '】') 3 | -------------------------------------------------------------------------------- /app/frontend/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /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 | GPT + Enterprise data | Sample 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 | "engines": { 7 | "node": ">=14.0.0" 8 | }, 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "tsc && vite build", 12 | "watch": "tsc && vite build --watch" 13 | }, 14 | "dependencies": { 15 | "@fluentui/react": "^8.110.7", 16 | "@fluentui/react-icons": "^2.0.206", 17 | "@react-spring/web": "^9.7.3", 18 | "dompurify": "^3.0.4", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-router-dom": "^6.14.1", 22 | "ndjson-readablestream": "^1.0.6" 23 | }, 24 | "devDependencies": { 25 | "@types/dompurify": "^3.0.2", 26 | "@types/react": "^18.2.14", 27 | "@types/react-dom": "^18.2.6", 28 | "@vitejs/plugin-react": "^4.0.2", 29 | "prettier": "^3.0.0", 30 | "typescript": "^5.1.6", 31 | "vite": "^4.4.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nohanaga/azure-search-openai-demo/405dc611ad1e0e755a1e964121e6f6c5dcab2869/app/frontend/public/favicon.ico -------------------------------------------------------------------------------- /app/frontend/src/api/api.ts: -------------------------------------------------------------------------------- 1 | import { AskRequest, AskResponse, ChatRequest } from "./models"; 2 | 3 | export async function askApi(options: AskRequest): Promise { 4 | const response = await fetch("/ask", { 5 | method: "POST", 6 | headers: { 7 | "Content-Type": "application/json" 8 | }, 9 | body: JSON.stringify({ 10 | question: options.question, 11 | approach: options.approach, 12 | overrides: { 13 | retrieval_mode: options.overrides?.retrievalMode, 14 | semantic_ranker: options.overrides?.semanticRanker, 15 | semantic_captions: options.overrides?.semanticCaptions, 16 | top: options.overrides?.top, 17 | temperature: options.overrides?.temperature, 18 | prompt_template: options.overrides?.promptTemplate, 19 | prompt_template_prefix: options.overrides?.promptTemplatePrefix, 20 | prompt_template_suffix: options.overrides?.promptTemplateSuffix, 21 | exclude_category: options.overrides?.excludeCategory 22 | } 23 | }) 24 | }); 25 | 26 | const parsedResponse: AskResponse = await response.json(); 27 | if (response.status > 299 || !response.ok) { 28 | throw Error(parsedResponse.error || "Unknown error"); 29 | } 30 | 31 | return parsedResponse; 32 | } 33 | 34 | export async function chatApi(options: ChatRequest): Promise { 35 | const url = options.shouldStream ? "/chat_stream" : "/chat"; 36 | return await fetch(url, { 37 | method: "POST", 38 | headers: { 39 | "Content-Type": "application/json" 40 | }, 41 | body: JSON.stringify({ 42 | history: options.history, 43 | approach: options.approach, 44 | overrides: { 45 | retrieval_mode: options.overrides?.retrievalMode, 46 | semantic_ranker: options.overrides?.semanticRanker, 47 | semantic_captions: options.overrides?.semanticCaptions, 48 | top: options.overrides?.top, 49 | temperature: options.overrides?.temperature, 50 | prompt_template: options.overrides?.promptTemplate, 51 | prompt_template_prefix: options.overrides?.promptTemplatePrefix, 52 | prompt_template_suffix: options.overrides?.promptTemplateSuffix, 53 | exclude_category: options.overrides?.excludeCategory, 54 | suggest_followup_questions: options.overrides?.suggestFollowupQuestions 55 | } 56 | }) 57 | }); 58 | } 59 | 60 | export function getCitationFilePath(citation: string): string { 61 | return `/content/${citation}`; 62 | } 63 | -------------------------------------------------------------------------------- /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 | ReadPluginsRetrieve = "rpr" 6 | } 7 | 8 | export const enum RetrievalMode { 9 | Hybrid = "hybrid", 10 | Vectors = "vectors", 11 | Text = "text" 12 | } 13 | 14 | export type AskRequestOverrides = { 15 | retrievalMode?: RetrievalMode; 16 | semanticRanker?: boolean; 17 | semanticCaptions?: boolean; 18 | excludeCategory?: string; 19 | top?: number; 20 | temperature?: number; 21 | promptTemplate?: string; 22 | promptTemplatePrefix?: string; 23 | promptTemplateSuffix?: string; 24 | suggestFollowupQuestions?: boolean; 25 | }; 26 | 27 | export type AskRequest = { 28 | question: string; 29 | approach: Approaches; 30 | overrides?: AskRequestOverrides; 31 | }; 32 | 33 | export type AskResponse = { 34 | answer: string; 35 | thoughts: string | null; 36 | data_points: string[]; 37 | error?: string; 38 | }; 39 | 40 | export type ChatTurn = { 41 | user: string; 42 | bot?: string; 43 | }; 44 | 45 | export type ChatRequest = { 46 | history: ChatTurn[]; 47 | approach: Approaches; 48 | overrides?: AskRequestOverrides; 49 | shouldStream?: boolean; 50 | }; 51 | -------------------------------------------------------------------------------- /app/frontend/src/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/frontend/src/assets/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/frontend/src/components/AnalysisPanel/AnalysisPanel.module.css: -------------------------------------------------------------------------------- 1 | .thoughtProcess { 2 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 3 | word-wrap: break-word; 4 | padding-top: 12px; 5 | padding-bottom: 12px; 6 | } 7 | -------------------------------------------------------------------------------- /app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx: -------------------------------------------------------------------------------- 1 | import { Pivot, PivotItem } from "@fluentui/react"; 2 | import DOMPurify from "dompurify"; 3 | 4 | import styles from "./AnalysisPanel.module.css"; 5 | 6 | import { SupportingContent } from "../SupportingContent"; 7 | import { AskResponse } from "../../api"; 8 | import { AnalysisPanelTabs } from "./AnalysisPanelTabs"; 9 | 10 | interface Props { 11 | className: string; 12 | activeTab: AnalysisPanelTabs; 13 | onActiveTabChanged: (tab: AnalysisPanelTabs) => void; 14 | activeCitation: string | undefined; 15 | citationHeight: string; 16 | answer: AskResponse; 17 | } 18 | 19 | const pivotItemDisabledStyle = { disabled: true, style: { color: "grey" } }; 20 | 21 | export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeight, className, onActiveTabChanged }: Props) => { 22 | const isDisabledThoughtProcessTab: boolean = !answer.thoughts; 23 | const isDisabledSupportingContentTab: boolean = !answer.data_points.length; 24 | const isDisabledCitationTab: boolean = !activeCitation; 25 | 26 | const sanitizedThoughts = DOMPurify.sanitize(answer.thoughts!); 27 | 28 | return ( 29 | pivotItem && onActiveTabChanged(pivotItem.props.itemKey! as AnalysisPanelTabs)} 33 | > 34 | 39 |
40 |
41 | 46 | 47 | 48 | 53 |