diff --git a/.azdo/pipelines/azure-dev.yml b/.azdo/pipelines/azure-dev.yml index 3016dd301c..ee7892a48d 100644 --- a/.azdo/pipelines/azure-dev.yml +++ b/.azdo/pipelines/azure-dev.yml @@ -1,119 +1,119 @@ -# Run when commits are pushed to mainline branch (main or master) -# Set this to the mainline branch you are using -trigger: - - main - - master - -# Azure Pipelines workflow to deploy to Azure using azd -# To configure required secrets and service connection for connecting to Azure, simply run `azd pipeline config --provider azdo` -# Task "Install azd" needs to install setup-azd extension for azdo - https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azd -# See below for alternative task to install azd if you can't install above task in your organization - -pool: - vmImage: ubuntu-latest - -steps: - - task: setup-azd@0 - displayName: Install azd - - # If you can't install above task in your organization, you can comment it and uncomment below task to install azd - # - task: Bash@3 - # displayName: Install azd - # inputs: - # targetType: 'inline' - # script: | - # curl -fsSL https://aka.ms/install-azd.sh | bash - - # azd delegate auth to az to use service connection with AzureCLI@2 - - pwsh: | - azd config set auth.useAzCliAuth "true" - displayName: Configure AZD to Use AZ CLI Authentication. - - - task: AzureCLI@2 - displayName: Provision Infrastructure - inputs: - # azconnection is the service connection created by azd. You can change it to any service connection you have in your organization. - azureSubscription: azconnection - scriptType: bash - scriptLocation: inlineScript - inlineScript: | - azd provision --no-prompt - env: - AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) - AZURE_ENV_NAME: $(AZURE_ENV_NAME) - AZURE_LOCATION: $(AZURE_LOCATION) - AZD_INITIAL_ENVIRONMENT_CONFIG: $(AZD_INITIAL_ENVIRONMENT_CONFIG) - AZURE_OPENAI_SERVICE: $(AZURE_OPENAI_SERVICE) - AZURE_OPENAI_API_VERSION: $(AZURE_OPENAI_API_VERSION) - AZURE_OPENAI_RESOURCE_GROUP: $(AZURE_OPENAI_RESOURCE_GROUP) - AZURE_DOCUMENTINTELLIGENCE_SERVICE: $(AZURE_DOCUMENTINTELLIGENCE_SERVICE) - AZURE_DOCUMENTINTELLIGENCE_RESOURCE_GROUP: $(AZURE_DOCUMENTINTELLIGENCE_RESOURCE_GROUP) - AZURE_DOCUMENTINTELLIGENCE_SKU: $(AZURE_DOCUMENTINTELLIGENCE_SKU) - AZURE_DOCUMENTINTELLIGENCE_LOCATION: $(AZURE_DOCUMENTINTELLIGENCE_LOCATION) - AZURE_SEARCH_INDEX: $(AZURE_SEARCH_INDEX) - AZURE_SEARCH_SERVICE: $(AZURE_SEARCH_SERVICE) - AZURE_SEARCH_SERVICE_RESOURCE_GROUP: $(AZURE_SEARCH_SERVICE_RESOURCE_GROUP) - AZURE_SEARCH_SERVICE_LOCATION: $(AZURE_SEARCH_SERVICE_LOCATION) - AZURE_SEARCH_SERVICE_SKU: $(AZURE_SEARCH_SERVICE_SKU) - AZURE_SEARCH_QUERY_LANGUAGE: $(AZURE_SEARCH_QUERY_LANGUAGE) - AZURE_SEARCH_QUERY_SPELLER: $(AZURE_SEARCH_QUERY_SPELLER) - AZURE_SEARCH_SEMANTIC_RANKER: $(AZURE_SEARCH_SEMANTIC_RANKER) - AZURE_STORAGE_ACCOUNT: $(AZURE_STORAGE_ACCOUNT) - AZURE_STORAGE_RESOURCE_GROUP: $(AZURE_STORAGE_RESOURCE_GROUP) - AZURE_STORAGE_SKU: $(AZURE_STORAGE_SKU) - AZURE_APP_SERVICE_SKU: $(AZURE_APP_SERVICE_SKU) - AZURE_OPENAI_CHATGPT_MODEL: $(AZURE_OPENAI_CHATGPT_MODEL) - AZURE_OPENAI_CHATGPT_DEPLOYMENT: $(AZURE_OPENAI_CHATGPT_DEPLOYMENT) - AZURE_OPENAI_CHATGPT_DEPLOYMENT_CAPACITY: $(AZURE_OPENAI_CHATGPT_DEPLOYMENT_CAPACITY) - AZURE_OPENAI_CHATGPT_DEPLOYMENT_VERSION: $(AZURE_OPENAI_CHATGPT_DEPLOYMENT_VERSION) - AZURE_OPENAI_EMB_MODEL_NAME: $(AZURE_OPENAI_EMB_MODEL_NAME) - AZURE_OPENAI_EMB_DEPLOYMENT: $(AZURE_OPENAI_EMB_DEPLOYMENT) - AZURE_OPENAI_EMB_DEPLOYMENT_CAPACITY: $(AZURE_OPENAI_EMB_DEPLOYMENT_CAPACITY) - AZURE_OPENAI_EMB_DEPLOYMENT_VERSION: $(AZURE_OPENAI_EMB_DEPLOYMENT_VERSION) - AZURE_OPENAI_EMB_DIMENSIONS: $(AZURE_OPENAI_EMB_DIMENSIONS) - OPENAI_HOST: $(OPENAI_HOST) - OPENAI_API_KEY: $(OPENAI_API_KEY) - OPENAI_ORGANIZATION: $(OPENAI_ORGANIZATION) - AZURE_USE_APPLICATION_INSIGHTS: $(AZURE_USE_APPLICATION_INSIGHTS) - AZURE_APPLICATION_INSIGHTS: $(AZURE_APPLICATION_INSIGHTS) - AZURE_APPLICATION_INSIGHTS_DASHBOARD: $(AZURE_APPLICATION_INSIGHTS_DASHBOARD) - AZURE_LOG_ANALYTICS: $(AZURE_LOG_ANALYTICS) - USE_VECTORS: $(USE_VECTORS) - USE_GPT4V: $(USE_GPT4V) - AZURE_VISION_ENDPOINT: $(AZURE_VISION_ENDPOINT) - VISION_SECRET_NAME: $(VISION_SECRET_NAME) - AZURE_COMPUTER_VISION_SERVICE: $(AZURE_COMPUTER_VISION_SERVICE) - AZURE_COMPUTER_VISION_RESOURCE_GROUP: $(AZURE_COMPUTER_VISION_RESOURCE_GROUP) - AZURE_COMPUTER_VISION_LOCATION: $(AZURE_COMPUTER_VISION_LOCATION) - AZURE_COMPUTER_VISION_SKU: $(AZURE_COMPUTER_VISION_SKU) - USE_SPEECH_INPUT_BROWSER: $(USE_SPEECH_INPUT_BROWSER) - USE_SPEECH_OUTPUT_BROWSER: $(USE_SPEECH_OUTPUT_BROWSER) - USE_SPEECH_OUTPUT_AZURE: $(USE_SPEECH_OUTPUT_AZURE) - AZURE_SPEECH_SERVICE: $(AZURE_SPEECH_SERVICE) - AZURE_SPEECH_SERVICE_RESOURCE_GROUP: $(AZURE_SPEECH_SERVICE_RESOURCE_GROUP) - AZURE_SPEECH_SERVICE_LOCATION: $(AZURE_SPEECH_SERVICE_LOCATION) - AZURE_SPEECH_SERVICE_SKU: $(AZURE_SPEECH_SERVICE_SKU) - AZURE_KEY_VAULT_NAME: $(AZURE_KEY_VAULT_NAME) - AZURE_USE_AUTHENTICATION: $(AZURE_USE_AUTHENTICATION) - AZURE_ENFORCE_ACCESS_CONTROL: $(AZURE_ENFORCE_ACCESS_CONTROL) - AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS: $(AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS) - AZURE_ENABLE_UNAUTHENTICATED_ACCESS: $(AZURE_ENABLE_UNAUTHENTICATED_ACCESS) - AZURE_TENANT_ID: $(AZURE_TENANT_ID) - AZURE_AUTH_TENANT_ID: $(AZURE_AUTH_TENANT_ID) - AZURE_SERVER_APP_ID: $(AZURE_SERVER_APP_ID) - AZURE_CLIENT_APP_ID: $(AZURE_CLIENT_APP_ID) - ALLOWED_ORIGIN: $(ALLOWED_ORIGIN) - AZURE_SERVER_APP_SECRET: $(AZURE_SERVER_APP_SECRET) - AZURE_CLIENT_APP_SECRET: $(AZURE_CLIENT_APP_SECRET) - AZURE_ADLS_GEN2_STORAGE_ACCOUNT: $(AZURE_ADLS_GEN2_STORAGE_ACCOUNT) - AZURE_ADLS_GEN2_FILESYSTEM_PATH: $(AZURE_ADLS_GEN2_FILESYSTEM_PATH) - AZURE_ADLS_GEN2_FILESYSTEM: $(AZURE_ADLS_GEN2_FILESYSTEM) - - - task: AzureCLI@2 - displayName: Deploy Application - inputs: - azureSubscription: azconnection - scriptType: bash - scriptLocation: inlineScript - inlineScript: | - azd deploy --no-prompt +# Run when commits are pushed to mainline branch (main or master) +# Set this to the mainline branch you are using +trigger: + - main + - master + +# Azure Pipelines workflow to deploy to Azure using azd +# To configure required secrets and service connection for connecting to Azure, simply run `azd pipeline config --provider azdo` +# Task "Install azd" needs to install setup-azd extension for azdo - https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azd +# See below for alternative task to install azd if you can't install above task in your organization + +pool: + vmImage: ubuntu-latest + +steps: + - task: setup-azd@0 + displayName: Install azd + + # If you can't install above task in your organization, you can comment it and uncomment below task to install azd + # - task: Bash@3 + # displayName: Install azd + # inputs: + # targetType: 'inline' + # script: | + # curl -fsSL https://aka.ms/install-azd.sh | bash + + # azd delegate auth to az to use service connection with AzureCLI@2 + - pwsh: | + azd config set auth.useAzCliAuth "true" + displayName: Configure AZD to Use AZ CLI Authentication. + + - task: AzureCLI@2 + displayName: Provision Infrastructure + inputs: + # azconnection is the service connection created by azd. You can change it to any service connection you have in your organization. + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + azd provision --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + AZD_INITIAL_ENVIRONMENT_CONFIG: $(AZD_INITIAL_ENVIRONMENT_CONFIG) + AZURE_OPENAI_SERVICE: $(AZURE_OPENAI_SERVICE) + AZURE_OPENAI_API_VERSION: $(AZURE_OPENAI_API_VERSION) + AZURE_OPENAI_RESOURCE_GROUP: $(AZURE_OPENAI_RESOURCE_GROUP) + AZURE_DOCUMENTINTELLIGENCE_SERVICE: $(AZURE_DOCUMENTINTELLIGENCE_SERVICE) + AZURE_DOCUMENTINTELLIGENCE_RESOURCE_GROUP: $(AZURE_DOCUMENTINTELLIGENCE_RESOURCE_GROUP) + AZURE_DOCUMENTINTELLIGENCE_SKU: $(AZURE_DOCUMENTINTELLIGENCE_SKU) + AZURE_DOCUMENTINTELLIGENCE_LOCATION: $(AZURE_DOCUMENTINTELLIGENCE_LOCATION) + AZURE_SEARCH_INDEX: $(AZURE_SEARCH_INDEX) + AZURE_SEARCH_SERVICE: $(AZURE_SEARCH_SERVICE) + AZURE_SEARCH_SERVICE_RESOURCE_GROUP: $(AZURE_SEARCH_SERVICE_RESOURCE_GROUP) + AZURE_SEARCH_SERVICE_LOCATION: $(AZURE_SEARCH_SERVICE_LOCATION) + AZURE_SEARCH_SERVICE_SKU: $(AZURE_SEARCH_SERVICE_SKU) + AZURE_SEARCH_QUERY_LANGUAGE: $(AZURE_SEARCH_QUERY_LANGUAGE) + AZURE_SEARCH_QUERY_SPELLER: $(AZURE_SEARCH_QUERY_SPELLER) + AZURE_SEARCH_SEMANTIC_RANKER: $(AZURE_SEARCH_SEMANTIC_RANKER) + AZURE_STORAGE_ACCOUNT: $(AZURE_STORAGE_ACCOUNT) + AZURE_STORAGE_RESOURCE_GROUP: $(AZURE_STORAGE_RESOURCE_GROUP) + AZURE_STORAGE_SKU: $(AZURE_STORAGE_SKU) + AZURE_APP_SERVICE_SKU: $(AZURE_APP_SERVICE_SKU) + AZURE_OPENAI_CHATGPT_MODEL: $(AZURE_OPENAI_CHATGPT_MODEL) + AZURE_OPENAI_CHATGPT_DEPLOYMENT: $(AZURE_OPENAI_CHATGPT_DEPLOYMENT) + AZURE_OPENAI_CHATGPT_DEPLOYMENT_CAPACITY: $(AZURE_OPENAI_CHATGPT_DEPLOYMENT_CAPACITY) + AZURE_OPENAI_CHATGPT_DEPLOYMENT_VERSION: $(AZURE_OPENAI_CHATGPT_DEPLOYMENT_VERSION) + AZURE_OPENAI_EMB_MODEL_NAME: $(AZURE_OPENAI_EMB_MODEL_NAME) + AZURE_OPENAI_EMB_DEPLOYMENT: $(AZURE_OPENAI_EMB_DEPLOYMENT) + AZURE_OPENAI_EMB_DEPLOYMENT_CAPACITY: $(AZURE_OPENAI_EMB_DEPLOYMENT_CAPACITY) + AZURE_OPENAI_EMB_DEPLOYMENT_VERSION: $(AZURE_OPENAI_EMB_DEPLOYMENT_VERSION) + AZURE_OPENAI_EMB_DIMENSIONS: $(AZURE_OPENAI_EMB_DIMENSIONS) + OPENAI_HOST: $(OPENAI_HOST) + OPENAI_API_KEY: $(OPENAI_API_KEY) + OPENAI_ORGANIZATION: $(OPENAI_ORGANIZATION) + AZURE_USE_APPLICATION_INSIGHTS: $(AZURE_USE_APPLICATION_INSIGHTS) + AZURE_APPLICATION_INSIGHTS: $(AZURE_APPLICATION_INSIGHTS) + AZURE_APPLICATION_INSIGHTS_DASHBOARD: $(AZURE_APPLICATION_INSIGHTS_DASHBOARD) + AZURE_LOG_ANALYTICS: $(AZURE_LOG_ANALYTICS) + USE_VECTORS: $(USE_VECTORS) + USE_GPT4V: $(USE_GPT4V) + AZURE_VISION_ENDPOINT: $(AZURE_VISION_ENDPOINT) + VISION_SECRET_NAME: $(VISION_SECRET_NAME) + AZURE_COMPUTER_VISION_SERVICE: $(AZURE_COMPUTER_VISION_SERVICE) + AZURE_COMPUTER_VISION_RESOURCE_GROUP: $(AZURE_COMPUTER_VISION_RESOURCE_GROUP) + AZURE_COMPUTER_VISION_LOCATION: $(AZURE_COMPUTER_VISION_LOCATION) + AZURE_COMPUTER_VISION_SKU: $(AZURE_COMPUTER_VISION_SKU) + USE_SPEECH_INPUT_BROWSER: $(USE_SPEECH_INPUT_BROWSER) + USE_SPEECH_OUTPUT_BROWSER: $(USE_SPEECH_OUTPUT_BROWSER) + USE_SPEECH_OUTPUT_AZURE: $(USE_SPEECH_OUTPUT_AZURE) + AZURE_SPEECH_SERVICE: $(AZURE_SPEECH_SERVICE) + AZURE_SPEECH_SERVICE_RESOURCE_GROUP: $(AZURE_SPEECH_SERVICE_RESOURCE_GROUP) + AZURE_SPEECH_SERVICE_LOCATION: $(AZURE_SPEECH_SERVICE_LOCATION) + AZURE_SPEECH_SERVICE_SKU: $(AZURE_SPEECH_SERVICE_SKU) + AZURE_KEY_VAULT_NAME: $(AZURE_KEY_VAULT_NAME) + AZURE_USE_AUTHENTICATION: $(AZURE_USE_AUTHENTICATION) + AZURE_ENFORCE_ACCESS_CONTROL: $(AZURE_ENFORCE_ACCESS_CONTROL) + AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS: $(AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS) + AZURE_ENABLE_UNAUTHENTICATED_ACCESS: $(AZURE_ENABLE_UNAUTHENTICATED_ACCESS) + AZURE_TENANT_ID: $(AZURE_TENANT_ID) + AZURE_AUTH_TENANT_ID: $(AZURE_AUTH_TENANT_ID) + AZURE_SERVER_APP_ID: $(AZURE_SERVER_APP_ID) + AZURE_CLIENT_APP_ID: $(AZURE_CLIENT_APP_ID) + ALLOWED_ORIGIN: $(ALLOWED_ORIGIN) + AZURE_SERVER_APP_SECRET: $(AZURE_SERVER_APP_SECRET) + AZURE_CLIENT_APP_SECRET: $(AZURE_CLIENT_APP_SECRET) + AZURE_ADLS_GEN2_STORAGE_ACCOUNT: $(AZURE_ADLS_GEN2_STORAGE_ACCOUNT) + AZURE_ADLS_GEN2_FILESYSTEM_PATH: $(AZURE_ADLS_GEN2_FILESYSTEM_PATH) + AZURE_ADLS_GEN2_FILESYSTEM: $(AZURE_ADLS_GEN2_FILESYSTEM) + + - task: AzureCLI@2 + displayName: Deploy Application + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + azd deploy --no-prompt diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c025b04874..2e430c2b85 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,31 +1,33 @@ -{ - "name": "Azure Search OpenAI Demo", - "image": "mcr.microsoft.com/devcontainers/python:3.11", - "features": { - "ghcr.io/devcontainers/features/node:1": { - // This should match the version of Node.js in Github Actions workflows - "version": "18", - "nodeGypDependencies": false - }, - "ghcr.io/devcontainers/features/azure-cli:1.0.8": {}, - "ghcr.io/azure/azure-dev/azd:latest": {} - }, - "customizations": { - "vscode": { - "extensions": [ - "ms-azuretools.azure-dev", - "ms-azuretools.vscode-bicep", - "ms-python.python", - "esbenp.prettier-vscode" - ] - } - }, - "forwardPorts": [ - 50505 - ], - "postCreateCommand": "", - "remoteUser": "vscode", - "hostRequirements": { - "memory": "8gb" - } -} +{ + "name": "Azure Search OpenAI Demo", + "image": "mcr.microsoft.com/devcontainers/python:3.11", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "18", + "nodeGypDependencies": false + }, + "ghcr.io/devcontainers/features/azure-cli:1.0.8": {}, + "ghcr.io/azure/azure-dev/azd:latest": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.azure-dev", + "ms-azuretools.vscode-bicep", + "ms-python.python", + "esbenp.prettier-vscode", + "ms-azuretools.vscode-azurefunctions", + "ms-azuretools.vscode-azurestorage", + "ms-vscode.azurecli" + ] + } + }, + "forwardPorts": [ + 50505 + ], + "postCreateCommand": "apt-get update && apt-get install -y wget apt-transport-https software-properties-common && wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb && dpkg -i packages-microsoft-prod.deb && apt-get update && apt-get install -y powershell", + "remoteUser": "vscode", + "hostRequirements": { + "memory": "8gb" + } +} diff --git a/.gitattributes b/.gitattributes index 99f84ac39d..4950d90c30 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ -*.sh text eol=lf -*.jsonlines text eol=lf +*.sh text eol=lf +*.jsonlines text eol=lf diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index f9ba8cf65f..c72a5749c5 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -1,9 +1,9 @@ -# Microsoft Open Source Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). - -Resources: - -- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) -- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) -- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3f7c1a7f70..09f1145cd5 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,36 +1,36 @@ - -> Please provide us with the following information: -> --------------------------------------------------------------- - -### This issue is for a: (mark with an `x`) -``` -- [ ] bug report -> please search issues before submitting -- [ ] feature request -- [ ] documentation issue or request -- [ ] regression (a behavior that used to work and stopped in a new release) -``` - -### Minimal steps to reproduce -> - -### Any log messages given by the failure -> - -### Expected/desired behavior -> - -### OS and Version? -> Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) - -### azd version? -> run `azd version` and copy paste here. - -### Versions -> - -### Mention any other details that might be useful - -> --------------------------------------------------------------- -> Thanks! We'll be in touch soon. + +> Please provide us with the following information: +> --------------------------------------------------------------- + +### This issue is for a: (mark with an `x`) +``` +- [ ] bug report -> please search issues before submitting +- [ ] feature request +- [ ] documentation issue or request +- [ ] regression (a behavior that used to work and stopped in a new release) +``` + +### Minimal steps to reproduce +> + +### Any log messages given by the failure +> + +### Expected/desired behavior +> + +### OS and Version? +> Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) + +### azd version? +> run `azd version` and copy paste here. + +### Versions +> + +### Mention any other details that might be useful + +> --------------------------------------------------------------- +> Thanks! We'll be in touch soon. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3303aaca8c..116d0e4669 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,46 +1,46 @@ -## Purpose - - - - -## Does this introduce a breaking change? - -When developers merge from main and run the server, azd up, or azd deploy, will this produce an error? -If you're not sure, try it out on an old environment. - -``` -[ ] Yes -[ ] No -``` - -## Does this require changes to learn.microsoft.com docs? - -This repository is referenced by [this tutorial](https://learn.microsoft.com/azure/developer/python/get-started-app-chat-template) -which includes deployment, settings and usage instructions. If text or screenshot need to change in the tutorial, -check the box below and notify the tutorial author. A Microsoft employee can do this for you if you're an external contributor. - -``` -[ ] Yes -[ ] No -``` - -## Type of change - -``` -[ ] Bugfix -[ ] Feature -[ ] Code style update (formatting, local variables) -[ ] Refactoring (no functional changes, no api changes) -[ ] Documentation content changes -[ ] Other... Please describe: -``` - -## Code quality checklist - -See [CONTRIBUTING.md](https://github.com/Azure-Samples/azure-search-openai-demo/blob/main/CONTRIBUTING.md#submit-pr) for more details. - -- [ ] The current tests all pass (`python -m pytest`). -- [ ] I added tests that prove my fix is effective or that my feature works -- [ ] I ran `python -m pytest --cov` to verify 100% coverage of added lines -- [ ] I ran `python -m mypy` to check for type errors -- [ ] I either used the pre-commit hooks or ran `ruff` and `black` manually on my code. +## Purpose + + + + +## Does this introduce a breaking change? + +When developers merge from main and run the server, azd up, or azd deploy, will this produce an error? +If you're not sure, try it out on an old environment. + +``` +[ ] Yes +[ ] No +``` + +## Does this require changes to learn.microsoft.com docs? + +This repository is referenced by [this tutorial](https://learn.microsoft.com/azure/developer/python/get-started-app-chat-template) +which includes deployment, settings and usage instructions. If text or screenshot need to change in the tutorial, +check the box below and notify the tutorial author. A Microsoft employee can do this for you if you're an external contributor. + +``` +[ ] Yes +[ ] No +``` + +## Type of change + +``` +[ ] Bugfix +[ ] Feature +[ ] Code style update (formatting, local variables) +[ ] Refactoring (no functional changes, no api changes) +[ ] Documentation content changes +[ ] Other... Please describe: +``` + +## Code quality checklist + +See [CONTRIBUTING.md](https://github.com/Azure-Samples/azure-search-openai-demo/blob/main/CONTRIBUTING.md#submit-pr) for more details. + +- [ ] The current tests all pass (`python -m pytest`). +- [ ] I added tests that prove my fix is effective or that my feature works +- [ ] I ran `python -m pytest --cov` to verify 100% coverage of added lines +- [ ] I ran `python -m mypy` to check for type errors +- [ ] I either used the pre-commit hooks or ran `ruff` and `black` manually on my code. diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 2b691d3dc4..bbb7382af6 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,30 +1,30 @@ -version: 2 -updates: - - # Maintain dependencies for GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - groups: - github-actions: - patterns: - - "*" - - # Maintain dependencies for npm - - package-ecosystem: "npm" - directory: "/app/frontend" - schedule: - interval: "weekly" - - # Maintain dependencies for pip - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "weekly" - groups: - python-requirements: - patterns: - - "*" - ignore: - - dependency-name: azure-search-documents +version: 2 +updates: + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + github-actions: + patterns: + - "*" + + # Maintain dependencies for npm + - package-ecosystem: "npm" + directory: "/app/frontend" + schedule: + interval: "weekly" + + # Maintain dependencies for pip + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + groups: + python-requirements: + patterns: + - "*" + ignore: + - dependency-name: azure-search-documents diff --git a/.github/workflows/azure-dev-validation.yaml b/.github/workflows/azure-dev-validation.yaml deleted file mode 100644 index 849ced2ada..0000000000 --- a/.github/workflows/azure-dev-validation.yaml +++ /dev/null @@ -1,54 +0,0 @@ -name: Validate AZD template -on: - push: - branches: [ main ] - paths: - - "infra/**" - pull_request: - branches: [ main ] - paths: - - "infra/**" - workflow_dispatch: - -jobs: - bicep: - runs-on: ubuntu-latest - permissions: - security-events: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Build Bicep for linting - uses: azure/CLI@v2 - with: - inlineScript: az config set bicep.use_binary_from_path=false && az bicep build -f infra/main.bicep --stdout - - psrule: - runs-on: ubuntu-latest - permissions: - security-events: write - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Run PSRule analysis - uses: microsoft/ps-rule@v2.9.0 - with: - modules: PSRule.Rules.Azure - baseline: Azure.Pillar.Security - inputPath: infra/*.test.bicep - outputFormat: Sarif - outputPath: reports/ps-rule-results.sarif - summary: true - continue-on-error: true - - env: - PSRULE_CONFIGURATION_AZURE_BICEP_FILE_EXPANSION: 'true' - PSRULE_CONFIGURATION_AZURE_BICEP_FILE_EXPANSION_TIMEOUT: '30' - - - name: Upload results to security tab - uses: github/codeql-action/upload-sarif@v3 - if: github.repository == 'Azure-Samples/azure-search-openai-demo' - with: - sarif_file: reports/ps-rule-results.sarif diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml deleted file mode 100644 index b00a1bb0ee..0000000000 --- a/.github/workflows/azure-dev.yml +++ /dev/null @@ -1,139 +0,0 @@ -name: Deploy - -on: - workflow_dispatch: - push: - # Run when commits are pushed to mainline branch (main or master) - # Set this to the mainline branch you are using - branches: - - main - - master - -# GitHub Actions workflow to deploy to Azure using azd -# To configure required secrets for connecting to Azure, simply run `azd pipeline config` - -# Set up permissions for deploying with secretless Azure federated credentials -# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication -permissions: - id-token: write - contents: read - -jobs: - build: - runs-on: ubuntu-latest - env: - # azd required - AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} - AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} - AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} - # project specific - AZURE_OPENAI_SERVICE: ${{ vars.AZURE_OPENAI_SERVICE }} - AZURE_OPENAI_API_VERSION: ${{ vars.AZURE_OPENAI_API_VERSION }} - AZURE_OPENAI_RESOURCE_GROUP: ${{ vars.AZURE_OPENAI_RESOURCE_GROUP }} - AZURE_COMPUTER_VISION_SERVICE: ${{ vars.AZURE_COMPUTER_VISION_SERVICE }} - AZURE_COMPUTER_VISION_RESOURCE_GROUP: ${{ vars.AZURE_COMPUTER_VISION_RESOURCE_GROUP }} - AZURE_COMPUTER_VISION_LOCATION: ${{ vars.AZURE_COMPUTER_VISION_LOCATION }} - AZURE_COMPUTER_VISION_SKU: ${{ vars.AZURE_COMPUTER_VISION_SKU }} - AZURE_FORMRECOGNIZER_SERVICE: ${{ vars.AZURE_FORMRECOGNIZER_SERVICE }} - AZURE_FORMRECOGNIZER_RESOURCE_GROUP: ${{ vars.AZURE_FORMRECOGNIZER_RESOURCE_GROUP }} - AZURE_FORMRECOGNIZER_SKU: ${{ vars.AZURE_FORMRECOGNIZER_SKU }} - AZURE_SEARCH_INDEX: ${{ vars.AZURE_SEARCH_INDEX }} - AZURE_SEARCH_SERVICE: ${{ vars.AZURE_SEARCH_SERVICE }} - AZURE_SEARCH_SERVICE_RESOURCE_GROUP: ${{ vars.AZURE_SEARCH_SERVICE_RESOURCE_GROUP }} - AZURE_SEARCH_SERVICE_LOCATION: ${{ vars.AZURE_SEARCH_SERVICE_LOCATION }} - AZURE_SEARCH_SERVICE_SKU: ${{ vars.AZURE_SEARCH_SERVICE_SKU }} - AZURE_SEARCH_QUERY_LANGUAGE: ${{ vars.AZURE_SEARCH_QUERY_LANGUAGE }} - AZURE_SEARCH_QUERY_SPELLER: ${{ vars.AZURE_SEARCH_QUERY_SPELLER }} - AZURE_SEARCH_SEMANTIC_RANKER: ${{ vars.AZURE_SEARCH_SEMANTIC_RANKER }} - AZURE_STORAGE_ACCOUNT: ${{ vars.AZURE_STORAGE_ACCOUNT }} - AZURE_STORAGE_RESOURCE_GROUP: ${{ vars.AZURE_STORAGE_RESOURCE_GROUP }} - AZURE_STORAGE_SKU: ${{ vars.AZURE_STORAGE_SKU }} - AZURE_APP_SERVICE_PLAN: ${{ vars.AZURE_APP_SERVICE_PLAN }} - AZURE_APP_SERVICE_SKU: ${{ vars.AZURE_APP_SERVICE_SKU }} - AZURE_APP_SERVICE: ${{ vars.AZURE_APP_SERVICE }} - AZURE_OPENAI_CHATGPT_MODEL: ${{ vars.AZURE_OPENAI_CHATGPT_MODEL }} - AZURE_OPENAI_CHATGPT_DEPLOYMENT: ${{ vars.AZURE_OPENAI_CHATGPT_DEPLOYMENT }} - AZURE_OPENAI_CHATGPT_DEPLOYMENT_CAPACITY: ${{ vars.AZURE_OPENAI_CHATGPT_DEPLOYMENT_CAPACITY }} - AZURE_OPENAI_CHATGPT_DEPLOYMENT_VERSION: ${{ vars.AZURE_OPENAI_CHATGPT_DEPLOYMENT_VERSION }} - AZURE_OPENAI_EMB_MODEL_NAME: ${{ vars.AZURE_OPENAI_EMB_MODEL_NAME }} - AZURE_OPENAI_EMB_DEPLOYMENT: ${{ vars.AZURE_OPENAI_EMB_DEPLOYMENT }} - AZURE_OPENAI_EMB_DEPLOYMENT_CAPACITY: ${{ vars.AZURE_OPENAI_EMB_DEPLOYMENT_CAPACITY }} - AZURE_OPENAI_EMB_DEPLOYMENT_VERSION: ${{ vars.AZURE_OPENAI_EMB_DEPLOYMENT_VERSION }} - AZURE_OPENAI_EMB_DIMENSIONS: ${{ vars.AZURE_OPENAI_EMB_DIMENSIONS }} - OPENAI_HOST: ${{ vars.OPENAI_HOST }} - OPENAI_API_KEY: ${{ vars.OPENAI_API_KEY }} - OPENAI_ORGANIZATION: ${{ vars.OPENAI_ORGANIZATION }} - AZURE_USE_APPLICATION_INSIGHTS: ${{ vars.AZURE_USE_APPLICATION_INSIGHTS }} - AZURE_APPLICATION_INSIGHTS: ${{ vars.AZURE_APPLICATION_INSIGHTS }} - AZURE_APPLICATION_INSIGHTS_DASHBOARD: ${{ vars.AZURE_APPLICATION_INSIGHTS_DASHBOARD }} - AZURE_LOG_ANALYTICS: ${{ vars.AZURE_LOG_ANALYTICS }} - USE_VECTORS: ${{ vars.USE_VECTORS }} - USE_GPT4V: ${{ vars.USE_GPT4V }} - AZURE_VISION_ENDPOINT: ${{ vars.AZURE_VISION_ENDPOINT }} - VISION_SECRET_NAME: ${{ vars.VISION_SECRET_NAME }} - USE_SPEECH_INPUT_BROWSER: ${{ vars.USE_SPEECH_INPUT_BROWSER }} - USE_SPEECH_OUTPUT_BROWSER: ${{ vars.USE_SPEECH_OUTPUT_BROWSER }} - USE_SPEECH_OUTPUT_AZURE: ${{ vars.USE_SPEECH_OUTPUT_AZURE }} - AZURE_SPEECH_SERVICE: ${{ vars.AZURE_SPEECH_SERVICE }} - AZURE_SPEECH_SERVICE_RESOURCE_GROUP: ${{ vars.AZURE_SPEECH_RESOURCE_GROUP }} - AZURE_SPEECH_SERVICE_LOCATION: ${{ vars.AZURE_SPEECH_SERVICE_LOCATION }} - AZURE_SPEECH_SERVICE_SKU: ${{ vars.AZURE_SPEECH_SERVICE_SKU }} - AZURE_KEY_VAULT_NAME: ${{ vars.AZURE_KEY_VAULT_NAME }} - AZURE_USE_AUTHENTICATION: ${{ vars.AZURE_USE_AUTHENTICATION }} - AZURE_ENFORCE_ACCESS_CONTROL: ${{ vars.AZURE_ENFORCE_ACCESS_CONTROL }} - AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS: ${{ vars.AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS }} - AZURE_ENABLE_UNAUTHENTICATED_ACCESS: ${{ vars.AZURE_ENABLE_UNAUTHENTICATED_ACCESS }} - AZURE_AUTH_TENANT_ID: ${{ vars.AZURE_AUTH_TENANT_ID }} - AZURE_SERVER_APP_ID: ${{ vars.AZURE_SERVER_APP_ID }} - AZURE_CLIENT_APP_ID: ${{ vars.AZURE_CLIENT_APP_ID }} - ALLOWED_ORIGIN: ${{ vars.ALLOWED_ORIGIN }} - AZURE_ADLS_GEN2_STORAGE_ACCOUNT: ${{ vars.AZURE_ADLS_GEN2_STORAGE_ACCOUNT }} - AZURE_ADLS_GEN2_FILESYSTEM_PATH: ${{ vars.AZURE_ADLS_GEN2_FILESYSTEM_PATH }} - AZURE_ADLS_GEN2_FILESYSTEM: ${{ vars.AZURE_ADLS_GEN2_FILESYSTEM }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install azd - uses: Azure/setup-azd@v1.0.0 - - - name: Install Nodejs - uses: actions/setup-node@v4 - with: - node-version: 18 - - - name: Log in with Azure (Federated Credentials) - if: ${{ env.AZURE_CLIENT_ID != '' }} - run: | - azd auth login ` - --client-id "$Env:AZURE_CLIENT_ID" ` - --federated-credential-provider "github" ` - --tenant-id "$Env:AZURE_TENANT_ID" - shell: pwsh - - - name: Log in with Azure (Client Credentials) - if: ${{ env.AZURE_CREDENTIALS != '' }} - run: | - $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; - Write-Host "::add-mask::$($info.clientSecret)" - - azd auth login ` - --client-id "$($info.clientId)" ` - --client-secret "$($info.clientSecret)" ` - --tenant-id "$($info.tenantId)" - shell: pwsh - env: - AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} - - - name: Provision Infrastructure - run: azd provision --no-prompt - env: - AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} - AZURE_SERVER_APP_SECRET: ${{ secrets.AZURE_SERVER_APP_SECRET }} - AZURE_CLIENT_APP_SECRET: ${{ secrets.AZURE_CLIENT_APP_SECRET }} - - - name: Deploy Application - run: azd deploy --no-prompt diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml deleted file mode 100644 index 3fea722f46..0000000000 --- a/.github/workflows/frontend.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: Frontend linting - -on: - push: - branches: [ main ] - paths: - - "app/frontend/**" - pull_request: - branches: [ main ] - paths: - - "app/frontend/**" - -jobs: - prettier: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Run prettier on frontend - run: | - cd ./app/frontend - npm install - npx prettier --check . diff --git a/.github/workflows/lint-markdown.yml b/.github/workflows/lint-markdown.yml deleted file mode 100644 index d1a573f3ea..0000000000 --- a/.github/workflows/lint-markdown.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Validate Markdown - -on: - pull_request: - branches: - - main - paths: - - '**.md' - -jobs: - lint-markdown: - name: Check for Markdown linting errors - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - name: Run markdownlint - uses: articulate/actions-markdownlint@v1 - with: - config: .github/workflows/markdownlint-config.json - files: '**/*.md' - ignore: data/ diff --git a/.github/workflows/markdownlint-config.json b/.github/workflows/markdownlint-config.json deleted file mode 100644 index 5f4341b93b..0000000000 --- a/.github/workflows/markdownlint-config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "default": true, - "line-length": false, - "MD033": { "allowed_elements": ["br", "details", "summary"] } -} diff --git a/.github/workflows/nightly-jobs.yaml b/.github/workflows/nightly-jobs.yaml deleted file mode 100644 index cfd9b84f38..0000000000 --- a/.github/workflows/nightly-jobs.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: Nightly Jobs - -on: - schedule: - - cron: '0 0 * * *' - workflow_dispatch: - -jobs: - python-test: - uses: ./.github/workflows/python-test.yaml diff --git a/.github/workflows/python-test.yaml b/.github/workflows/python-test.yaml deleted file mode 100644 index 8d5fbb8723..0000000000 --- a/.github/workflows/python-test.yaml +++ /dev/null @@ -1,73 +0,0 @@ -name: Python check - -on: - push: - branches: [ main ] - paths-ignore: - - "**.md" - - ".azdo/**" - - ".devcontainer/**" - - ".github/**" - pull_request: - branches: [ main ] - paths-ignore: - - "**.md" - - ".azdo/**" - - ".devcontainer/**" - - ".github/**" - workflow_call: - -jobs: - test_package: - name: Test ${{ matrix.os }} Python ${{ matrix.python_version }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: ["ubuntu-20.04", "windows-latest"] - python_version: ["3.9", "3.10", "3.11", "3.12"] - steps: - - uses: actions/checkout@v4 - - name: Setup python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python_version }} - architecture: x64 - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: 18 - - name: Build frontend - run: | - cd ./app/frontend - npm install - npm run build - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements-dev.txt - - name: Lint with ruff - run: ruff . - - name: Check types with mypy - run: | - cd scripts/ - python3 -m mypy . --config-file=../pyproject.toml - cd ../app/backend/ - python3 -m mypy . --config-file=../../pyproject.toml - - name: Check formatting with black - run: black . --check --verbose - - name: Run Python tests - if: runner.os != 'Windows' - run: python3 -m pytest -s -vv --cov --cov-fail-under=87 - - name: Run E2E tests with Playwright - id: e2e - if: runner.os != 'Windows' - run: | - playwright install chromium --with-deps - python3 -m pytest tests/e2e.py --tracing=retain-on-failure - - name: Upload test artifacts - if: ${{ failure() && steps.e2e.conclusion == 'failure' }} - uses: actions/upload-artifact@v4 - with: - name: playwright-traces${{ matrix.python_version }} - path: test-results diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml deleted file mode 100644 index 20d24d2d82..0000000000 --- a/.github/workflows/stale-bot.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: 'Close stale issues and PRs' -on: - schedule: - - cron: '30 1 * * *' - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v9 - with: - stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this issue will be closed.' - stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed.' - close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' - close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' - days-before-issue-stale: 60 - days-before-pr-stale: 60 - days-before-issue-close: -1 - days-before-pr-close: -1 diff --git a/.github/workflows/validate-markdown.yml b/.github/workflows/validate-markdown.yml deleted file mode 100644 index 6342e8934a..0000000000 --- a/.github/workflows/validate-markdown.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Validate Markdown - -on: - # Trigger the workflow on pull request - pull_request_target: - branches: - - main - paths: - - '**.md' - - '**.ipynb' - -permissions: - contents: read - pull-requests: write - -jobs: - check-broken-paths: - name: Check Broken Relative Paths - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Check broken Paths - id: check-broken-paths - uses: john0isaac/action-check-markdown@v1.0.6 - with: - command: check_broken_paths - directory: ./ - guide-url: 'https://github.com/Azure-Samples/azure-search-openai-demo/blob/main/CONTRIBUTING.md' - github-token: ${{ secrets.GITHUB_TOKEN }} - check-urls-locale: - if: ${{ always() }} - needs: check-broken-paths - name: Check URLs Don't Have Locale - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Run Check URLs Country Locale - id: check-urls-locale - uses: john0isaac/action-check-markdown@v1.0.6 - with: - command: check_urls_locale - directory: ./ - guide-url: 'https://github.com/Azure-Samples/azure-search-openai-demo/blob/main/CONTRIBUTING.md' - github-token: ${{ secrets.GITHUB_TOKEN }} - check-broken-urls: - if: ${{ always() }} - name: Check Broken URLs - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - name: Run Check Broken URLs - id: check-broken-urls - uses: john0isaac/action-check-markdown@v1.0.6 - with: - command: check_broken_urls - directory: ./ - guide-url: 'https://github.com/Azure-Samples/azure-search-openai-demo/blob/main/CONTRIBUTING.md' - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index e51f3af2e2..871c38cd7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,151 +1,151 @@ -# Azure az webapp deployment details -.azure -*_env - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# NPM -npm-debug.log* -node_modules -static/ - -data/**/*.md5 - -.DS_Store +# Azure az webapp deployment details +.azure +*_env + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# NPM +npm-debug.log* +node_modules +static/ + +data/**/*.md5 + +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cca9034f9e..587375bb95 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,21 +1,21 @@ -exclude: '^tests/snapshots/' -repos: -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 - hooks: - - id: check-yaml - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.14 - hooks: - - id: ruff -- repo: https://github.com/psf/black - rev: 24.1.0 - hooks: - - id: black -- repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 - hooks: - - id: prettier - types_or: [css, javascript, ts, tsx, html] +exclude: '^tests/snapshots/' +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.14 + hooks: + - id: ruff +- repo: https://github.com/psf/black + rev: 24.1.0 + hooks: + - id: black +- repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + types_or: [css, javascript, ts, tsx, html] diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c38e4736dc..f83e1d6aac 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,8 +1,8 @@ -{ - "recommendations": [ - "ms-azuretools.azure-dev", - "ms-azuretools.vscode-bicep", - "ms-python.python", - "esbenp.prettier-vscode" - ] -} +{ + "recommendations": [ + "ms-azuretools.azure-dev", + "ms-azuretools.vscode-bicep", + "ms-python.python", + "esbenp.prettier-vscode" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 5a83dfd713..1555324a41 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,52 +1,52 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Quart", - "type": "debugpy", - "request": "launch", - "module": "quart", - "cwd": "${workspaceFolder}/app/backend", - "python": "${workspaceFolder}/.venv/bin/python", - "env": { - "QUART_APP": "main:app", - "QUART_ENV": "development", - "QUART_DEBUG": "0" - }, - "args": [ - "run", - "--no-reload", - "-p 50505" - ], - "console": "integratedTerminal", - "justMyCode": false, - "envFile": "${input:dotEnvFilePath}", - }, - { - "name": "Frontend: watch", - "type": "node-terminal", - "request": "launch", - "command": "npm run dev", - "cwd": "${workspaceFolder}/app/frontend", - }, - { - "name": "Python: Debug Tests", - "type": "debugpy", - "request": "launch", - "program": "${file}", - "purpose": ["debug-test"], - "console": "integratedTerminal", - "justMyCode": false - } - ], - "inputs": [ - { - "id": "dotEnvFilePath", - "type": "command", - "command": "azure-dev.commands.getDotEnvFilePath" - } - ] -} +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Quart", + "type": "debugpy", + "request": "launch", + "module": "quart", + "cwd": "${workspaceFolder}/app/backend", + "python": "${workspaceFolder}/.venv/bin/python", + "env": { + "QUART_APP": "main:app", + "QUART_ENV": "development", + "QUART_DEBUG": "0" + }, + "args": [ + "run", + "--no-reload", + "-p 50505" + ], + "console": "integratedTerminal", + "justMyCode": false, + "envFile": "${input:dotEnvFilePath}", + }, + { + "name": "Frontend: watch", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev", + "cwd": "${workspaceFolder}/app/frontend", + }, + { + "name": "Python: Debug Tests", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "purpose": ["debug-test"], + "console": "integratedTerminal", + "justMyCode": false + } + ], + "inputs": [ + { + "id": "dotEnvFilePath", + "type": "command", + "command": "azure-dev.commands.getDotEnvFilePath" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index aae6b8db93..78acc53995 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,34 +1,34 @@ -{ - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[css]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "files.exclude": { - "**/__pycache__": true, - "**/.coverage": true, - "**/.pytest_cache": true, - "**/.ruff_cache": true, - "**/.mypy_cache": true - }, - "search.exclude": { - "**/node_modules": true, - "static": true - }, - "python.testing.pytestArgs": [ - "tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true -} +{ + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[css]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "files.exclude": { + "**/__pycache__": true, + "**/.coverage": true, + "**/.pytest_cache": true, + "**/.ruff_cache": true, + "**/.mypy_cache": true + }, + "search.exclude": { + "**/node_modules": true, + "static": true + }, + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1ca7d896d4..609fab648e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,35 +1,35 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Start App", - "type": "dotenv", - "targetTasks": [ - "Start App (Script)" - ], - "file": "${input:dotEnvFilePath}" - }, - { - "label": "Start App (Script)", - "type": "shell", - "command": "${workspaceFolder}/app/start.sh", - "windows": { - "command": "pwsh ${workspaceFolder}/app/start.ps1" - }, - "presentation": { - "reveal": "silent" - }, - "options": { - "cwd": "${workspaceFolder}/app" - }, - "problemMatcher": [] - } - ], - "inputs": [ - { - "id": "dotEnvFilePath", - "type": "command", - "command": "azure-dev.commands.getDotEnvFilePath" - } - ] +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start App", + "type": "dotenv", + "targetTasks": [ + "Start App (Script)" + ], + "file": "${input:dotEnvFilePath}" + }, + { + "label": "Start App (Script)", + "type": "shell", + "command": "${workspaceFolder}/app/start.sh", + "windows": { + "command": "pwsh ${workspaceFolder}/app/start.ps1" + }, + "presentation": { + "reveal": "silent" + }, + "options": { + "cwd": "${workspaceFolder}/app" + }, + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "dotEnvFilePath", + "type": "command", + "command": "azure-dev.commands.getDotEnvFilePath" + } + ] } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index efffe0b9a8..09f31d6262 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,162 +1,162 @@ -# Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit . - -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -- [Code of Conduct](#code-of-conduct) -- [Found an Issue?](#found-an-issue) -- [Want a Feature?](#want-a-feature) -- [Submission Guidelines](#submission-guidelines) - - [Submitting an Issue](#submitting-an-issue) - - [Submitting a Pull Request (PR)](#submitting-a-pull-request-pr) -- [Setting up the development environment](#setting-up-the-development-environment) -- [Running unit tests](#running-unit-tests) -- [Running E2E tests](#running-e2e-tests) -- [Code Style](#code-style) - -## Code of Conduct - -Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). - -## Found an Issue? - -If you find a bug in the source code or a mistake in the documentation, you can help us by -[submitting an issue](#submitting-an-issue) to the GitHub Repository. Even better, you can -[submit a Pull Request](#submitting-a-pull-request-pr) with a fix. - -## Want a Feature? - -You can *request* a new feature by [submitting an issue](#submitting-an-issue) to the GitHub -Repository. If you would like to *implement* a new feature, please submit an issue with -a proposal for your work first, to be sure that we can use it. - -- **Small Features** can be crafted and directly [submitted as a Pull Request](#submitting-a-pull-request-pr). - -## Submission Guidelines - -### Submitting an Issue - -Before you submit an issue, search the archive, maybe your question was already answered. - -If your issue appears to be a bug, and hasn't been reported, open a new issue. -Help us to maximize the effort we can spend fixing issues and adding new -features, by not reporting duplicate issues. Providing the following information will increase the -chances of your issue being dealt with quickly: - -- **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps -- **Version** - what version is affected (e.g. 0.1.2) -- **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you -- **Browsers and Operating System** - is this a problem with all browsers? -- **Reproduce the Error** - provide a live example or a unambiguous set of steps -- **Related Issues** - has a similar issue been reported before? -- **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be - causing the problem (line of code or commit) - -You can file new issues by providing the above information at the corresponding repository's issues link: ]/[repository-name]/issues/new]. - -### Submitting a Pull Request (PR) - -Before you submit your Pull Request (PR) consider the following guidelines: - -- Search the repository (]/[repository-name]/pulls) for an open or closed PR - that relates to your submission. You don't want to duplicate effort. -- Make your changes in a new git fork -- Follow [Code style conventions](#code-style) -- [Run the tests](#running-unit-tests) (and write new ones, if needed) -- Commit your changes using a descriptive commit message -- Push your fork to GitHub -- In GitHub, create a pull request to the `main` branch of the repository -- Ask a maintainer to review your PR and address any comments they might have - -## Setting up the development environment - -Install the development dependencies: - -```shell -python -m pip install -r requirements-dev.txt -``` - -Install the pre-commit hooks: - -```shell -pre-commit install -``` - -Compile the JavaScript: - -```shell -( cd ./app/frontend ; npm install ; npm run build ) -``` - -## Running unit tests - -Run the tests: - -```shell -python -m pytest -``` - -Check the coverage report to make sure your changes are covered. - -```shell -python -m pytest --cov -``` - -## Running E2E tests - -Install Playwright browser dependencies: - -```shell -playwright install --with-deps -``` - -Run the tests: - -```shell -python -m pytest tests/e2e.py --tracing=retain-on-failure -``` - -When a failure happens, the trace zip will be saved in the test-results folder. -You can view that using the Playwright CLI: - -```shell -playwright show-trace test-results/ -``` - -You can also use the online trace viewer at - -## Code Style - -This codebase includes several languages: TypeScript, Python, Bicep, Powershell, and Bash. -Code should follow the standard conventions of each language. - -For Python, you can enforce the conventions using `ruff` and `black`. - -Install the development dependencies: - -```shell -python -m pip install -r requirements-dev.txt -``` - -Run `ruff` to lint a file: - -```shell -python -m ruff -``` - -Run `black` to format a file: - -```shell -python -m black -``` - -If you followed the steps above to install the pre-commit hooks, then you can just wait for those hooks to run `ruff` and `black` for you. +# Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit . + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +- [Code of Conduct](#code-of-conduct) +- [Found an Issue?](#found-an-issue) +- [Want a Feature?](#want-a-feature) +- [Submission Guidelines](#submission-guidelines) + - [Submitting an Issue](#submitting-an-issue) + - [Submitting a Pull Request (PR)](#submitting-a-pull-request-pr) +- [Setting up the development environment](#setting-up-the-development-environment) +- [Running unit tests](#running-unit-tests) +- [Running E2E tests](#running-e2e-tests) +- [Code Style](#code-style) + +## Code of Conduct + +Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +## Found an Issue? + +If you find a bug in the source code or a mistake in the documentation, you can help us by +[submitting an issue](#submitting-an-issue) to the GitHub Repository. Even better, you can +[submit a Pull Request](#submitting-a-pull-request-pr) with a fix. + +## Want a Feature? + +You can *request* a new feature by [submitting an issue](#submitting-an-issue) to the GitHub +Repository. If you would like to *implement* a new feature, please submit an issue with +a proposal for your work first, to be sure that we can use it. + +- **Small Features** can be crafted and directly [submitted as a Pull Request](#submitting-a-pull-request-pr). + +## Submission Guidelines + +### Submitting an Issue + +Before you submit an issue, search the archive, maybe your question was already answered. + +If your issue appears to be a bug, and hasn't been reported, open a new issue. +Help us to maximize the effort we can spend fixing issues and adding new +features, by not reporting duplicate issues. Providing the following information will increase the +chances of your issue being dealt with quickly: + +- **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps +- **Version** - what version is affected (e.g. 0.1.2) +- **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you +- **Browsers and Operating System** - is this a problem with all browsers? +- **Reproduce the Error** - provide a live example or a unambiguous set of steps +- **Related Issues** - has a similar issue been reported before? +- **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be + causing the problem (line of code or commit) + +You can file new issues by providing the above information at the corresponding repository's issues link: ]/[repository-name]/issues/new]. + +### Submitting a Pull Request (PR) + +Before you submit your Pull Request (PR) consider the following guidelines: + +- Search the repository (]/[repository-name]/pulls) for an open or closed PR + that relates to your submission. You don't want to duplicate effort. +- Make your changes in a new git fork +- Follow [Code style conventions](#code-style) +- [Run the tests](#running-unit-tests) (and write new ones, if needed) +- Commit your changes using a descriptive commit message +- Push your fork to GitHub +- In GitHub, create a pull request to the `main` branch of the repository +- Ask a maintainer to review your PR and address any comments they might have + +## Setting up the development environment + +Install the development dependencies: + +```shell +python -m pip install -r requirements-dev.txt +``` + +Install the pre-commit hooks: + +```shell +pre-commit install +``` + +Compile the JavaScript: + +```shell +( cd ./app/frontend ; npm install ; npm run build ) +``` + +## Running unit tests + +Run the tests: + +```shell +python -m pytest +``` + +Check the coverage report to make sure your changes are covered. + +```shell +python -m pytest --cov +``` + +## Running E2E tests + +Install Playwright browser dependencies: + +```shell +playwright install --with-deps +``` + +Run the tests: + +```shell +python -m pytest tests/e2e.py --tracing=retain-on-failure +``` + +When a failure happens, the trace zip will be saved in the test-results folder. +You can view that using the Playwright CLI: + +```shell +playwright show-trace test-results/ +``` + +You can also use the online trace viewer at + +## Code Style + +This codebase includes several languages: TypeScript, Python, Bicep, Powershell, and Bash. +Code should follow the standard conventions of each language. + +For Python, you can enforce the conventions using `ruff` and `black`. + +Install the development dependencies: + +```shell +python -m pip install -r requirements-dev.txt +``` + +Run `ruff` to lint a file: + +```shell +python -m ruff +``` + +Run `black` to format a file: + +```shell +python -m black +``` + +If you followed the steps above to install the pre-commit hooks, then you can just wait for those hooks to run `ruff` and `black` for you. diff --git a/LICENSE b/LICENSE index 493936ef10..65f32d0156 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2023 Azure Samples - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2023 Azure Samples + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c449875eea..a28e1924c4 100644 --- a/README.md +++ b/README.md @@ -1,272 +1,272 @@ -# ChatGPT-like app with your data using Azure OpenAI and Azure AI Search (Python) - -This solution's backend is written in Python. There are also [**JavaScript**](https://aka.ms/azai/js/code), [**.NET**](https://aka.ms/azai/net/code), and [**Java**](https://aka.ms/azai/java/code) samples based on this one. Learn more about [developing AI apps using Azure AI Services](https://aka.ms/azai). - -## Table of Contents - -- [Features](#features) -- [Azure account requirements](#azure-account-requirements) -- [Azure deployment](#azure-deployment) - - [Cost estimation](#cost-estimation) - - [Project setup](#project-setup) - - [GitHub Codespaces](#github-codespaces) - - [VS Code Dev Containers](#vs-code-dev-containers) - - [Local environment](#local-environment) - - [Deploying](#deploying) - - [Deploying again](#deploying-again) -- [Sharing environments](#sharing-environments) -- [Using the app](#using-the-app) -- [Running locally](#running-locally) -- [Monitoring with Application Insights](#monitoring-with-application-insights) -- [Customizing the UI and data](#customizing-the-ui-and-data) -- [Productionizing](#productionizing) -- [Clean up](#clean-up) -- [Troubleshooting](#troubleshooting) -- [Resources](#resources) - -[![Open in GitHub Codespaces](https://img.shields.io/static/v1?style=for-the-badge&label=GitHub+Codespaces&message=Open&color=brightgreen&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=599293758&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestUs2) -[![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/azure-search-openai-demo) - -This sample demonstrates a few approaches for creating ChatGPT-like experiences over your own data using the Retrieval Augmented Generation pattern. It uses Azure OpenAI Service to access a GPT model (gpt-35-turbo), and Azure AI Search for data indexing and retrieval. - -The repo includes sample data so it's ready to try end to end. In this sample application we use a fictitious company called Contoso Electronics, and the experience allows its employees to ask questions about the benefits, internal policies, as well as job descriptions and roles. - -![RAG Architecture](docs/images/appcomponents.png) - -## Features - -- Chat (multi-turn) and Q&A (single turn) interfaces -- Renders citations and thought process for each answer -- Includes settings directly in the UI to tweak the behavior and experiment with options -- Integrates Azure AI Search for indexing and retrieval of documents, with support for [many document formats](/docs/data_ingestion.md#supported-document-formats) as well as [integrated vectorization](/docs/data_ingestion.md#overview-of-integrated-vectorization) -- Optional usage of [GPT-4 with vision](/docs/gpt4vision.md) to reason over image-heavy documents -- Optional addition of [speech input/output](/docs/deploy_features.md#enabling-speech-inputoutput) for accessibility -- Optional automation of [user login and data access](/docs/login_and_acl.md) via Microsoft Entra -- Performance tracing and monitoring with Application Insights - -![Chat screen](docs/images/chatscreen.png) - -[📺 Watch a video overview of the app.](https://youtu.be/3acB0OWmLvM) - -## Azure account requirements - -**IMPORTANT:** In order to deploy and run this example, you'll need: - -- **Azure account**. If you're new to Azure, [get an Azure account for free](https://azure.microsoft.com/free/cognitive-search/) and you'll get some free Azure credits to get started. See [guide to deploying with the free trial](docs/deploy_lowcost.md). -- **Azure subscription with access enabled for the Azure OpenAI service**. You can request access with [this form](https://aka.ms/oaiapply). If your access request to Azure OpenAI service doesn't match the [acceptance criteria](https://learn.microsoft.com/legal/cognitive-services/openai/limited-access?context=%2Fazure%2Fcognitive-services%2Fopenai%2Fcontext%2Fcontext), you can use [OpenAI public API](https://platform.openai.com/docs/api-reference/introduction) instead. Learn [how to switch to an OpenAI instance](docs/deploy_existing.md#openaicom-openai). -- **Azure account permissions**: - - Your Azure account must have `Microsoft.Authorization/roleAssignments/write` permissions, such as [Role Based Access Control Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator-preview), [User Access Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#user-access-administrator), or [Owner](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#owner). If you don't have subscription-level permissions, you must be granted [RBAC](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator-preview) for an existing resource group and [deploy to that existing group](docs/deploy_existing.md#resource-group). - - Your Azure account also needs `Microsoft.Resources/deployments/write` permissions on the subscription level. - -## Azure deployment - -### Cost estimation - -Pricing varies per region and usage, so it isn't possible to predict exact costs for your usage. -However, you can try the [Azure pricing calculator](https://azure.com/e/d18187516e9e421e925b3b311eec8aae) for the resources below. - -- Azure App Service: Basic Tier with 1 CPU core, 1.75 GB RAM. Pricing per hour. [Pricing](https://azure.microsoft.com/pricing/details/app-service/linux/) -- Azure OpenAI: Standard tier, GPT and Ada models. Pricing per 1K tokens used, and at least 1K tokens are used per question. [Pricing](https://azure.microsoft.com/pricing/details/cognitive-services/openai-service/) -- Azure AI Document Intelligence: SO (Standard) tier using pre-built layout. Pricing per document page, sample documents have 261 pages total. [Pricing](https://azure.microsoft.com/pricing/details/form-recognizer/) -- Azure AI Search: Standard tier, 1 replica, free level of semantic search. Pricing per hour. [Pricing](https://azure.microsoft.com/pricing/details/search/) -- Azure Blob Storage: Standard tier with ZRS (Zone-redundant storage). Pricing per storage and read operations. [Pricing](https://azure.microsoft.com/pricing/details/storage/blobs/) -- Azure Monitor: Pay-as-you-go tier. Costs based on data ingested. [Pricing](https://azure.microsoft.com/pricing/details/monitor/) - -To reduce costs, you can switch to free SKUs for various services, but those SKUs have limitations. -See this guide on [deploying with minimal costs](docs/deploy_lowcost.md) for more details. - -⚠️ To avoid unnecessary costs, remember to take down your app if it's no longer in use, -either by deleting the resource group in the Portal or running `azd down`. - -### Project setup - -You have a few options for setting up this project. -The easiest way to get started is GitHub Codespaces, since it will setup all the tools for you, -but you can also [set it up locally](#local-environment) if desired. - -#### GitHub Codespaces - -You can run this repo virtually by using GitHub Codespaces, which will open a web-based VS Code in your browser: - -[![Open in GitHub Codespaces](https://img.shields.io/static/v1?style=for-the-badge&label=GitHub+Codespaces&message=Open&color=brightgreen&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=599293758&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestUs2) - -Once the codespace opens (this may take several minutes), open a terminal window. - -#### VS Code Dev Containers - -A related option is VS Code Dev Containers, which will open the project in your local VS Code using the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers): - -1. Start Docker Desktop (install it if not already installed) -1. Open the project: - [![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/azure-search-openai-demo) -1. In the VS Code window that opens, once the project files show up (this may take several minutes), open a terminal window. - -#### Local environment - -1. Install the required tools: - - - [Azure Developer CLI](https://aka.ms/azure-dev/install) - - [Python 3.9, 3.10, or 3.11](https://www.python.org/downloads/) - - **Important**: Python and the pip package manager must be in the path in Windows for the setup scripts to work. - - **Important**: Ensure you can run `python --version` from console. On Ubuntu, you might need to run `sudo apt install python-is-python3` to link `python` to `python3`. - - [Node.js 18+](https://nodejs.org/download/) - - [Git](https://git-scm.com/downloads) - - [Powershell 7+ (pwsh)](https://github.com/powershell/powershell) - For Windows users only. - - **Important**: Ensure you can run `pwsh.exe` from a PowerShell terminal. If this fails, you likely need to upgrade PowerShell. - -2. Create a new folder and switch to it in the terminal. -3. Run this command to download the project code: - - ```shell - azd init -t azure-search-openai-demo - ``` - - Note that this command will initialize a git repository, so you do not need to clone this repository. - -### Deploying - -Follow these steps to provision Azure resources and deploy the application code: - -1. Login to your Azure account: - - ```shell - azd auth login - ``` - -1. Create a new azd environment: - - ```shell - azd env new - ``` - - Enter a name that will be used for the resource group. - This will create a new folder in the `.azure` folder, and set it as the active environment for any calls to `azd` going forward. -1. (Optional) This is the point where you can customize the deployment by setting environment variables, in order to [use existing resources](docs/deploy_existing.md), [enable optional features (such as auth or vision)](docs/deploy_features.md), or [deploy to free tiers](docs/deploy_lowcost.md). -1. Run `azd up` - This will provision Azure resources and deploy this sample to those resources, including building the search index based on the files found in the `./data` folder. - - **Important**: Beware that the resources created by this command will incur immediate costs, primarily from the AI Search resource. These resources may accrue costs even if you interrupt the command before it is fully executed. You can run `azd down` or delete the resources manually to avoid unnecessary spending. - - You will be prompted to select two locations, one for the majority of resources and one for the OpenAI resource, which is currently a short list. That location list is based on the [OpenAI model availability table](https://learn.microsoft.com/azure/cognitive-services/openai/concepts/models#model-summary-table-and-region-availability) and may become outdated as availability changes. -1. After the application has been successfully deployed you will see a URL printed to the console. Click that URL to interact with the application in your browser. -It will look like the following: - -!['Output from running azd up'](docs/images/endpoint.png) - -> NOTE: It may take 5-10 minutes after you see 'SUCCESS' for the application to be fully deployed. If you see a "Python Developer" welcome screen or an error page, then wait a bit and refresh the page. See [guide on debugging App Service deployments](docs/appservice.md). - -### Deploying again - -If you've only changed the backend/frontend code in the `app` folder, then you don't need to re-provision the Azure resources. You can just run: - -```azd deploy``` - -If you've changed the infrastructure files (`infra` folder or `azure.yaml`), then you'll need to re-provision the Azure resources. You can do that by running: - -```azd up``` - -## Sharing environments - -To give someone else access to a completely deployed and existing environment, -either you or they can follow these steps: - -1. Install the [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) -1. Run `azd init -t azure-search-openai-demo` or clone this repository. -1. Run `azd env refresh -e {environment name}` - They will need the azd environment name, subscription ID, and location to run this command. You can find those values in your `.azure/{env name}/.env` file. This will populate their azd environment's `.env` file with all the settings needed to run the app locally. -1. Set the environment variable `AZURE_PRINCIPAL_ID` either in that `.env` file or in the active shell to their Azure ID, which they can get with `az ad signed-in-user show`. -1. Run `./scripts/roles.ps1` or `.scripts/roles.sh` to assign all of the necessary roles to the user. If they do not have the necessary permission to create roles in the subscription, then you may need to run this script for them. Once the script runs, they should be able to run the app locally. - -## Running locally - -You can only run locally **after** having successfully run the `azd up` command. If you haven't yet, follow the steps in [Azure deployment](#azure-deployment) above. - -1. Run `azd auth login` -2. Change dir to `app` -3. Run `./start.ps1` or `./start.sh` or run the "VS Code Task: Start App" to start the project locally. - -See more tips in [the local development guide](docs/localdev.md). - -## Using the app - -- In Azure: navigate to the Azure WebApp deployed by azd. The URL is printed out when azd completes (as "Endpoint"), or you can find it in the Azure portal. -- Running locally: navigate to 127.0.0.1:50505 - -Once in the web app: - -- Try different topics in chat or Q&A context. For chat, try follow up questions, clarifications, ask to simplify or elaborate on answer, etc. -- Explore citations and sources -- Click on "settings" to try different options, tweak prompts, etc. - -## Monitoring with Application Insights - -By default, deployed apps use Application Insights for the tracing of each request, along with the logging of errors. - -To see the performance data, go to the Application Insights resource in your resource group, click on the "Investigate -> Performance" blade and navigate to any HTTP request to see the timing data. -To inspect the performance of chat requests, use the "Drill into Samples" button to see end-to-end traces of all the API calls made for any chat request: - -![Tracing screenshot](docs/images/transaction-tracing.png) - -To see any exceptions and server errors, navigate to the "Investigate -> Failures" blade and use the filtering tools to locate a specific exception. You can see Python stack traces on the right-hand side. - -You can also see chart summaries on a dashboard by running the following command: - -```shell -azd monitor -``` - -## Customizing the UI and data - -Once you successfully deploy the app, you can start customizing it for your needs: changing the text, tweaking the prompts, and replacing the data. Consult the [app customization guide](docs/customization.md) as well as the [data ingestion guide](docs/data_ingestion.md) for more details. - -## Productionizing - -This sample is designed to be a starting point for your own production application, -but you should do a thorough review of the security and performance before deploying -to production. Read through our [productionizing guide](docs/productionizing.md) for more details. - -## Clean up - -To clean up all the resources created by this sample: - -1. Run `azd down` -2. When asked if you are sure you want to continue, enter `y` -3. When asked if you want to permanently delete the resources, enter `y` - -The resource group and all the resources will be deleted. - -## Troubleshooting - -Here are the most common failure scenarios and solutions: - -1. The subscription (`AZURE_SUBSCRIPTION_ID`) doesn't have access to the Azure OpenAI service. Please ensure `AZURE_SUBSCRIPTION_ID` matches the ID specified in the [OpenAI access request process](https://aka.ms/oai/access). - -1. You're attempting to create resources in regions not enabled for Azure OpenAI (e.g. East US 2 instead of East US), or where the model you're trying to use isn't enabled. See [this matrix of model availability](https://aka.ms/oai/models). - -1. You've exceeded a quota, most often number of resources per region. See [this article on quotas and limits](https://aka.ms/oai/quotas). - -1. You're getting "same resource name not allowed" conflicts. That's likely because you've run the sample multiple times and deleted the resources you've been creating each time, but are forgetting to purge them. Azure keeps resources for 48 hours unless you purge from soft delete. See [this article on purging resources](https://learn.microsoft.com/azure/cognitive-services/manage-resources?tabs=azure-portal#purge-a-deleted-resource). - -1. You see `CERTIFICATE_VERIFY_FAILED` when the `prepdocs.py` script runs. That's typically due to incorrect SSL certificates setup on your machine. Try the suggestions in this [StackOverflow answer](https://stackoverflow.com/questions/35569042/ssl-certificate-verify-failed-with-python3/43855394#43855394). - -1. After running `azd up` and visiting the website, you see a '404 Not Found' in the browser. Wait 10 minutes and try again, as it might be still starting up. Then try running `azd deploy` and wait again. If you still encounter errors with the deployed app, consult the [guide on debugging App Service deployments](docs/appservice.md). Please file an issue if the logs don't help you resolve the error. - -## Resources - -- [Additional documentation for this app](docs/README.md) -- [📖 Revolutionize your Enterprise Data with ChatGPT: Next-gen Apps w/ Azure OpenAI and AI Search](https://aka.ms/entgptsearchblog) -- [📖 Azure AI Search](https://learn.microsoft.com/azure/search/search-what-is-azure-search) -- [📖 Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/overview) -- [📖 Comparing Azure OpenAI and OpenAI](https://learn.microsoft.com/azure/cognitive-services/openai/overview#comparing-azure-openai-and-openai/) -- [📖 Access Control in Generative AI applications with Azure Cognitive Search](https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/access-control-in-generative-ai-applications-with-azure/ba-p/3956408) -- [📺 Quickly build and deploy OpenAI apps on Azure, infused with your own data](https://www.youtube.com/watch?v=j8i-OM5kwiY) -- [📺 AI Chat App Hack series](https://www.youtube.com/playlist?list=PL5lwDBUC0ag6_dGZst5m3G72ewfwXLcXV) - -### Getting help - -This is a sample built to demonstrate the capabilities of modern Generative AI apps and how they can be built in Azure. -For help with deploying this sample, please post in [GitHub Issues](/issues). If you're a Microsoft employee, you can also post in [our Teams channel](https://aka.ms/azai-python-help). - -This repository is supported by the maintainers, _not_ by Microsoft Support, -so please use the support mechanisms described above, and we will do our best to help you out. - -### Note - ->Note: The PDF documents used in this demo contain information generated using a language model (Azure OpenAI Service). The information contained in these documents is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft. +# ChatGPT-like app with your data using Azure OpenAI and Azure AI Search (Python) + +This solution's backend is written in Python. There are also [**JavaScript**](https://aka.ms/azai/js/code), [**.NET**](https://aka.ms/azai/net/code), and [**Java**](https://aka.ms/azai/java/code) samples based on this one. Learn more about [developing AI apps using Azure AI Services](https://aka.ms/azai). + +## Table of Contents + +- [Features](#features) +- [Azure account requirements](#azure-account-requirements) +- [Azure deployment](#azure-deployment) + - [Cost estimation](#cost-estimation) + - [Project setup](#project-setup) + - [GitHub Codespaces](#github-codespaces) + - [VS Code Dev Containers](#vs-code-dev-containers) + - [Local environment](#local-environment) + - [Deploying](#deploying) + - [Deploying again](#deploying-again) +- [Sharing environments](#sharing-environments) +- [Using the app](#using-the-app) +- [Running locally](#running-locally) +- [Monitoring with Application Insights](#monitoring-with-application-insights) +- [Customizing the UI and data](#customizing-the-ui-and-data) +- [Productionizing](#productionizing) +- [Clean up](#clean-up) +- [Troubleshooting](#troubleshooting) +- [Resources](#resources) + +[![Open in GitHub Codespaces](https://img.shields.io/static/v1?style=for-the-badge&label=GitHub+Codespaces&message=Open&color=brightgreen&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=599293758&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestUs2) +[![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/azure-search-openai-demo) + +This sample demonstrates a few approaches for creating ChatGPT-like experiences over your own data using the Retrieval Augmented Generation pattern. It uses Azure OpenAI Service to access a GPT model (gpt-35-turbo), and Azure AI Search for data indexing and retrieval. + +The repo includes sample data so it's ready to try end to end. In this sample application we use a fictitious company called Contoso Electronics, and the experience allows its employees to ask questions about the benefits, internal policies, as well as job descriptions and roles. + +![RAG Architecture](docs/images/appcomponents.png) + +## Features + +- Chat (multi-turn) and Q&A (single turn) interfaces +- Renders citations and thought process for each answer +- Includes settings directly in the UI to tweak the behavior and experiment with options +- Integrates Azure AI Search for indexing and retrieval of documents, with support for [many document formats](/docs/data_ingestion.md#supported-document-formats) as well as [integrated vectorization](/docs/data_ingestion.md#overview-of-integrated-vectorization) +- Optional usage of [GPT-4 with vision](/docs/gpt4vision.md) to reason over image-heavy documents +- Optional addition of [speech input/output](/docs/deploy_features.md#enabling-speech-inputoutput) for accessibility +- Optional automation of [user login and data access](/docs/login_and_acl.md) via Microsoft Entra +- Performance tracing and monitoring with Application Insights + +![Chat screen](docs/images/chatscreen.png) + +[📺 Watch a video overview of the app.](https://youtu.be/3acB0OWmLvM) + +## Azure account requirements + +**IMPORTANT:** In order to deploy and run this example, you'll need: + +- **Azure account**. If you're new to Azure, [get an Azure account for free](https://azure.microsoft.com/free/cognitive-search/) and you'll get some free Azure credits to get started. See [guide to deploying with the free trial](docs/deploy_lowcost.md). +- **Azure subscription with access enabled for the Azure OpenAI service**. You can request access with [this form](https://aka.ms/oaiapply). If your access request to Azure OpenAI service doesn't match the [acceptance criteria](https://learn.microsoft.com/legal/cognitive-services/openai/limited-access?context=%2Fazure%2Fcognitive-services%2Fopenai%2Fcontext%2Fcontext), you can use [OpenAI public API](https://platform.openai.com/docs/api-reference/introduction) instead. Learn [how to switch to an OpenAI instance](docs/deploy_existing.md#openaicom-openai). +- **Azure account permissions**: + - Your Azure account must have `Microsoft.Authorization/roleAssignments/write` permissions, such as [Role Based Access Control Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator-preview), [User Access Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#user-access-administrator), or [Owner](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#owner). If you don't have subscription-level permissions, you must be granted [RBAC](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#role-based-access-control-administrator-preview) for an existing resource group and [deploy to that existing group](docs/deploy_existing.md#resource-group). + - Your Azure account also needs `Microsoft.Resources/deployments/write` permissions on the subscription level. + +## Azure deployment + +### Cost estimation + +Pricing varies per region and usage, so it isn't possible to predict exact costs for your usage. +However, you can try the [Azure pricing calculator](https://azure.com/e/d18187516e9e421e925b3b311eec8aae) for the resources below. + +- Azure App Service: Basic Tier with 1 CPU core, 1.75 GB RAM. Pricing per hour. [Pricing](https://azure.microsoft.com/pricing/details/app-service/linux/) +- Azure OpenAI: Standard tier, GPT and Ada models. Pricing per 1K tokens used, and at least 1K tokens are used per question. [Pricing](https://azure.microsoft.com/pricing/details/cognitive-services/openai-service/) +- Azure AI Document Intelligence: SO (Standard) tier using pre-built layout. Pricing per document page, sample documents have 261 pages total. [Pricing](https://azure.microsoft.com/pricing/details/form-recognizer/) +- Azure AI Search: Standard tier, 1 replica, free level of semantic search. Pricing per hour. [Pricing](https://azure.microsoft.com/pricing/details/search/) +- Azure Blob Storage: Standard tier with ZRS (Zone-redundant storage). Pricing per storage and read operations. [Pricing](https://azure.microsoft.com/pricing/details/storage/blobs/) +- Azure Monitor: Pay-as-you-go tier. Costs based on data ingested. [Pricing](https://azure.microsoft.com/pricing/details/monitor/) + +To reduce costs, you can switch to free SKUs for various services, but those SKUs have limitations. +See this guide on [deploying with minimal costs](docs/deploy_lowcost.md) for more details. + +⚠️ To avoid unnecessary costs, remember to take down your app if it's no longer in use, +either by deleting the resource group in the Portal or running `azd down`. + +### Project setup + +You have a few options for setting up this project. +The easiest way to get started is GitHub Codespaces, since it will setup all the tools for you, +but you can also [set it up locally](#local-environment) if desired. + +#### GitHub Codespaces + +You can run this repo virtually by using GitHub Codespaces, which will open a web-based VS Code in your browser: + +[![Open in GitHub Codespaces](https://img.shields.io/static/v1?style=for-the-badge&label=GitHub+Codespaces&message=Open&color=brightgreen&logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=599293758&machine=standardLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestUs2) + +Once the codespace opens (this may take several minutes), open a terminal window. + +#### VS Code Dev Containers + +A related option is VS Code Dev Containers, which will open the project in your local VS Code using the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers): + +1. Start Docker Desktop (install it if not already installed) +1. Open the project: + [![Open in Dev Containers](https://img.shields.io/static/v1?style=for-the-badge&label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/azure-search-openai-demo) +1. In the VS Code window that opens, once the project files show up (this may take several minutes), open a terminal window. + +#### Local environment + +1. Install the required tools: + + - [Azure Developer CLI](https://aka.ms/azure-dev/install) + - [Python 3.9, 3.10, or 3.11](https://www.python.org/downloads/) + - **Important**: Python and the pip package manager must be in the path in Windows for the setup scripts to work. + - **Important**: Ensure you can run `python --version` from console. On Ubuntu, you might need to run `sudo apt install python-is-python3` to link `python` to `python3`. + - [Node.js 18+](https://nodejs.org/download/) + - [Git](https://git-scm.com/downloads) + - [Powershell 7+ (pwsh)](https://github.com/powershell/powershell) - For Windows users only. + - **Important**: Ensure you can run `pwsh.exe` from a PowerShell terminal. If this fails, you likely need to upgrade PowerShell. + +2. Create a new folder and switch to it in the terminal. +3. Run this command to download the project code: + + ```shell + azd init -t azure-search-openai-demo + ``` + + Note that this command will initialize a git repository, so you do not need to clone this repository. + +### Deploying + +Follow these steps to provision Azure resources and deploy the application code: + +1. Login to your Azure account: + + ```shell + azd auth login + ``` + +1. Create a new azd environment: + + ```shell + azd env new + ``` + + Enter a name that will be used for the resource group. + This will create a new folder in the `.azure` folder, and set it as the active environment for any calls to `azd` going forward. +1. (Optional) This is the point where you can customize the deployment by setting environment variables, in order to [use existing resources](docs/deploy_existing.md), [enable optional features (such as auth or vision)](docs/deploy_features.md), or [deploy to free tiers](docs/deploy_lowcost.md). +1. Run `azd up` - This will provision Azure resources and deploy this sample to those resources, including building the search index based on the files found in the `./data` folder. + - **Important**: Beware that the resources created by this command will incur immediate costs, primarily from the AI Search resource. These resources may accrue costs even if you interrupt the command before it is fully executed. You can run `azd down` or delete the resources manually to avoid unnecessary spending. + - You will be prompted to select two locations, one for the majority of resources and one for the OpenAI resource, which is currently a short list. That location list is based on the [OpenAI model availability table](https://learn.microsoft.com/azure/cognitive-services/openai/concepts/models#model-summary-table-and-region-availability) and may become outdated as availability changes. +1. After the application has been successfully deployed you will see a URL printed to the console. Click that URL to interact with the application in your browser. +It will look like the following: + +!['Output from running azd up'](docs/images/endpoint.png) + +> NOTE: It may take 5-10 minutes after you see 'SUCCESS' for the application to be fully deployed. If you see a "Python Developer" welcome screen or an error page, then wait a bit and refresh the page. See [guide on debugging App Service deployments](docs/appservice.md). + +### Deploying again + +If you've only changed the backend/frontend code in the `app` folder, then you don't need to re-provision the Azure resources. You can just run: + +```azd deploy``` + +If you've changed the infrastructure files (`infra` folder or `azure.yaml`), then you'll need to re-provision the Azure resources. You can do that by running: + +```azd up``` + +## Sharing environments + +To give someone else access to a completely deployed and existing environment, +either you or they can follow these steps: + +1. Install the [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) +1. Run `azd init -t azure-search-openai-demo` or clone this repository. +1. Run `azd env refresh -e {environment name}` + They will need the azd environment name, subscription ID, and location to run this command. You can find those values in your `.azure/{env name}/.env` file. This will populate their azd environment's `.env` file with all the settings needed to run the app locally. +1. Set the environment variable `AZURE_PRINCIPAL_ID` either in that `.env` file or in the active shell to their Azure ID, which they can get with `az ad signed-in-user show`. +1. Run `./scripts/roles.ps1` or `.scripts/roles.sh` to assign all of the necessary roles to the user. If they do not have the necessary permission to create roles in the subscription, then you may need to run this script for them. Once the script runs, they should be able to run the app locally. + +## Running locally + +You can only run locally **after** having successfully run the `azd up` command. If you haven't yet, follow the steps in [Azure deployment](#azure-deployment) above. + +1. Run `azd auth login` +2. Change dir to `app` +3. Run `./start.ps1` or `./start.sh` or run the "VS Code Task: Start App" to start the project locally. + +See more tips in [the local development guide](docs/localdev.md). + +## Using the app + +- In Azure: navigate to the Azure WebApp deployed by azd. The URL is printed out when azd completes (as "Endpoint"), or you can find it in the Azure portal. +- Running locally: navigate to 127.0.0.1:50505 + +Once in the web app: + +- Try different topics in chat or Q&A context. For chat, try follow up questions, clarifications, ask to simplify or elaborate on answer, etc. +- Explore citations and sources +- Click on "settings" to try different options, tweak prompts, etc. + +## Monitoring with Application Insights + +By default, deployed apps use Application Insights for the tracing of each request, along with the logging of errors. + +To see the performance data, go to the Application Insights resource in your resource group, click on the "Investigate -> Performance" blade and navigate to any HTTP request to see the timing data. +To inspect the performance of chat requests, use the "Drill into Samples" button to see end-to-end traces of all the API calls made for any chat request: + +![Tracing screenshot](docs/images/transaction-tracing.png) + +To see any exceptions and server errors, navigate to the "Investigate -> Failures" blade and use the filtering tools to locate a specific exception. You can see Python stack traces on the right-hand side. + +You can also see chart summaries on a dashboard by running the following command: + +```shell +azd monitor +``` + +## Customizing the UI and data + +Once you successfully deploy the app, you can start customizing it for your needs: changing the text, tweaking the prompts, and replacing the data. Consult the [app customization guide](docs/customization.md) as well as the [data ingestion guide](docs/data_ingestion.md) for more details. + +## Productionizing + +This sample is designed to be a starting point for your own production application, +but you should do a thorough review of the security and performance before deploying +to production. Read through our [productionizing guide](docs/productionizing.md) for more details. + +## Clean up + +To clean up all the resources created by this sample: + +1. Run `azd down` +2. When asked if you are sure you want to continue, enter `y` +3. When asked if you want to permanently delete the resources, enter `y` + +The resource group and all the resources will be deleted. + +## Troubleshooting + +Here are the most common failure scenarios and solutions: + +1. The subscription (`AZURE_SUBSCRIPTION_ID`) doesn't have access to the Azure OpenAI service. Please ensure `AZURE_SUBSCRIPTION_ID` matches the ID specified in the [OpenAI access request process](https://aka.ms/oai/access). + +1. You're attempting to create resources in regions not enabled for Azure OpenAI (e.g. East US 2 instead of East US), or where the model you're trying to use isn't enabled. See [this matrix of model availability](https://aka.ms/oai/models). + +1. You've exceeded a quota, most often number of resources per region. See [this article on quotas and limits](https://aka.ms/oai/quotas). + +1. You're getting "same resource name not allowed" conflicts. That's likely because you've run the sample multiple times and deleted the resources you've been creating each time, but are forgetting to purge them. Azure keeps resources for 48 hours unless you purge from soft delete. See [this article on purging resources](https://learn.microsoft.com/azure/cognitive-services/manage-resources?tabs=azure-portal#purge-a-deleted-resource). + +1. You see `CERTIFICATE_VERIFY_FAILED` when the `prepdocs.py` script runs. That's typically due to incorrect SSL certificates setup on your machine. Try the suggestions in this [StackOverflow answer](https://stackoverflow.com/questions/35569042/ssl-certificate-verify-failed-with-python3/43855394#43855394). + +1. After running `azd up` and visiting the website, you see a '404 Not Found' in the browser. Wait 10 minutes and try again, as it might be still starting up. Then try running `azd deploy` and wait again. If you still encounter errors with the deployed app, consult the [guide on debugging App Service deployments](docs/appservice.md). Please file an issue if the logs don't help you resolve the error. + +## Resources + +- [Additional documentation for this app](docs/README.md) +- [📖 Revolutionize your Enterprise Data with ChatGPT: Next-gen Apps w/ Azure OpenAI and AI Search](https://aka.ms/entgptsearchblog) +- [📖 Azure AI Search](https://learn.microsoft.com/azure/search/search-what-is-azure-search) +- [📖 Azure OpenAI Service](https://learn.microsoft.com/azure/cognitive-services/openai/overview) +- [📖 Comparing Azure OpenAI and OpenAI](https://learn.microsoft.com/azure/cognitive-services/openai/overview#comparing-azure-openai-and-openai/) +- [📖 Access Control in Generative AI applications with Azure Cognitive Search](https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/access-control-in-generative-ai-applications-with-azure/ba-p/3956408) +- [📺 Quickly build and deploy OpenAI apps on Azure, infused with your own data](https://www.youtube.com/watch?v=j8i-OM5kwiY) +- [📺 AI Chat App Hack series](https://www.youtube.com/playlist?list=PL5lwDBUC0ag6_dGZst5m3G72ewfwXLcXV) + +### Getting help + +This is a sample built to demonstrate the capabilities of modern Generative AI apps and how they can be built in Azure. +For help with deploying this sample, please post in [GitHub Issues](/issues). If you're a Microsoft employee, you can also post in [our Teams channel](https://aka.ms/azai-python-help). + +This repository is supported by the maintainers, _not_ by Microsoft Support, +so please use the support mechanisms described above, and we will do our best to help you out. + +### Note + +>Note: The PDF documents used in this demo contain information generated using a language model (Azure OpenAI Service). The information contained in these documents is only for demonstration purposes and does not reflect the opinions or beliefs of Microsoft. Microsoft makes no representations or warranties of any kind, express or implied, about the completeness, accuracy, reliability, suitability or availability with respect to the information contained in this document. All rights reserved to Microsoft. diff --git a/app/backend/app.py b/app/backend/app.py index 1d1751359e..3e9a0886c3 100644 --- a/app/backend/app.py +++ b/app/backend/app.py @@ -1,678 +1,678 @@ -import dataclasses -import io -import json -import logging -import mimetypes -import os -import time -from pathlib import Path -from typing import Any, AsyncGenerator, Dict, Union, cast - -from azure.cognitiveservices.speech import ( - ResultReason, - SpeechConfig, - SpeechSynthesisOutputFormat, - SpeechSynthesisResult, - SpeechSynthesizer, -) -from azure.core.exceptions import ResourceNotFoundError -from azure.identity.aio import DefaultAzureCredential, get_bearer_token_provider -from azure.monitor.opentelemetry import configure_azure_monitor -from azure.search.documents.aio import SearchClient -from azure.search.documents.indexes.aio import SearchIndexClient -from azure.storage.blob.aio import ContainerClient -from azure.storage.blob.aio import StorageStreamDownloader as BlobDownloader -from azure.storage.filedatalake.aio import FileSystemClient -from azure.storage.filedatalake.aio import StorageStreamDownloader as DatalakeDownloader -from openai import AsyncAzureOpenAI, AsyncOpenAI -from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor -from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware -from opentelemetry.instrumentation.httpx import ( - HTTPXClientInstrumentor, -) -from opentelemetry.instrumentation.openai import OpenAIInstrumentor -from quart import ( - Blueprint, - Quart, - abort, - current_app, - jsonify, - make_response, - request, - send_file, - send_from_directory, -) -from quart_cors import cors - -from approaches.approach import Approach -from approaches.chatreadretrieveread import ChatReadRetrieveReadApproach -from approaches.chatreadretrievereadvision import ChatReadRetrieveReadVisionApproach -from approaches.retrievethenread import RetrieveThenReadApproach -from approaches.retrievethenreadvision import RetrieveThenReadVisionApproach -from config import ( - CONFIG_ASK_APPROACH, - CONFIG_ASK_VISION_APPROACH, - CONFIG_AUTH_CLIENT, - CONFIG_BLOB_CONTAINER_CLIENT, - CONFIG_CHAT_APPROACH, - CONFIG_CHAT_VISION_APPROACH, - CONFIG_CREDENTIAL, - CONFIG_GPT4V_DEPLOYED, - CONFIG_INGESTER, - CONFIG_OPENAI_CLIENT, - CONFIG_SEARCH_CLIENT, - CONFIG_SEMANTIC_RANKER_DEPLOYED, - CONFIG_SPEECH_INPUT_ENABLED, - CONFIG_SPEECH_OUTPUT_AZURE_ENABLED, - CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED, - CONFIG_SPEECH_SERVICE_ID, - CONFIG_SPEECH_SERVICE_LOCATION, - CONFIG_SPEECH_SERVICE_TOKEN, - CONFIG_SPEECH_SERVICE_VOICE, - CONFIG_USER_BLOB_CONTAINER_CLIENT, - CONFIG_USER_UPLOAD_ENABLED, - CONFIG_VECTOR_SEARCH_ENABLED, -) -from core.authentication import AuthenticationHelper -from decorators import authenticated, authenticated_path -from error import error_dict, error_response -from prepdocs import ( - clean_key_if_exists, - setup_embeddings_service, - setup_file_processors, - setup_search_info, -) -from prepdocslib.filestrategy import UploadUserFileStrategy -from prepdocslib.listfilestrategy import File - -bp = Blueprint("routes", __name__, static_folder="static") -# Fix Windows registry issue with mimetypes -mimetypes.add_type("application/javascript", ".js") -mimetypes.add_type("text/css", ".css") - - -@bp.route("/") -async def index(): - return await bp.send_static_file("index.html") - - -# Empty page is recommended for login redirect to work. -# See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md#redirecturi-considerations for more information -@bp.route("/redirect") -async def redirect(): - return "" - - -@bp.route("/favicon.ico") -async def favicon(): - return await bp.send_static_file("favicon.ico") - - -@bp.route("/assets/") -async def assets(path): - return await send_from_directory(Path(__file__).resolve().parent / "static" / "assets", path) - - -@bp.route("/content/") -@authenticated_path -async def content_file(path: str, auth_claims: Dict[str, Any]): - """ - Serve content files from blob storage from within the app to keep the example self-contained. - *** NOTE *** if you are using app services authentication, this route will return unauthorized to all users that are not logged in - if AZURE_ENFORCE_ACCESS_CONTROL is not set or false, logged in users can access all files regardless of access control - if AZURE_ENFORCE_ACCESS_CONTROL is set to true, logged in users can only access files they have access to - This is also slow and memory hungry. - """ - # Remove page number from path, filename-1.txt -> filename.txt - # This shouldn't typically be necessary as browsers don't send hash fragments to servers - if path.find("#page=") > 0: - path_parts = path.rsplit("#page=", 1) - path = path_parts[0] - logging.info("Opening file %s", path) - blob_container_client: ContainerClient = current_app.config[CONFIG_BLOB_CONTAINER_CLIENT] - blob: Union[BlobDownloader, DatalakeDownloader] - try: - blob = await blob_container_client.get_blob_client(path).download_blob() - except ResourceNotFoundError: - logging.info("Path not found in general Blob container: %s", path) - if current_app.config[CONFIG_USER_UPLOAD_ENABLED]: - try: - user_oid = auth_claims["oid"] - user_blob_container_client = current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT] - user_directory_client: FileSystemClient = user_blob_container_client.get_directory_client(user_oid) - file_client = user_directory_client.get_file_client(path) - blob = await file_client.download_file() - except ResourceNotFoundError: - logging.exception("Path not found in DataLake: %s", path) - abort(404) - else: - abort(404) - if not blob.properties or not blob.properties.has_key("content_settings"): - abort(404) - mime_type = blob.properties["content_settings"]["content_type"] - if mime_type == "application/octet-stream": - mime_type = mimetypes.guess_type(path)[0] or "application/octet-stream" - blob_file = io.BytesIO() - await blob.readinto(blob_file) - blob_file.seek(0) - return await send_file(blob_file, mimetype=mime_type, as_attachment=False, attachment_filename=path) - - -@bp.route("/ask", methods=["POST"]) -@authenticated -async def ask(auth_claims: Dict[str, Any]): - if not request.is_json: - return jsonify({"error": "request must be json"}), 415 - request_json = await request.get_json() - context = request_json.get("context", {}) - context["auth_claims"] = auth_claims - try: - use_gpt4v = context.get("overrides", {}).get("use_gpt4v", False) - approach: Approach - if use_gpt4v and CONFIG_ASK_VISION_APPROACH in current_app.config: - approach = cast(Approach, current_app.config[CONFIG_ASK_VISION_APPROACH]) - else: - approach = cast(Approach, current_app.config[CONFIG_ASK_APPROACH]) - r = await approach.run( - request_json["messages"], context=context, session_state=request_json.get("session_state") - ) - return jsonify(r) - except Exception as error: - return error_response(error, "/ask") - - -class JSONEncoder(json.JSONEncoder): - def default(self, o): - if dataclasses.is_dataclass(o): - return dataclasses.asdict(o) - return super().default(o) - - -async def format_as_ndjson(r: AsyncGenerator[dict, None]) -> AsyncGenerator[str, None]: - try: - async for event in r: - yield json.dumps(event, ensure_ascii=False, cls=JSONEncoder) + "\n" - except Exception as error: - logging.exception("Exception while generating response stream: %s", error) - yield json.dumps(error_dict(error)) - - -@bp.route("/chat", methods=["POST"]) -@authenticated -async def chat(auth_claims: Dict[str, Any]): - if not request.is_json: - return jsonify({"error": "request must be json"}), 415 - request_json = await request.get_json() - context = request_json.get("context", {}) - context["auth_claims"] = auth_claims - try: - use_gpt4v = context.get("overrides", {}).get("use_gpt4v", False) - approach: Approach - if use_gpt4v and CONFIG_CHAT_VISION_APPROACH in current_app.config: - approach = cast(Approach, current_app.config[CONFIG_CHAT_VISION_APPROACH]) - else: - approach = cast(Approach, current_app.config[CONFIG_CHAT_APPROACH]) - - result = await approach.run( - request_json["messages"], - context=context, - session_state=request_json.get("session_state"), - ) - return jsonify(result) - except Exception as error: - return error_response(error, "/chat") - - -@bp.route("/chat/stream", methods=["POST"]) -@authenticated -async def chat_stream(auth_claims: Dict[str, Any]): - if not request.is_json: - return jsonify({"error": "request must be json"}), 415 - request_json = await request.get_json() - context = request_json.get("context", {}) - context["auth_claims"] = auth_claims - try: - use_gpt4v = context.get("overrides", {}).get("use_gpt4v", False) - approach: Approach - if use_gpt4v and CONFIG_CHAT_VISION_APPROACH in current_app.config: - approach = cast(Approach, current_app.config[CONFIG_CHAT_VISION_APPROACH]) - else: - approach = cast(Approach, current_app.config[CONFIG_CHAT_APPROACH]) - - result = await approach.run_stream( - request_json["messages"], - context=context, - session_state=request_json.get("session_state"), - ) - response = await make_response(format_as_ndjson(result)) - response.timeout = None # type: ignore - response.mimetype = "application/json-lines" - return response - except Exception as error: - return error_response(error, "/chat") - - -# Send MSAL.js settings to the client UI -@bp.route("/auth_setup", methods=["GET"]) -def auth_setup(): - auth_helper = current_app.config[CONFIG_AUTH_CLIENT] - return jsonify(auth_helper.get_auth_setup_for_client()) - - -@bp.route("/config", methods=["GET"]) -def config(): - return jsonify( - { - "showGPT4VOptions": current_app.config[CONFIG_GPT4V_DEPLOYED], - "showSemanticRankerOption": current_app.config[CONFIG_SEMANTIC_RANKER_DEPLOYED], - "showVectorOption": current_app.config[CONFIG_VECTOR_SEARCH_ENABLED], - "showUserUpload": current_app.config[CONFIG_USER_UPLOAD_ENABLED], - "showSpeechInput": current_app.config[CONFIG_SPEECH_INPUT_ENABLED], - "showSpeechOutputBrowser": current_app.config[CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED], - "showSpeechOutputAzure": current_app.config[CONFIG_SPEECH_OUTPUT_AZURE_ENABLED], - } - ) - - -@bp.route("/speech", methods=["POST"]) -async def speech(): - if not request.is_json: - return jsonify({"error": "request must be json"}), 415 - - speech_token = current_app.config.get(CONFIG_SPEECH_SERVICE_TOKEN) - if speech_token is None or speech_token.expires_on < time.time() + 60: - speech_token = await current_app.config[CONFIG_CREDENTIAL].get_token( - "https://cognitiveservices.azure.com/.default" - ) - current_app.config[CONFIG_SPEECH_SERVICE_TOKEN] = speech_token - - request_json = await request.get_json() - text = request_json["text"] - try: - # Construct a token as described in documentation: - # https://learn.microsoft.com/azure/ai-services/speech-service/how-to-configure-azure-ad-auth?pivots=programming-language-python - auth_token = ( - "aad#" - + current_app.config[CONFIG_SPEECH_SERVICE_ID] - + "#" - + current_app.config[CONFIG_SPEECH_SERVICE_TOKEN].token - ) - speech_config = SpeechConfig(auth_token=auth_token, region=current_app.config[CONFIG_SPEECH_SERVICE_LOCATION]) - speech_config.speech_synthesis_voice_name = current_app.config[CONFIG_SPEECH_SERVICE_VOICE] - speech_config.speech_synthesis_output_format = SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3 - synthesizer = SpeechSynthesizer(speech_config=speech_config, audio_config=None) - result: SpeechSynthesisResult = synthesizer.speak_text_async(text).get() - if result.reason == ResultReason.SynthesizingAudioCompleted: - return result.audio_data, 200, {"Content-Type": "audio/mp3"} - elif result.reason == ResultReason.Canceled: - cancellation_details = result.cancellation_details - current_app.logger.error( - "Speech synthesis canceled: %s %s", cancellation_details.reason, cancellation_details.error_details - ) - raise Exception("Speech synthesis canceled. Check logs for details.") - else: - current_app.logger.error("Unexpected result reason: %s", result.reason) - raise Exception("Speech synthesis failed. Check logs for details.") - except Exception as e: - logging.exception("Exception in /speech") - return jsonify({"error": str(e)}), 500 - - -@bp.post("/upload") -@authenticated -async def upload(auth_claims: dict[str, Any]): - request_files = await request.files - if "file" not in request_files: - # If no files were included in the request, return an error response - return jsonify({"message": "No file part in the request", "status": "failed"}), 400 - - user_oid = auth_claims["oid"] - file = request_files.getlist("file")[0] - user_blob_container_client: FileSystemClient = current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT] - user_directory_client = user_blob_container_client.get_directory_client(user_oid) - try: - await user_directory_client.get_directory_properties() - except ResourceNotFoundError: - current_app.logger.info("Creating directory for user %s", user_oid) - await user_directory_client.create_directory() - await user_directory_client.set_access_control(owner=user_oid) - file_client = user_directory_client.get_file_client(file.filename) - file_io = file - file_io.name = file.filename - file_io = io.BufferedReader(file_io) - await file_client.upload_data(file_io, overwrite=True, metadata={"UploadedBy": user_oid}) - file_io.seek(0) - ingester: UploadUserFileStrategy = current_app.config[CONFIG_INGESTER] - await ingester.add_file(File(content=file_io, acls={"oids": [user_oid]}, url=file_client.url)) - return jsonify({"message": "File uploaded successfully"}), 200 - - -@bp.post("/delete_uploaded") -@authenticated -async def delete_uploaded(auth_claims: dict[str, Any]): - request_json = await request.get_json() - filename = request_json.get("filename") - user_oid = auth_claims["oid"] - user_blob_container_client: FileSystemClient = current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT] - user_directory_client = user_blob_container_client.get_directory_client(user_oid) - file_client = user_directory_client.get_file_client(filename) - await file_client.delete_file() - ingester = current_app.config[CONFIG_INGESTER] - await ingester.remove_file(filename, user_oid) - return jsonify({"message": f"File {filename} deleted successfully"}), 200 - - -@bp.get("/list_uploaded") -@authenticated -async def list_uploaded(auth_claims: dict[str, Any]): - user_oid = auth_claims["oid"] - user_blob_container_client: FileSystemClient = current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT] - files = [] - try: - all_paths = user_blob_container_client.get_paths(path=user_oid) - async for path in all_paths: - files.append(path.name.split("/", 1)[1]) - except ResourceNotFoundError as error: - if error.status_code != 404: - current_app.logger.exception("Error listing uploaded files", error) - return jsonify(files), 200 - - -@bp.before_app_serving -async def setup_clients(): - # Replace these with your own values, either in environment variables or directly here - AZURE_STORAGE_ACCOUNT = os.environ["AZURE_STORAGE_ACCOUNT"] - AZURE_STORAGE_CONTAINER = os.environ["AZURE_STORAGE_CONTAINER"] - AZURE_USERSTORAGE_ACCOUNT = os.environ.get("AZURE_USERSTORAGE_ACCOUNT") - AZURE_USERSTORAGE_CONTAINER = os.environ.get("AZURE_USERSTORAGE_CONTAINER") - AZURE_SEARCH_SERVICE = os.environ["AZURE_SEARCH_SERVICE"] - AZURE_SEARCH_INDEX = os.environ["AZURE_SEARCH_INDEX"] - # Shared by all OpenAI deployments - OPENAI_HOST = os.getenv("OPENAI_HOST", "azure") - OPENAI_CHATGPT_MODEL = os.environ["AZURE_OPENAI_CHATGPT_MODEL"] - OPENAI_EMB_MODEL = os.getenv("AZURE_OPENAI_EMB_MODEL_NAME", "text-embedding-ada-002") - OPENAI_EMB_DIMENSIONS = int(os.getenv("AZURE_OPENAI_EMB_DIMENSIONS", 1536)) - # Used with Azure OpenAI deployments - AZURE_OPENAI_SERVICE = os.getenv("AZURE_OPENAI_SERVICE") - AZURE_OPENAI_GPT4V_DEPLOYMENT = os.environ.get("AZURE_OPENAI_GPT4V_DEPLOYMENT") - AZURE_OPENAI_GPT4V_MODEL = os.environ.get("AZURE_OPENAI_GPT4V_MODEL") - AZURE_OPENAI_CHATGPT_DEPLOYMENT = ( - os.getenv("AZURE_OPENAI_CHATGPT_DEPLOYMENT") if OPENAI_HOST.startswith("azure") else None - ) - AZURE_OPENAI_EMB_DEPLOYMENT = os.getenv("AZURE_OPENAI_EMB_DEPLOYMENT") if OPENAI_HOST.startswith("azure") else None - AZURE_VISION_ENDPOINT = os.getenv("AZURE_VISION_ENDPOINT", "") - # Used only with non-Azure OpenAI deployments - OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") - OPENAI_ORGANIZATION = os.getenv("OPENAI_ORGANIZATION") - - AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID") - AZURE_USE_AUTHENTICATION = os.getenv("AZURE_USE_AUTHENTICATION", "").lower() == "true" - AZURE_ENFORCE_ACCESS_CONTROL = os.getenv("AZURE_ENFORCE_ACCESS_CONTROL", "").lower() == "true" - AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS = os.getenv("AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS", "").lower() == "true" - AZURE_ENABLE_UNAUTHENTICATED_ACCESS = os.getenv("AZURE_ENABLE_UNAUTHENTICATED_ACCESS", "").lower() == "true" - AZURE_SERVER_APP_ID = os.getenv("AZURE_SERVER_APP_ID") - AZURE_SERVER_APP_SECRET = os.getenv("AZURE_SERVER_APP_SECRET") - AZURE_CLIENT_APP_ID = os.getenv("AZURE_CLIENT_APP_ID") - AZURE_AUTH_TENANT_ID = os.getenv("AZURE_AUTH_TENANT_ID", AZURE_TENANT_ID) - - KB_FIELDS_CONTENT = os.getenv("KB_FIELDS_CONTENT", "content") - KB_FIELDS_SOURCEPAGE = os.getenv("KB_FIELDS_SOURCEPAGE", "sourcepage") - - AZURE_SEARCH_QUERY_LANGUAGE = os.getenv("AZURE_SEARCH_QUERY_LANGUAGE", "en-us") - AZURE_SEARCH_QUERY_SPELLER = os.getenv("AZURE_SEARCH_QUERY_SPELLER", "lexicon") - AZURE_SEARCH_SEMANTIC_RANKER = os.getenv("AZURE_SEARCH_SEMANTIC_RANKER", "free").lower() - - AZURE_SPEECH_SERVICE_ID = os.getenv("AZURE_SPEECH_SERVICE_ID") - AZURE_SPEECH_SERVICE_LOCATION = os.getenv("AZURE_SPEECH_SERVICE_LOCATION") - AZURE_SPEECH_VOICE = os.getenv("AZURE_SPEECH_VOICE", "en-US-AndrewMultilingualNeural") - - USE_GPT4V = os.getenv("USE_GPT4V", "").lower() == "true" - USE_USER_UPLOAD = os.getenv("USE_USER_UPLOAD", "").lower() == "true" - USE_SPEECH_INPUT_BROWSER = os.getenv("USE_SPEECH_INPUT_BROWSER", "").lower() == "true" - USE_SPEECH_OUTPUT_BROWSER = os.getenv("USE_SPEECH_OUTPUT_BROWSER", "").lower() == "true" - USE_SPEECH_OUTPUT_AZURE = os.getenv("USE_SPEECH_OUTPUT_AZURE", "").lower() == "true" - - # Use the current user identity to authenticate with Azure OpenAI, AI Search and Blob Storage (no secrets needed, - # just use 'az login' locally, and managed identity when deployed on Azure). If you need to use keys, use separate AzureKeyCredential instances with the - # keys for each service - # 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) - azure_credential = DefaultAzureCredential(exclude_shared_token_cache_credential=True) - - # Set up clients for AI Search and Storage - search_client = SearchClient( - endpoint=f"https://{AZURE_SEARCH_SERVICE}.search.windows.net", - index_name=AZURE_SEARCH_INDEX, - credential=azure_credential, - ) - - blob_container_client = ContainerClient( - f"https://{AZURE_STORAGE_ACCOUNT}.blob.core.windows.net", AZURE_STORAGE_CONTAINER, credential=azure_credential - ) - - # Set up authentication helper - search_index = None - if AZURE_USE_AUTHENTICATION: - search_index_client = SearchIndexClient( - endpoint=f"https://{AZURE_SEARCH_SERVICE}.search.windows.net", - credential=azure_credential, - ) - search_index = await search_index_client.get_index(AZURE_SEARCH_INDEX) - await search_index_client.close() - auth_helper = AuthenticationHelper( - search_index=search_index, - use_authentication=AZURE_USE_AUTHENTICATION, - server_app_id=AZURE_SERVER_APP_ID, - server_app_secret=AZURE_SERVER_APP_SECRET, - client_app_id=AZURE_CLIENT_APP_ID, - tenant_id=AZURE_AUTH_TENANT_ID, - require_access_control=AZURE_ENFORCE_ACCESS_CONTROL, - enable_global_documents=AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS, - enable_unauthenticated_access=AZURE_ENABLE_UNAUTHENTICATED_ACCESS, - ) - - if USE_USER_UPLOAD: - current_app.logger.info("USE_USER_UPLOAD is true, setting up user upload feature") - if not AZURE_USERSTORAGE_ACCOUNT or not AZURE_USERSTORAGE_CONTAINER: - raise ValueError( - "AZURE_USERSTORAGE_ACCOUNT and AZURE_USERSTORAGE_CONTAINER must be set when USE_USER_UPLOAD is true" - ) - user_blob_container_client = FileSystemClient( - f"https://{AZURE_USERSTORAGE_ACCOUNT}.dfs.core.windows.net", - AZURE_USERSTORAGE_CONTAINER, - credential=azure_credential, - ) - current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT] = user_blob_container_client - - # Set up ingester - file_processors = setup_file_processors( - azure_credential=azure_credential, - document_intelligence_service=os.getenv("AZURE_DOCUMENTINTELLIGENCE_SERVICE"), - local_pdf_parser=os.getenv("USE_LOCAL_PDF_PARSER", "").lower() == "true", - local_html_parser=os.getenv("USE_LOCAL_HTML_PARSER", "").lower() == "true", - search_images=USE_GPT4V, - ) - search_info = await setup_search_info( - search_service=AZURE_SEARCH_SERVICE, index_name=AZURE_SEARCH_INDEX, azure_credential=azure_credential - ) - text_embeddings_service = setup_embeddings_service( - azure_credential=azure_credential, - openai_host=OPENAI_HOST, - openai_model_name=OPENAI_EMB_MODEL, - openai_service=AZURE_OPENAI_SERVICE, - openai_deployment=AZURE_OPENAI_EMB_DEPLOYMENT, - openai_dimensions=OPENAI_EMB_DIMENSIONS, - openai_key=clean_key_if_exists(OPENAI_API_KEY), - openai_org=OPENAI_ORGANIZATION, - disable_vectors=os.getenv("USE_VECTORS", "").lower() == "false", - ) - ingester = UploadUserFileStrategy( - search_info=search_info, embeddings=text_embeddings_service, file_processors=file_processors - ) - current_app.config[CONFIG_INGESTER] = ingester - - # Used by the OpenAI SDK - openai_client: AsyncOpenAI - - if USE_SPEECH_OUTPUT_AZURE: - if not AZURE_SPEECH_SERVICE_ID or AZURE_SPEECH_SERVICE_ID == "": - raise ValueError("Azure speech resource not configured correctly, missing AZURE_SPEECH_SERVICE_ID") - if not AZURE_SPEECH_SERVICE_LOCATION or AZURE_SPEECH_SERVICE_LOCATION == "": - raise ValueError("Azure speech resource not configured correctly, missing AZURE_SPEECH_SERVICE_LOCATION") - current_app.config[CONFIG_SPEECH_SERVICE_ID] = AZURE_SPEECH_SERVICE_ID - current_app.config[CONFIG_SPEECH_SERVICE_LOCATION] = AZURE_SPEECH_SERVICE_LOCATION - current_app.config[CONFIG_SPEECH_SERVICE_VOICE] = AZURE_SPEECH_VOICE - # Wait until token is needed to fetch for the first time - current_app.config[CONFIG_SPEECH_SERVICE_TOKEN] = None - current_app.config[CONFIG_CREDENTIAL] = azure_credential - - if OPENAI_HOST.startswith("azure"): - api_version = os.getenv("AZURE_OPENAI_API_VERSION") or "2024-03-01-preview" - - if OPENAI_HOST == "azure_custom": - endpoint = os.environ["AZURE_OPENAI_CUSTOM_URL"] - else: - endpoint = f"https://{AZURE_OPENAI_SERVICE}.openai.azure.com" - - if api_key := os.getenv("AZURE_OPENAI_API_KEY"): - openai_client = AsyncAzureOpenAI(api_version=api_version, azure_endpoint=endpoint, api_key=api_key) - else: - token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default") - openai_client = AsyncAzureOpenAI( - api_version=api_version, - azure_endpoint=endpoint, - azure_ad_token_provider=token_provider, - ) - elif OPENAI_HOST == "local": - openai_client = AsyncOpenAI( - base_url=os.environ["OPENAI_BASE_URL"], - api_key="no-key-required", - ) - else: - openai_client = AsyncOpenAI( - api_key=OPENAI_API_KEY, - organization=OPENAI_ORGANIZATION, - ) - - current_app.config[CONFIG_OPENAI_CLIENT] = openai_client - current_app.config[CONFIG_SEARCH_CLIENT] = search_client - current_app.config[CONFIG_BLOB_CONTAINER_CLIENT] = blob_container_client - current_app.config[CONFIG_AUTH_CLIENT] = auth_helper - - current_app.config[CONFIG_GPT4V_DEPLOYED] = bool(USE_GPT4V) - current_app.config[CONFIG_SEMANTIC_RANKER_DEPLOYED] = AZURE_SEARCH_SEMANTIC_RANKER != "disabled" - current_app.config[CONFIG_VECTOR_SEARCH_ENABLED] = os.getenv("USE_VECTORS", "").lower() != "false" - current_app.config[CONFIG_USER_UPLOAD_ENABLED] = bool(USE_USER_UPLOAD) - current_app.config[CONFIG_SPEECH_INPUT_ENABLED] = USE_SPEECH_INPUT_BROWSER - current_app.config[CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED] = USE_SPEECH_OUTPUT_BROWSER - current_app.config[CONFIG_SPEECH_OUTPUT_AZURE_ENABLED] = USE_SPEECH_OUTPUT_AZURE - - # Various approaches to integrate GPT and external knowledge, most applications will use a single one of these patterns - # or some derivative, here we include several for exploration purposes - current_app.config[CONFIG_ASK_APPROACH] = RetrieveThenReadApproach( - search_client=search_client, - openai_client=openai_client, - auth_helper=auth_helper, - chatgpt_model=OPENAI_CHATGPT_MODEL, - chatgpt_deployment=AZURE_OPENAI_CHATGPT_DEPLOYMENT, - embedding_model=OPENAI_EMB_MODEL, - embedding_deployment=AZURE_OPENAI_EMB_DEPLOYMENT, - embedding_dimensions=OPENAI_EMB_DIMENSIONS, - sourcepage_field=KB_FIELDS_SOURCEPAGE, - content_field=KB_FIELDS_CONTENT, - query_language=AZURE_SEARCH_QUERY_LANGUAGE, - query_speller=AZURE_SEARCH_QUERY_SPELLER, - ) - - current_app.config[CONFIG_CHAT_APPROACH] = ChatReadRetrieveReadApproach( - search_client=search_client, - openai_client=openai_client, - auth_helper=auth_helper, - chatgpt_model=OPENAI_CHATGPT_MODEL, - chatgpt_deployment=AZURE_OPENAI_CHATGPT_DEPLOYMENT, - embedding_model=OPENAI_EMB_MODEL, - embedding_deployment=AZURE_OPENAI_EMB_DEPLOYMENT, - embedding_dimensions=OPENAI_EMB_DIMENSIONS, - sourcepage_field=KB_FIELDS_SOURCEPAGE, - content_field=KB_FIELDS_CONTENT, - query_language=AZURE_SEARCH_QUERY_LANGUAGE, - query_speller=AZURE_SEARCH_QUERY_SPELLER, - ) - - if USE_GPT4V: - current_app.logger.info("USE_GPT4V is true, setting up GPT4V approach") - if not AZURE_OPENAI_GPT4V_MODEL: - raise ValueError("AZURE_OPENAI_GPT4V_MODEL must be set when USE_GPT4V is true") - token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default") - - current_app.config[CONFIG_ASK_VISION_APPROACH] = RetrieveThenReadVisionApproach( - search_client=search_client, - openai_client=openai_client, - blob_container_client=blob_container_client, - auth_helper=auth_helper, - vision_endpoint=AZURE_VISION_ENDPOINT, - vision_token_provider=token_provider, - gpt4v_deployment=AZURE_OPENAI_GPT4V_DEPLOYMENT, - gpt4v_model=AZURE_OPENAI_GPT4V_MODEL, - embedding_model=OPENAI_EMB_MODEL, - embedding_deployment=AZURE_OPENAI_EMB_DEPLOYMENT, - embedding_dimensions=OPENAI_EMB_DIMENSIONS, - sourcepage_field=KB_FIELDS_SOURCEPAGE, - content_field=KB_FIELDS_CONTENT, - query_language=AZURE_SEARCH_QUERY_LANGUAGE, - query_speller=AZURE_SEARCH_QUERY_SPELLER, - ) - - current_app.config[CONFIG_CHAT_VISION_APPROACH] = ChatReadRetrieveReadVisionApproach( - search_client=search_client, - openai_client=openai_client, - blob_container_client=blob_container_client, - auth_helper=auth_helper, - vision_endpoint=AZURE_VISION_ENDPOINT, - vision_token_provider=token_provider, - chatgpt_model=OPENAI_CHATGPT_MODEL, - chatgpt_deployment=AZURE_OPENAI_CHATGPT_DEPLOYMENT, - gpt4v_deployment=AZURE_OPENAI_GPT4V_DEPLOYMENT, - gpt4v_model=AZURE_OPENAI_GPT4V_MODEL, - embedding_model=OPENAI_EMB_MODEL, - embedding_deployment=AZURE_OPENAI_EMB_DEPLOYMENT, - embedding_dimensions=OPENAI_EMB_DIMENSIONS, - sourcepage_field=KB_FIELDS_SOURCEPAGE, - content_field=KB_FIELDS_CONTENT, - query_language=AZURE_SEARCH_QUERY_LANGUAGE, - query_speller=AZURE_SEARCH_QUERY_SPELLER, - ) - - -@bp.after_app_serving -async def close_clients(): - await current_app.config[CONFIG_SEARCH_CLIENT].close() - await current_app.config[CONFIG_BLOB_CONTAINER_CLIENT].close() - if current_app.config.get(CONFIG_USER_BLOB_CONTAINER_CLIENT): - await current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT].close() - - -def create_app(): - app = Quart(__name__) - app.register_blueprint(bp) - - if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"): - configure_azure_monitor() - # This tracks HTTP requests made by aiohttp: - AioHttpClientInstrumentor().instrument() - # This tracks HTTP requests made by httpx: - HTTPXClientInstrumentor().instrument() - # This tracks OpenAI SDK requests: - OpenAIInstrumentor().instrument() - # This middleware tracks app route requests: - app.asgi_app = OpenTelemetryMiddleware(app.asgi_app) # type: ignore[assignment] - - # Level should be one of https://docs.python.org/3/library/logging.html#logging-levels - default_level = "INFO" # In development, log more verbosely - if os.getenv("WEBSITE_HOSTNAME"): # In production, don't log as heavily - default_level = "WARNING" - logging.basicConfig(level=os.getenv("APP_LOG_LEVEL", default_level)) - - if allowed_origin := os.getenv("ALLOWED_ORIGIN"): - app.logger.info("CORS enabled for %s", allowed_origin) - cors(app, allow_origin=allowed_origin, allow_methods=["GET", "POST"]) - return app +import dataclasses +import io +import json +import logging +import mimetypes +import os +import time +from pathlib import Path +from typing import Any, AsyncGenerator, Dict, Union, cast + +from azure.cognitiveservices.speech import ( + ResultReason, + SpeechConfig, + SpeechSynthesisOutputFormat, + SpeechSynthesisResult, + SpeechSynthesizer, +) +from azure.core.exceptions import ResourceNotFoundError +from azure.identity.aio import DefaultAzureCredential, get_bearer_token_provider +from azure.monitor.opentelemetry import configure_azure_monitor +from azure.search.documents.aio import SearchClient +from azure.search.documents.indexes.aio import SearchIndexClient +from azure.storage.blob.aio import ContainerClient +from azure.storage.blob.aio import StorageStreamDownloader as BlobDownloader +from azure.storage.filedatalake.aio import FileSystemClient +from azure.storage.filedatalake.aio import StorageStreamDownloader as DatalakeDownloader +from openai import AsyncAzureOpenAI, AsyncOpenAI +from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor +from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware +from opentelemetry.instrumentation.httpx import ( + HTTPXClientInstrumentor, +) +from opentelemetry.instrumentation.openai import OpenAIInstrumentor +from quart import ( + Blueprint, + Quart, + abort, + current_app, + jsonify, + make_response, + request, + send_file, + send_from_directory, +) +from quart_cors import cors + +from approaches.approach import Approach +from approaches.chatreadretrieveread import ChatReadRetrieveReadApproach +from approaches.chatreadretrievereadvision import ChatReadRetrieveReadVisionApproach +from approaches.retrievethenread import RetrieveThenReadApproach +from approaches.retrievethenreadvision import RetrieveThenReadVisionApproach +from config import ( + CONFIG_ASK_APPROACH, + CONFIG_ASK_VISION_APPROACH, + CONFIG_AUTH_CLIENT, + CONFIG_BLOB_CONTAINER_CLIENT, + CONFIG_CHAT_APPROACH, + CONFIG_CHAT_VISION_APPROACH, + CONFIG_CREDENTIAL, + CONFIG_GPT4V_DEPLOYED, + CONFIG_INGESTER, + CONFIG_OPENAI_CLIENT, + CONFIG_SEARCH_CLIENT, + CONFIG_SEMANTIC_RANKER_DEPLOYED, + CONFIG_SPEECH_INPUT_ENABLED, + CONFIG_SPEECH_OUTPUT_AZURE_ENABLED, + CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED, + CONFIG_SPEECH_SERVICE_ID, + CONFIG_SPEECH_SERVICE_LOCATION, + CONFIG_SPEECH_SERVICE_TOKEN, + CONFIG_SPEECH_SERVICE_VOICE, + CONFIG_USER_BLOB_CONTAINER_CLIENT, + CONFIG_USER_UPLOAD_ENABLED, + CONFIG_VECTOR_SEARCH_ENABLED, +) +from core.authentication import AuthenticationHelper +from decorators import authenticated, authenticated_path +from error import error_dict, error_response +from prepdocs import ( + clean_key_if_exists, + setup_embeddings_service, + setup_file_processors, + setup_search_info, +) +from prepdocslib.filestrategy import UploadUserFileStrategy +from prepdocslib.listfilestrategy import File + +bp = Blueprint("routes", __name__, static_folder="static") +# Fix Windows registry issue with mimetypes +mimetypes.add_type("application/javascript", ".js") +mimetypes.add_type("text/css", ".css") + + +@bp.route("/") +async def index(): + return await bp.send_static_file("index.html") + + +# Empty page is recommended for login redirect to work. +# See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/initialization.md#redirecturi-considerations for more information +@bp.route("/redirect") +async def redirect(): + return "" + + +@bp.route("/favicon.ico") +async def favicon(): + return await bp.send_static_file("favicon.ico") + + +@bp.route("/assets/") +async def assets(path): + return await send_from_directory(Path(__file__).resolve().parent / "static" / "assets", path) + + +@bp.route("/content/") +@authenticated_path +async def content_file(path: str, auth_claims: Dict[str, Any]): + """ + Serve content files from blob storage from within the app to keep the example self-contained. + *** NOTE *** if you are using app services authentication, this route will return unauthorized to all users that are not logged in + if AZURE_ENFORCE_ACCESS_CONTROL is not set or false, logged in users can access all files regardless of access control + if AZURE_ENFORCE_ACCESS_CONTROL is set to true, logged in users can only access files they have access to + This is also slow and memory hungry. + """ + # Remove page number from path, filename-1.txt -> filename.txt + # This shouldn't typically be necessary as browsers don't send hash fragments to servers + if path.find("#page=") > 0: + path_parts = path.rsplit("#page=", 1) + path = path_parts[0] + logging.info("Opening file %s", path) + blob_container_client: ContainerClient = current_app.config[CONFIG_BLOB_CONTAINER_CLIENT] + blob: Union[BlobDownloader, DatalakeDownloader] + try: + blob = await blob_container_client.get_blob_client(path).download_blob() + except ResourceNotFoundError: + logging.info("Path not found in general Blob container: %s", path) + if current_app.config[CONFIG_USER_UPLOAD_ENABLED]: + try: + user_oid = auth_claims["oid"] + user_blob_container_client = current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT] + user_directory_client: FileSystemClient = user_blob_container_client.get_directory_client(user_oid) + file_client = user_directory_client.get_file_client(path) + blob = await file_client.download_file() + except ResourceNotFoundError: + logging.exception("Path not found in DataLake: %s", path) + abort(404) + else: + abort(404) + if not blob.properties or not blob.properties.has_key("content_settings"): + abort(404) + mime_type = blob.properties["content_settings"]["content_type"] + if mime_type == "application/octet-stream": + mime_type = mimetypes.guess_type(path)[0] or "application/octet-stream" + blob_file = io.BytesIO() + await blob.readinto(blob_file) + blob_file.seek(0) + return await send_file(blob_file, mimetype=mime_type, as_attachment=False, attachment_filename=path) + + +@bp.route("/ask", methods=["POST"]) +@authenticated +async def ask(auth_claims: Dict[str, Any]): + if not request.is_json: + return jsonify({"error": "request must be json"}), 415 + request_json = await request.get_json() + context = request_json.get("context", {}) + context["auth_claims"] = auth_claims + try: + use_gpt4v = context.get("overrides", {}).get("use_gpt4v", False) + approach: Approach + if use_gpt4v and CONFIG_ASK_VISION_APPROACH in current_app.config: + approach = cast(Approach, current_app.config[CONFIG_ASK_VISION_APPROACH]) + else: + approach = cast(Approach, current_app.config[CONFIG_ASK_APPROACH]) + r = await approach.run( + request_json["messages"], context=context, session_state=request_json.get("session_state") + ) + return jsonify(r) + except Exception as error: + return error_response(error, "/ask") + + +class JSONEncoder(json.JSONEncoder): + def default(self, o): + if dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + return super().default(o) + + +async def format_as_ndjson(r: AsyncGenerator[dict, None]) -> AsyncGenerator[str, None]: + try: + async for event in r: + yield json.dumps(event, ensure_ascii=False, cls=JSONEncoder) + "\n" + except Exception as error: + logging.exception("Exception while generating response stream: %s", error) + yield json.dumps(error_dict(error)) + + +@bp.route("/chat", methods=["POST"]) +@authenticated +async def chat(auth_claims: Dict[str, Any]): + if not request.is_json: + return jsonify({"error": "request must be json"}), 415 + request_json = await request.get_json() + context = request_json.get("context", {}) + context["auth_claims"] = auth_claims + try: + use_gpt4v = context.get("overrides", {}).get("use_gpt4v", False) + approach: Approach + if use_gpt4v and CONFIG_CHAT_VISION_APPROACH in current_app.config: + approach = cast(Approach, current_app.config[CONFIG_CHAT_VISION_APPROACH]) + else: + approach = cast(Approach, current_app.config[CONFIG_CHAT_APPROACH]) + + result = await approach.run( + request_json["messages"], + context=context, + session_state=request_json.get("session_state"), + ) + return jsonify(result) + except Exception as error: + return error_response(error, "/chat") + + +@bp.route("/chat/stream", methods=["POST"]) +@authenticated +async def chat_stream(auth_claims: Dict[str, Any]): + if not request.is_json: + return jsonify({"error": "request must be json"}), 415 + request_json = await request.get_json() + context = request_json.get("context", {}) + context["auth_claims"] = auth_claims + try: + use_gpt4v = context.get("overrides", {}).get("use_gpt4v", False) + approach: Approach + if use_gpt4v and CONFIG_CHAT_VISION_APPROACH in current_app.config: + approach = cast(Approach, current_app.config[CONFIG_CHAT_VISION_APPROACH]) + else: + approach = cast(Approach, current_app.config[CONFIG_CHAT_APPROACH]) + + result = await approach.run_stream( + request_json["messages"], + context=context, + session_state=request_json.get("session_state"), + ) + response = await make_response(format_as_ndjson(result)) + response.timeout = None # type: ignore + response.mimetype = "application/json-lines" + return response + except Exception as error: + return error_response(error, "/chat") + + +# Send MSAL.js settings to the client UI +@bp.route("/auth_setup", methods=["GET"]) +def auth_setup(): + auth_helper = current_app.config[CONFIG_AUTH_CLIENT] + return jsonify(auth_helper.get_auth_setup_for_client()) + + +@bp.route("/config", methods=["GET"]) +def config(): + return jsonify( + { + "showGPT4VOptions": current_app.config[CONFIG_GPT4V_DEPLOYED], + "showSemanticRankerOption": current_app.config[CONFIG_SEMANTIC_RANKER_DEPLOYED], + "showVectorOption": current_app.config[CONFIG_VECTOR_SEARCH_ENABLED], + "showUserUpload": current_app.config[CONFIG_USER_UPLOAD_ENABLED], + "showSpeechInput": current_app.config[CONFIG_SPEECH_INPUT_ENABLED], + "showSpeechOutputBrowser": current_app.config[CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED], + "showSpeechOutputAzure": current_app.config[CONFIG_SPEECH_OUTPUT_AZURE_ENABLED], + } + ) + + +@bp.route("/speech", methods=["POST"]) +async def speech(): + if not request.is_json: + return jsonify({"error": "request must be json"}), 415 + + speech_token = current_app.config.get(CONFIG_SPEECH_SERVICE_TOKEN) + if speech_token is None or speech_token.expires_on < time.time() + 60: + speech_token = await current_app.config[CONFIG_CREDENTIAL].get_token( + "https://cognitiveservices.azure.com/.default" + ) + current_app.config[CONFIG_SPEECH_SERVICE_TOKEN] = speech_token + + request_json = await request.get_json() + text = request_json["text"] + try: + # Construct a token as described in documentation: + # https://learn.microsoft.com/azure/ai-services/speech-service/how-to-configure-azure-ad-auth?pivots=programming-language-python + auth_token = ( + "aad#" + + current_app.config[CONFIG_SPEECH_SERVICE_ID] + + "#" + + current_app.config[CONFIG_SPEECH_SERVICE_TOKEN].token + ) + speech_config = SpeechConfig(auth_token=auth_token, region=current_app.config[CONFIG_SPEECH_SERVICE_LOCATION]) + speech_config.speech_synthesis_voice_name = current_app.config[CONFIG_SPEECH_SERVICE_VOICE] + speech_config.speech_synthesis_output_format = SpeechSynthesisOutputFormat.Audio16Khz32KBitRateMonoMp3 + synthesizer = SpeechSynthesizer(speech_config=speech_config, audio_config=None) + result: SpeechSynthesisResult = synthesizer.speak_text_async(text).get() + if result.reason == ResultReason.SynthesizingAudioCompleted: + return result.audio_data, 200, {"Content-Type": "audio/mp3"} + elif result.reason == ResultReason.Canceled: + cancellation_details = result.cancellation_details + current_app.logger.error( + "Speech synthesis canceled: %s %s", cancellation_details.reason, cancellation_details.error_details + ) + raise Exception("Speech synthesis canceled. Check logs for details.") + else: + current_app.logger.error("Unexpected result reason: %s", result.reason) + raise Exception("Speech synthesis failed. Check logs for details.") + except Exception as e: + logging.exception("Exception in /speech") + return jsonify({"error": str(e)}), 500 + + +@bp.post("/upload") +@authenticated +async def upload(auth_claims: dict[str, Any]): + request_files = await request.files + if "file" not in request_files: + # If no files were included in the request, return an error response + return jsonify({"message": "No file part in the request", "status": "failed"}), 400 + + user_oid = auth_claims["oid"] + file = request_files.getlist("file")[0] + user_blob_container_client: FileSystemClient = current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT] + user_directory_client = user_blob_container_client.get_directory_client(user_oid) + try: + await user_directory_client.get_directory_properties() + except ResourceNotFoundError: + current_app.logger.info("Creating directory for user %s", user_oid) + await user_directory_client.create_directory() + await user_directory_client.set_access_control(owner=user_oid) + file_client = user_directory_client.get_file_client(file.filename) + file_io = file + file_io.name = file.filename + file_io = io.BufferedReader(file_io) + await file_client.upload_data(file_io, overwrite=True, metadata={"UploadedBy": user_oid}) + file_io.seek(0) + ingester: UploadUserFileStrategy = current_app.config[CONFIG_INGESTER] + await ingester.add_file(File(content=file_io, acls={"oids": [user_oid]}, url=file_client.url)) + return jsonify({"message": "File uploaded successfully"}), 200 + + +@bp.post("/delete_uploaded") +@authenticated +async def delete_uploaded(auth_claims: dict[str, Any]): + request_json = await request.get_json() + filename = request_json.get("filename") + user_oid = auth_claims["oid"] + user_blob_container_client: FileSystemClient = current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT] + user_directory_client = user_blob_container_client.get_directory_client(user_oid) + file_client = user_directory_client.get_file_client(filename) + await file_client.delete_file() + ingester = current_app.config[CONFIG_INGESTER] + await ingester.remove_file(filename, user_oid) + return jsonify({"message": f"File {filename} deleted successfully"}), 200 + + +@bp.get("/list_uploaded") +@authenticated +async def list_uploaded(auth_claims: dict[str, Any]): + user_oid = auth_claims["oid"] + user_blob_container_client: FileSystemClient = current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT] + files = [] + try: + all_paths = user_blob_container_client.get_paths(path=user_oid) + async for path in all_paths: + files.append(path.name.split("/", 1)[1]) + except ResourceNotFoundError as error: + if error.status_code != 404: + current_app.logger.exception("Error listing uploaded files", error) + return jsonify(files), 200 + + +@bp.before_app_serving +async def setup_clients(): + # Replace these with your own values, either in environment variables or directly here + AZURE_STORAGE_ACCOUNT = os.environ["AZURE_STORAGE_ACCOUNT"] + AZURE_STORAGE_CONTAINER = os.environ["AZURE_STORAGE_CONTAINER"] + AZURE_USERSTORAGE_ACCOUNT = os.environ.get("AZURE_USERSTORAGE_ACCOUNT") + AZURE_USERSTORAGE_CONTAINER = os.environ.get("AZURE_USERSTORAGE_CONTAINER") + AZURE_SEARCH_SERVICE = os.environ["AZURE_SEARCH_SERVICE"] + AZURE_SEARCH_INDEX = os.environ["AZURE_SEARCH_INDEX"] + # Shared by all OpenAI deployments + OPENAI_HOST = os.getenv("OPENAI_HOST", "azure") + OPENAI_CHATGPT_MODEL = os.environ["AZURE_OPENAI_CHATGPT_MODEL"] + OPENAI_EMB_MODEL = os.getenv("AZURE_OPENAI_EMB_MODEL_NAME", "text-embedding-ada-002") + OPENAI_EMB_DIMENSIONS = int(os.getenv("AZURE_OPENAI_EMB_DIMENSIONS", 1536)) + # Used with Azure OpenAI deployments + AZURE_OPENAI_SERVICE = os.getenv("AZURE_OPENAI_SERVICE") + AZURE_OPENAI_GPT4V_DEPLOYMENT = os.environ.get("AZURE_OPENAI_GPT4V_DEPLOYMENT") + AZURE_OPENAI_GPT4V_MODEL = os.environ.get("AZURE_OPENAI_GPT4V_MODEL") + AZURE_OPENAI_CHATGPT_DEPLOYMENT = ( + os.getenv("AZURE_OPENAI_CHATGPT_DEPLOYMENT") if OPENAI_HOST.startswith("azure") else None + ) + AZURE_OPENAI_EMB_DEPLOYMENT = os.getenv("AZURE_OPENAI_EMB_DEPLOYMENT") if OPENAI_HOST.startswith("azure") else None + AZURE_VISION_ENDPOINT = os.getenv("AZURE_VISION_ENDPOINT", "") + # Used only with non-Azure OpenAI deployments + OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + OPENAI_ORGANIZATION = os.getenv("OPENAI_ORGANIZATION") + + AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID") + AZURE_USE_AUTHENTICATION = os.getenv("AZURE_USE_AUTHENTICATION", "").lower() == "true" + AZURE_ENFORCE_ACCESS_CONTROL = os.getenv("AZURE_ENFORCE_ACCESS_CONTROL", "").lower() == "true" + AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS = os.getenv("AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS", "").lower() == "true" + AZURE_ENABLE_UNAUTHENTICATED_ACCESS = os.getenv("AZURE_ENABLE_UNAUTHENTICATED_ACCESS", "").lower() == "true" + AZURE_SERVER_APP_ID = os.getenv("AZURE_SERVER_APP_ID") + AZURE_SERVER_APP_SECRET = os.getenv("AZURE_SERVER_APP_SECRET") + AZURE_CLIENT_APP_ID = os.getenv("AZURE_CLIENT_APP_ID") + AZURE_AUTH_TENANT_ID = os.getenv("AZURE_AUTH_TENANT_ID", AZURE_TENANT_ID) + + KB_FIELDS_CONTENT = os.getenv("KB_FIELDS_CONTENT", "content") + KB_FIELDS_SOURCEPAGE = os.getenv("KB_FIELDS_SOURCEPAGE", "sourcepage") + + AZURE_SEARCH_QUERY_LANGUAGE = os.getenv("AZURE_SEARCH_QUERY_LANGUAGE", "en-us") + AZURE_SEARCH_QUERY_SPELLER = os.getenv("AZURE_SEARCH_QUERY_SPELLER", "lexicon") + AZURE_SEARCH_SEMANTIC_RANKER = os.getenv("AZURE_SEARCH_SEMANTIC_RANKER", "free").lower() + + AZURE_SPEECH_SERVICE_ID = os.getenv("AZURE_SPEECH_SERVICE_ID") + AZURE_SPEECH_SERVICE_LOCATION = os.getenv("AZURE_SPEECH_SERVICE_LOCATION") + AZURE_SPEECH_VOICE = os.getenv("AZURE_SPEECH_VOICE", "en-US-AndrewMultilingualNeural") + + USE_GPT4V = os.getenv("USE_GPT4V", "").lower() == "true" + USE_USER_UPLOAD = os.getenv("USE_USER_UPLOAD", "").lower() == "true" + USE_SPEECH_INPUT_BROWSER = os.getenv("USE_SPEECH_INPUT_BROWSER", "").lower() == "true" + USE_SPEECH_OUTPUT_BROWSER = os.getenv("USE_SPEECH_OUTPUT_BROWSER", "").lower() == "true" + USE_SPEECH_OUTPUT_AZURE = os.getenv("USE_SPEECH_OUTPUT_AZURE", "").lower() == "true" + + # Use the current user identity to authenticate with Azure OpenAI, AI Search and Blob Storage (no secrets needed, + # just use 'az login' locally, and managed identity when deployed on Azure). If you need to use keys, use separate AzureKeyCredential instances with the + # keys for each service + # 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) + azure_credential = DefaultAzureCredential(exclude_shared_token_cache_credential=True) + + # Set up clients for AI Search and Storage + search_client = SearchClient( + endpoint=f"https://{AZURE_SEARCH_SERVICE}.search.windows.net", + index_name=AZURE_SEARCH_INDEX, + credential=azure_credential, + ) + + blob_container_client = ContainerClient( + f"https://{AZURE_STORAGE_ACCOUNT}.blob.core.windows.net", AZURE_STORAGE_CONTAINER, credential=azure_credential + ) + + # Set up authentication helper + search_index = None + if AZURE_USE_AUTHENTICATION: + search_index_client = SearchIndexClient( + endpoint=f"https://{AZURE_SEARCH_SERVICE}.search.windows.net", + credential=azure_credential, + ) + search_index = await search_index_client.get_index(AZURE_SEARCH_INDEX) + await search_index_client.close() + auth_helper = AuthenticationHelper( + search_index=search_index, + use_authentication=AZURE_USE_AUTHENTICATION, + server_app_id=AZURE_SERVER_APP_ID, + server_app_secret=AZURE_SERVER_APP_SECRET, + client_app_id=AZURE_CLIENT_APP_ID, + tenant_id=AZURE_AUTH_TENANT_ID, + require_access_control=AZURE_ENFORCE_ACCESS_CONTROL, + enable_global_documents=AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS, + enable_unauthenticated_access=AZURE_ENABLE_UNAUTHENTICATED_ACCESS, + ) + + if USE_USER_UPLOAD: + current_app.logger.info("USE_USER_UPLOAD is true, setting up user upload feature") + if not AZURE_USERSTORAGE_ACCOUNT or not AZURE_USERSTORAGE_CONTAINER: + raise ValueError( + "AZURE_USERSTORAGE_ACCOUNT and AZURE_USERSTORAGE_CONTAINER must be set when USE_USER_UPLOAD is true" + ) + user_blob_container_client = FileSystemClient( + f"https://{AZURE_USERSTORAGE_ACCOUNT}.dfs.core.windows.net", + AZURE_USERSTORAGE_CONTAINER, + credential=azure_credential, + ) + current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT] = user_blob_container_client + + # Set up ingester + file_processors = setup_file_processors( + azure_credential=azure_credential, + document_intelligence_service=os.getenv("AZURE_DOCUMENTINTELLIGENCE_SERVICE"), + local_pdf_parser=os.getenv("USE_LOCAL_PDF_PARSER", "").lower() == "true", + local_html_parser=os.getenv("USE_LOCAL_HTML_PARSER", "").lower() == "true", + search_images=USE_GPT4V, + ) + search_info = await setup_search_info( + search_service=AZURE_SEARCH_SERVICE, index_name=AZURE_SEARCH_INDEX, azure_credential=azure_credential + ) + text_embeddings_service = setup_embeddings_service( + azure_credential=azure_credential, + openai_host=OPENAI_HOST, + openai_model_name=OPENAI_EMB_MODEL, + openai_service=AZURE_OPENAI_SERVICE, + openai_deployment=AZURE_OPENAI_EMB_DEPLOYMENT, + openai_dimensions=OPENAI_EMB_DIMENSIONS, + openai_key=clean_key_if_exists(OPENAI_API_KEY), + openai_org=OPENAI_ORGANIZATION, + disable_vectors=os.getenv("USE_VECTORS", "").lower() == "false", + ) + ingester = UploadUserFileStrategy( + search_info=search_info, embeddings=text_embeddings_service, file_processors=file_processors + ) + current_app.config[CONFIG_INGESTER] = ingester + + # Used by the OpenAI SDK + openai_client: AsyncOpenAI + + if USE_SPEECH_OUTPUT_AZURE: + if not AZURE_SPEECH_SERVICE_ID or AZURE_SPEECH_SERVICE_ID == "": + raise ValueError("Azure speech resource not configured correctly, missing AZURE_SPEECH_SERVICE_ID") + if not AZURE_SPEECH_SERVICE_LOCATION or AZURE_SPEECH_SERVICE_LOCATION == "": + raise ValueError("Azure speech resource not configured correctly, missing AZURE_SPEECH_SERVICE_LOCATION") + current_app.config[CONFIG_SPEECH_SERVICE_ID] = AZURE_SPEECH_SERVICE_ID + current_app.config[CONFIG_SPEECH_SERVICE_LOCATION] = AZURE_SPEECH_SERVICE_LOCATION + current_app.config[CONFIG_SPEECH_SERVICE_VOICE] = AZURE_SPEECH_VOICE + # Wait until token is needed to fetch for the first time + current_app.config[CONFIG_SPEECH_SERVICE_TOKEN] = None + current_app.config[CONFIG_CREDENTIAL] = azure_credential + + if OPENAI_HOST.startswith("azure"): + api_version = os.getenv("AZURE_OPENAI_API_VERSION") or "2024-03-01-preview" + + if OPENAI_HOST == "azure_custom": + endpoint = os.environ["AZURE_OPENAI_CUSTOM_URL"] + else: + endpoint = f"https://{AZURE_OPENAI_SERVICE}.openai.azure.com" + + if api_key := os.getenv("AZURE_OPENAI_API_KEY"): + openai_client = AsyncAzureOpenAI(api_version=api_version, azure_endpoint=endpoint, api_key=api_key) + else: + token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default") + openai_client = AsyncAzureOpenAI( + api_version=api_version, + azure_endpoint=endpoint, + azure_ad_token_provider=token_provider, + ) + elif OPENAI_HOST == "local": + openai_client = AsyncOpenAI( + base_url=os.environ["OPENAI_BASE_URL"], + api_key="no-key-required", + ) + else: + openai_client = AsyncOpenAI( + api_key=OPENAI_API_KEY, + organization=OPENAI_ORGANIZATION, + ) + + current_app.config[CONFIG_OPENAI_CLIENT] = openai_client + current_app.config[CONFIG_SEARCH_CLIENT] = search_client + current_app.config[CONFIG_BLOB_CONTAINER_CLIENT] = blob_container_client + current_app.config[CONFIG_AUTH_CLIENT] = auth_helper + + current_app.config[CONFIG_GPT4V_DEPLOYED] = bool(USE_GPT4V) + current_app.config[CONFIG_SEMANTIC_RANKER_DEPLOYED] = AZURE_SEARCH_SEMANTIC_RANKER != "disabled" + current_app.config[CONFIG_VECTOR_SEARCH_ENABLED] = os.getenv("USE_VECTORS", "").lower() != "false" + current_app.config[CONFIG_USER_UPLOAD_ENABLED] = bool(USE_USER_UPLOAD) + current_app.config[CONFIG_SPEECH_INPUT_ENABLED] = USE_SPEECH_INPUT_BROWSER + current_app.config[CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED] = USE_SPEECH_OUTPUT_BROWSER + current_app.config[CONFIG_SPEECH_OUTPUT_AZURE_ENABLED] = USE_SPEECH_OUTPUT_AZURE + + # Various approaches to integrate GPT and external knowledge, most applications will use a single one of these patterns + # or some derivative, here we include several for exploration purposes + current_app.config[CONFIG_ASK_APPROACH] = RetrieveThenReadApproach( + search_client=search_client, + openai_client=openai_client, + auth_helper=auth_helper, + chatgpt_model=OPENAI_CHATGPT_MODEL, + chatgpt_deployment=AZURE_OPENAI_CHATGPT_DEPLOYMENT, + embedding_model=OPENAI_EMB_MODEL, + embedding_deployment=AZURE_OPENAI_EMB_DEPLOYMENT, + embedding_dimensions=OPENAI_EMB_DIMENSIONS, + sourcepage_field=KB_FIELDS_SOURCEPAGE, + content_field=KB_FIELDS_CONTENT, + query_language=AZURE_SEARCH_QUERY_LANGUAGE, + query_speller=AZURE_SEARCH_QUERY_SPELLER, + ) + + current_app.config[CONFIG_CHAT_APPROACH] = ChatReadRetrieveReadApproach( + search_client=search_client, + openai_client=openai_client, + auth_helper=auth_helper, + chatgpt_model=OPENAI_CHATGPT_MODEL, + chatgpt_deployment=AZURE_OPENAI_CHATGPT_DEPLOYMENT, + embedding_model=OPENAI_EMB_MODEL, + embedding_deployment=AZURE_OPENAI_EMB_DEPLOYMENT, + embedding_dimensions=OPENAI_EMB_DIMENSIONS, + sourcepage_field=KB_FIELDS_SOURCEPAGE, + content_field=KB_FIELDS_CONTENT, + query_language=AZURE_SEARCH_QUERY_LANGUAGE, + query_speller=AZURE_SEARCH_QUERY_SPELLER, + ) + + if USE_GPT4V: + current_app.logger.info("USE_GPT4V is true, setting up GPT4V approach") + if not AZURE_OPENAI_GPT4V_MODEL: + raise ValueError("AZURE_OPENAI_GPT4V_MODEL must be set when USE_GPT4V is true") + token_provider = get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default") + + current_app.config[CONFIG_ASK_VISION_APPROACH] = RetrieveThenReadVisionApproach( + search_client=search_client, + openai_client=openai_client, + blob_container_client=blob_container_client, + auth_helper=auth_helper, + vision_endpoint=AZURE_VISION_ENDPOINT, + vision_token_provider=token_provider, + gpt4v_deployment=AZURE_OPENAI_GPT4V_DEPLOYMENT, + gpt4v_model=AZURE_OPENAI_GPT4V_MODEL, + embedding_model=OPENAI_EMB_MODEL, + embedding_deployment=AZURE_OPENAI_EMB_DEPLOYMENT, + embedding_dimensions=OPENAI_EMB_DIMENSIONS, + sourcepage_field=KB_FIELDS_SOURCEPAGE, + content_field=KB_FIELDS_CONTENT, + query_language=AZURE_SEARCH_QUERY_LANGUAGE, + query_speller=AZURE_SEARCH_QUERY_SPELLER, + ) + + current_app.config[CONFIG_CHAT_VISION_APPROACH] = ChatReadRetrieveReadVisionApproach( + search_client=search_client, + openai_client=openai_client, + blob_container_client=blob_container_client, + auth_helper=auth_helper, + vision_endpoint=AZURE_VISION_ENDPOINT, + vision_token_provider=token_provider, + chatgpt_model=OPENAI_CHATGPT_MODEL, + chatgpt_deployment=AZURE_OPENAI_CHATGPT_DEPLOYMENT, + gpt4v_deployment=AZURE_OPENAI_GPT4V_DEPLOYMENT, + gpt4v_model=AZURE_OPENAI_GPT4V_MODEL, + embedding_model=OPENAI_EMB_MODEL, + embedding_deployment=AZURE_OPENAI_EMB_DEPLOYMENT, + embedding_dimensions=OPENAI_EMB_DIMENSIONS, + sourcepage_field=KB_FIELDS_SOURCEPAGE, + content_field=KB_FIELDS_CONTENT, + query_language=AZURE_SEARCH_QUERY_LANGUAGE, + query_speller=AZURE_SEARCH_QUERY_SPELLER, + ) + + +@bp.after_app_serving +async def close_clients(): + await current_app.config[CONFIG_SEARCH_CLIENT].close() + await current_app.config[CONFIG_BLOB_CONTAINER_CLIENT].close() + if current_app.config.get(CONFIG_USER_BLOB_CONTAINER_CLIENT): + await current_app.config[CONFIG_USER_BLOB_CONTAINER_CLIENT].close() + + +def create_app(): + app = Quart(__name__) + app.register_blueprint(bp) + + if os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"): + configure_azure_monitor() + # This tracks HTTP requests made by aiohttp: + AioHttpClientInstrumentor().instrument() + # This tracks HTTP requests made by httpx: + HTTPXClientInstrumentor().instrument() + # This tracks OpenAI SDK requests: + OpenAIInstrumentor().instrument() + # This middleware tracks app route requests: + app.asgi_app = OpenTelemetryMiddleware(app.asgi_app) # type: ignore[assignment] + + # Level should be one of https://docs.python.org/3/library/logging.html#logging-levels + default_level = "INFO" # In development, log more verbosely + if os.getenv("WEBSITE_HOSTNAME"): # In production, don't log as heavily + default_level = "WARNING" + logging.basicConfig(level=os.getenv("APP_LOG_LEVEL", default_level)) + + if allowed_origin := os.getenv("ALLOWED_ORIGIN"): + app.logger.info("CORS enabled for %s", allowed_origin) + cors(app, allow_origin=allowed_origin, allow_methods=["GET", "POST"]) + return app diff --git a/app/backend/approaches/approach.py b/app/backend/approaches/approach.py index f1fb0a444d..504a3f64e4 100644 --- a/app/backend/approaches/approach.py +++ b/app/backend/approaches/approach.py @@ -1,277 +1,277 @@ -import os -from abc import ABC -from dataclasses import dataclass -from typing import ( - Any, - AsyncGenerator, - Awaitable, - Callable, - List, - Optional, - TypedDict, - cast, -) -from urllib.parse import urljoin - -import aiohttp -from azure.search.documents.aio import SearchClient -from azure.search.documents.models import ( - QueryCaptionResult, - QueryType, - VectorizedQuery, - VectorQuery, -) -from openai import AsyncOpenAI -from openai.types.chat import ChatCompletionMessageParam - -from core.authentication import AuthenticationHelper -from text import nonewlines - - -@dataclass -class Document: - id: Optional[str] - content: Optional[str] - embedding: Optional[List[float]] - image_embedding: Optional[List[float]] - category: Optional[str] - sourcepage: Optional[str] - sourcefile: Optional[str] - oids: Optional[List[str]] - groups: Optional[List[str]] - captions: List[QueryCaptionResult] - score: Optional[float] = None - reranker_score: Optional[float] = None - - def serialize_for_results(self) -> dict[str, Any]: - return { - "id": self.id, - "content": self.content, - "embedding": Document.trim_embedding(self.embedding), - "imageEmbedding": Document.trim_embedding(self.image_embedding), - "category": self.category, - "sourcepage": self.sourcepage, - "sourcefile": self.sourcefile, - "oids": self.oids, - "groups": self.groups, - "captions": ( - [ - { - "additional_properties": caption.additional_properties, - "text": caption.text, - "highlights": caption.highlights, - } - for caption in self.captions - ] - if self.captions - else [] - ), - "score": self.score, - "reranker_score": self.reranker_score, - } - - @classmethod - def trim_embedding(cls, embedding: Optional[List[float]]) -> Optional[str]: - """Returns a trimmed list of floats from the vector embedding.""" - if embedding: - if len(embedding) > 2: - # Format the embedding list to show the first 2 items followed by the count of the remaining items.""" - return f"[{embedding[0]}, {embedding[1]} ...+{len(embedding) - 2} more]" - else: - return str(embedding) - - return None - - -@dataclass -class ThoughtStep: - title: str - description: Optional[Any] - props: Optional[dict[str, Any]] = None - - -class Approach(ABC): - def __init__( - self, - search_client: SearchClient, - openai_client: AsyncOpenAI, - auth_helper: AuthenticationHelper, - query_language: Optional[str], - query_speller: Optional[str], - embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text" - embedding_model: str, - embedding_dimensions: int, - openai_host: str, - vision_endpoint: str, - vision_token_provider: Callable[[], Awaitable[str]], - ): - self.search_client = search_client - self.openai_client = openai_client - self.auth_helper = auth_helper - self.query_language = query_language - self.query_speller = query_speller - self.embedding_deployment = embedding_deployment - self.embedding_model = embedding_model - self.embedding_dimensions = embedding_dimensions - self.openai_host = openai_host - self.vision_endpoint = vision_endpoint - self.vision_token_provider = vision_token_provider - - def build_filter(self, overrides: dict[str, Any], auth_claims: dict[str, Any]) -> Optional[str]: - exclude_category = overrides.get("exclude_category") - security_filter = self.auth_helper.build_security_filters(overrides, auth_claims) - filters = [] - if exclude_category: - filters.append("category ne '{}'".format(exclude_category.replace("'", "''"))) - if security_filter: - filters.append(security_filter) - return None if len(filters) == 0 else " and ".join(filters) - - async def search( - self, - top: int, - query_text: Optional[str], - filter: Optional[str], - vectors: List[VectorQuery], - use_text_search: bool, - use_vector_search: bool, - use_semantic_ranker: bool, - use_semantic_captions: bool, - minimum_search_score: Optional[float], - minimum_reranker_score: Optional[float], - ) -> List[Document]: - search_text = query_text if use_text_search else "" - search_vectors = vectors if use_vector_search else [] - if use_semantic_ranker: - results = await self.search_client.search( - search_text=search_text, - filter=filter, - top=top, - query_caption="extractive|highlight-false" if use_semantic_captions else None, - vector_queries=search_vectors, - query_type=QueryType.SEMANTIC, - query_language=self.query_language, - query_speller=self.query_speller, - semantic_configuration_name="default", - semantic_query=query_text, - ) - else: - results = await self.search_client.search( - search_text=search_text, - filter=filter, - top=top, - vector_queries=search_vectors, - ) - - documents = [] - async for page in results.by_page(): - async for document in page: - documents.append( - Document( - id=document.get("id"), - content=document.get("content"), - embedding=document.get("embedding"), - image_embedding=document.get("imageEmbedding"), - category=document.get("category"), - sourcepage=document.get("sourcepage"), - sourcefile=document.get("sourcefile"), - oids=document.get("oids"), - groups=document.get("groups"), - captions=cast(List[QueryCaptionResult], document.get("@search.captions")), - score=document.get("@search.score"), - reranker_score=document.get("@search.reranker_score"), - ) - ) - - qualified_documents = [ - doc - for doc in documents - if ( - (doc.score or 0) >= (minimum_search_score or 0) - and (doc.reranker_score or 0) >= (minimum_reranker_score or 0) - ) - ] - - return qualified_documents - - def get_sources_content( - self, results: List[Document], use_semantic_captions: bool, use_image_citation: bool - ) -> list[str]: - if use_semantic_captions: - return [ - (self.get_citation((doc.sourcepage or ""), use_image_citation)) - + ": " - + nonewlines(" . ".join([cast(str, c.text) for c in (doc.captions or [])])) - for doc in results - ] - else: - return [ - (self.get_citation((doc.sourcepage or ""), use_image_citation)) + ": " + nonewlines(doc.content or "") - for doc in results - ] - - def get_citation(self, sourcepage: str, use_image_citation: bool) -> str: - if use_image_citation: - return sourcepage - else: - path, ext = os.path.splitext(sourcepage) - if ext.lower() == ".png": - page_idx = path.rfind("-") - page_number = int(path[page_idx + 1 :]) - return f"{path[:page_idx]}.pdf#page={page_number}" - - return sourcepage - - async def compute_text_embedding(self, q: str): - SUPPORTED_DIMENSIONS_MODEL = { - "text-embedding-ada-002": False, - "text-embedding-3-small": True, - "text-embedding-3-large": True, - } - - class ExtraArgs(TypedDict, total=False): - dimensions: int - - dimensions_args: ExtraArgs = ( - {"dimensions": self.embedding_dimensions} if SUPPORTED_DIMENSIONS_MODEL[self.embedding_model] else {} - ) - embedding = await self.openai_client.embeddings.create( - # Azure OpenAI takes the deployment name as the model name - model=self.embedding_deployment if self.embedding_deployment else self.embedding_model, - input=q, - **dimensions_args, - ) - query_vector = embedding.data[0].embedding - return VectorizedQuery(vector=query_vector, k_nearest_neighbors=50, fields="embedding") - - async def compute_image_embedding(self, q: str): - endpoint = urljoin(self.vision_endpoint, "computervision/retrieval:vectorizeText") - headers = {"Content-Type": "application/json"} - params = {"api-version": "2023-02-01-preview", "modelVersion": "latest"} - data = {"text": q} - - headers["Authorization"] = "Bearer " + await self.vision_token_provider() - - async with aiohttp.ClientSession() as session: - async with session.post( - url=endpoint, params=params, headers=headers, json=data, raise_for_status=True - ) as response: - json = await response.json() - image_query_vector = json["vector"] - return VectorizedQuery(vector=image_query_vector, k_nearest_neighbors=50, fields="imageEmbedding") - - async def run( - self, - messages: list[ChatCompletionMessageParam], - session_state: Any = None, - context: dict[str, Any] = {}, - ) -> dict[str, Any]: - raise NotImplementedError - - async def run_stream( - self, - messages: list[ChatCompletionMessageParam], - session_state: Any = None, - context: dict[str, Any] = {}, - ) -> AsyncGenerator[dict[str, Any], None]: - raise NotImplementedError +import os +from abc import ABC +from dataclasses import dataclass +from typing import ( + Any, + AsyncGenerator, + Awaitable, + Callable, + List, + Optional, + TypedDict, + cast, +) +from urllib.parse import urljoin + +import aiohttp +from azure.search.documents.aio import SearchClient +from azure.search.documents.models import ( + QueryCaptionResult, + QueryType, + VectorizedQuery, + VectorQuery, +) +from openai import AsyncOpenAI +from openai.types.chat import ChatCompletionMessageParam + +from core.authentication import AuthenticationHelper +from text import nonewlines + + +@dataclass +class Document: + id: Optional[str] + content: Optional[str] + embedding: Optional[List[float]] + image_embedding: Optional[List[float]] + category: Optional[str] + sourcepage: Optional[str] + sourcefile: Optional[str] + oids: Optional[List[str]] + groups: Optional[List[str]] + captions: List[QueryCaptionResult] + score: Optional[float] = None + reranker_score: Optional[float] = None + + def serialize_for_results(self) -> dict[str, Any]: + return { + "id": self.id, + "content": self.content, + "embedding": Document.trim_embedding(self.embedding), + "imageEmbedding": Document.trim_embedding(self.image_embedding), + "category": self.category, + "sourcepage": self.sourcepage, + "sourcefile": self.sourcefile, + "oids": self.oids, + "groups": self.groups, + "captions": ( + [ + { + "additional_properties": caption.additional_properties, + "text": caption.text, + "highlights": caption.highlights, + } + for caption in self.captions + ] + if self.captions + else [] + ), + "score": self.score, + "reranker_score": self.reranker_score, + } + + @classmethod + def trim_embedding(cls, embedding: Optional[List[float]]) -> Optional[str]: + """Returns a trimmed list of floats from the vector embedding.""" + if embedding: + if len(embedding) > 2: + # Format the embedding list to show the first 2 items followed by the count of the remaining items.""" + return f"[{embedding[0]}, {embedding[1]} ...+{len(embedding) - 2} more]" + else: + return str(embedding) + + return None + + +@dataclass +class ThoughtStep: + title: str + description: Optional[Any] + props: Optional[dict[str, Any]] = None + + +class Approach(ABC): + def __init__( + self, + search_client: SearchClient, + openai_client: AsyncOpenAI, + auth_helper: AuthenticationHelper, + query_language: Optional[str], + query_speller: Optional[str], + embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text" + embedding_model: str, + embedding_dimensions: int, + openai_host: str, + vision_endpoint: str, + vision_token_provider: Callable[[], Awaitable[str]], + ): + self.search_client = search_client + self.openai_client = openai_client + self.auth_helper = auth_helper + self.query_language = query_language + self.query_speller = query_speller + self.embedding_deployment = embedding_deployment + self.embedding_model = embedding_model + self.embedding_dimensions = embedding_dimensions + self.openai_host = openai_host + self.vision_endpoint = vision_endpoint + self.vision_token_provider = vision_token_provider + + def build_filter(self, overrides: dict[str, Any], auth_claims: dict[str, Any]) -> Optional[str]: + exclude_category = overrides.get("exclude_category") + security_filter = self.auth_helper.build_security_filters(overrides, auth_claims) + filters = [] + if exclude_category: + filters.append("category ne '{}'".format(exclude_category.replace("'", "''"))) + if security_filter: + filters.append(security_filter) + return None if len(filters) == 0 else " and ".join(filters) + + async def search( + self, + top: int, + query_text: Optional[str], + filter: Optional[str], + vectors: List[VectorQuery], + use_text_search: bool, + use_vector_search: bool, + use_semantic_ranker: bool, + use_semantic_captions: bool, + minimum_search_score: Optional[float], + minimum_reranker_score: Optional[float], + ) -> List[Document]: + search_text = query_text if use_text_search else "" + search_vectors = vectors if use_vector_search else [] + if use_semantic_ranker: + results = await self.search_client.search( + search_text=search_text, + filter=filter, + top=top, + query_caption="extractive|highlight-false" if use_semantic_captions else None, + vector_queries=search_vectors, + query_type=QueryType.SEMANTIC, + query_language=self.query_language, + query_speller=self.query_speller, + semantic_configuration_name="default", + semantic_query=query_text, + ) + else: + results = await self.search_client.search( + search_text=search_text, + filter=filter, + top=top, + vector_queries=search_vectors, + ) + + documents = [] + async for page in results.by_page(): + async for document in page: + documents.append( + Document( + id=document.get("id"), + content=document.get("content"), + embedding=document.get("embedding"), + image_embedding=document.get("imageEmbedding"), + category=document.get("category"), + sourcepage=document.get("sourcepage"), + sourcefile=document.get("sourcefile"), + oids=document.get("oids"), + groups=document.get("groups"), + captions=cast(List[QueryCaptionResult], document.get("@search.captions")), + score=document.get("@search.score"), + reranker_score=document.get("@search.reranker_score"), + ) + ) + + qualified_documents = [ + doc + for doc in documents + if ( + (doc.score or 0) >= (minimum_search_score or 0) + and (doc.reranker_score or 0) >= (minimum_reranker_score or 0) + ) + ] + + return qualified_documents + + def get_sources_content( + self, results: List[Document], use_semantic_captions: bool, use_image_citation: bool + ) -> list[str]: + if use_semantic_captions: + return [ + (self.get_citation((doc.sourcepage or ""), use_image_citation)) + + ": " + + nonewlines(" . ".join([cast(str, c.text) for c in (doc.captions or [])])) + for doc in results + ] + else: + return [ + (self.get_citation((doc.sourcepage or ""), use_image_citation)) + ": " + nonewlines(doc.content or "") + for doc in results + ] + + def get_citation(self, sourcepage: str, use_image_citation: bool) -> str: + if use_image_citation: + return sourcepage + else: + path, ext = os.path.splitext(sourcepage) + if ext.lower() == ".png": + page_idx = path.rfind("-") + page_number = int(path[page_idx + 1 :]) + return f"{path[:page_idx]}.pdf#page={page_number}" + + return sourcepage + + async def compute_text_embedding(self, q: str): + SUPPORTED_DIMENSIONS_MODEL = { + "text-embedding-ada-002": False, + "text-embedding-3-small": True, + "text-embedding-3-large": True, + } + + class ExtraArgs(TypedDict, total=False): + dimensions: int + + dimensions_args: ExtraArgs = ( + {"dimensions": self.embedding_dimensions} if SUPPORTED_DIMENSIONS_MODEL[self.embedding_model] else {} + ) + embedding = await self.openai_client.embeddings.create( + # Azure OpenAI takes the deployment name as the model name + model=self.embedding_deployment if self.embedding_deployment else self.embedding_model, + input=q, + **dimensions_args, + ) + query_vector = embedding.data[0].embedding + return VectorizedQuery(vector=query_vector, k_nearest_neighbors=50, fields="embedding") + + async def compute_image_embedding(self, q: str): + endpoint = urljoin(self.vision_endpoint, "computervision/retrieval:vectorizeText") + headers = {"Content-Type": "application/json"} + params = {"api-version": "2023-02-01-preview", "modelVersion": "latest"} + data = {"text": q} + + headers["Authorization"] = "Bearer " + await self.vision_token_provider() + + async with aiohttp.ClientSession() as session: + async with session.post( + url=endpoint, params=params, headers=headers, json=data, raise_for_status=True + ) as response: + json = await response.json() + image_query_vector = json["vector"] + return VectorizedQuery(vector=image_query_vector, k_nearest_neighbors=50, fields="imageEmbedding") + + async def run( + self, + messages: list[ChatCompletionMessageParam], + session_state: Any = None, + context: dict[str, Any] = {}, + ) -> dict[str, Any]: + raise NotImplementedError + + async def run_stream( + self, + messages: list[ChatCompletionMessageParam], + session_state: Any = None, + context: dict[str, Any] = {}, + ) -> AsyncGenerator[dict[str, Any], None]: + raise NotImplementedError diff --git a/app/backend/approaches/chatapproach.py b/app/backend/approaches/chatapproach.py index 2b133eca1a..0c47014d26 100644 --- a/app/backend/approaches/chatapproach.py +++ b/app/backend/approaches/chatapproach.py @@ -1,157 +1,157 @@ -import json -import re -from abc import ABC, abstractmethod -from typing import Any, AsyncGenerator, Optional - -from openai.types.chat import ChatCompletion, ChatCompletionMessageParam - -from approaches.approach import Approach - - -class ChatApproach(Approach, ABC): - query_prompt_few_shots: list[ChatCompletionMessageParam] = [ - {"role": "user", "content": "How did crypto do last year?"}, - {"role": "assistant", "content": "Summarize Cryptocurrency Market Dynamics from last year"}, - {"role": "user", "content": "What are my health plans?"}, - {"role": "assistant", "content": "Show available health plans"}, - ] - NO_RESPONSE = "0" - - follow_up_questions_prompt_content = """Generate 3 very brief follow-up questions that the user would likely ask next. - Enclose the follow-up questions in double angle brackets. Example: - <> - <> - <> - Do no repeat questions that have already been asked. - Make sure the last question ends with ">>". - """ - - query_prompt_template = """Below is a history of the conversation so far, and a new question asked by the user that needs to be answered by searching in a knowledge base. - You have access to Azure AI Search index with 100's of documents. - Generate a search query based on the conversation and the new question. - Do not include cited source filenames and document names e.g info.txt or doc.pdf in the search query terms. - Do not include any text inside [] or <<>> in the search query terms. - Do not include any special characters like '+'. - If the question is not in English, translate the question to English before generating the search query. - If you cannot generate a search query, return just the number 0. - """ - - @property - @abstractmethod - def system_message_chat_conversation(self) -> str: - pass - - @abstractmethod - async def run_until_final_call(self, messages, overrides, auth_claims, should_stream) -> tuple: - pass - - def get_system_prompt(self, override_prompt: Optional[str], follow_up_questions_prompt: str) -> str: - if override_prompt is None: - return self.system_message_chat_conversation.format( - injected_prompt="", follow_up_questions_prompt=follow_up_questions_prompt - ) - elif override_prompt.startswith(">>>"): - return self.system_message_chat_conversation.format( - injected_prompt=override_prompt[3:] + "\n", follow_up_questions_prompt=follow_up_questions_prompt - ) - else: - return override_prompt.format(follow_up_questions_prompt=follow_up_questions_prompt) - - def get_search_query(self, chat_completion: ChatCompletion, user_query: str): - response_message = chat_completion.choices[0].message - - if response_message.tool_calls: - for tool in response_message.tool_calls: - if tool.type != "function": - continue - function = tool.function - if function.name == "search_sources": - arg = json.loads(function.arguments) - search_query = arg.get("search_query", self.NO_RESPONSE) - if search_query != self.NO_RESPONSE: - return search_query - elif query_text := response_message.content: - if query_text.strip() != self.NO_RESPONSE: - return query_text - return user_query - - def extract_followup_questions(self, content: str): - return content.split("<<")[0], re.findall(r"<<([^>>]+)>>", content) - - async def run_without_streaming( - self, - messages: list[ChatCompletionMessageParam], - overrides: dict[str, Any], - auth_claims: dict[str, Any], - session_state: Any = None, - ) -> dict[str, Any]: - extra_info, chat_coroutine = await self.run_until_final_call( - messages, overrides, auth_claims, should_stream=False - ) - chat_completion_response: ChatCompletion = await chat_coroutine - chat_resp = chat_completion_response.model_dump() # Convert to dict to make it JSON serializable - chat_resp = chat_resp["choices"][0] - chat_resp["context"] = extra_info - if overrides.get("suggest_followup_questions"): - content, followup_questions = self.extract_followup_questions(chat_resp["message"]["content"]) - chat_resp["message"]["content"] = content - chat_resp["context"]["followup_questions"] = followup_questions - chat_resp["session_state"] = session_state - return chat_resp - - async def run_with_streaming( - self, - messages: list[ChatCompletionMessageParam], - overrides: dict[str, Any], - auth_claims: dict[str, Any], - session_state: Any = None, - ) -> AsyncGenerator[dict, None]: - extra_info, chat_coroutine = await self.run_until_final_call( - messages, overrides, auth_claims, should_stream=True - ) - yield {"delta": {"role": "assistant"}, "context": extra_info, "session_state": session_state} - - followup_questions_started = False - followup_content = "" - async for event_chunk in await chat_coroutine: - # "2023-07-01-preview" API version has a bug where first response has empty choices - event = event_chunk.model_dump() # Convert pydantic model to dict - if event["choices"]: - completion = {"delta": event["choices"][0]["delta"]} - # if event contains << and not >>, it is start of follow-up question, truncate - content = completion["delta"].get("content") - content = content or "" # content may either not exist in delta, or explicitly be None - if overrides.get("suggest_followup_questions") and "<<" in content: - followup_questions_started = True - earlier_content = content[: content.index("<<")] - if earlier_content: - completion["delta"]["content"] = earlier_content - yield completion - followup_content += content[content.index("<<") :] - elif followup_questions_started: - followup_content += content - else: - yield completion - if followup_content: - _, followup_questions = self.extract_followup_questions(followup_content) - yield {"delta": {"role": "assistant"}, "context": {"followup_questions": followup_questions}} - - async def run( - self, - messages: list[ChatCompletionMessageParam], - session_state: Any = None, - context: dict[str, Any] = {}, - ) -> dict[str, Any]: - overrides = context.get("overrides", {}) - auth_claims = context.get("auth_claims", {}) - return await self.run_without_streaming(messages, overrides, auth_claims, session_state) - - async def run_stream( - self, - messages: list[ChatCompletionMessageParam], - session_state: Any = None, - context: dict[str, Any] = {}, - ) -> AsyncGenerator[dict[str, Any], None]: - overrides = context.get("overrides", {}) - auth_claims = context.get("auth_claims", {}) - return self.run_with_streaming(messages, overrides, auth_claims, session_state) +import json +import re +from abc import ABC, abstractmethod +from typing import Any, AsyncGenerator, Optional + +from openai.types.chat import ChatCompletion, ChatCompletionMessageParam + +from approaches.approach import Approach + + +class ChatApproach(Approach, ABC): + query_prompt_few_shots: list[ChatCompletionMessageParam] = [ + {"role": "user", "content": "What happens if an existing facility undergoes significant refurbishment?"}, + {"role": "assistant", "content": "If an existing health facility undergoes substantial changes, such as increasing its size or changing the scope of services, it must comply fully with the latest DHA Health Facility Guidelines. For example, converting an inpatient unit into an ICU would require the new design to fully meet the current guidelines."}, + {"role": "user", "content": "How does the Clinical Governance Framework ensure continuous quality improvement in healthcare facilities, and what role do clinical audits play in this process?"}, + {"role": "assistant", "content": "The Clinical Governance Framework ensures continuous quality improvement through regular monitoring and evaluation of clinical care and services. Clinical audits are a key component of this process. They involve systematically reviewing clinical practices against established standards to identify areas for improvement. Health facilities are required to conduct clinical audits biannually, submit reports to the DHA, and implement action plans to address identified issues. The findings from these audits help inform quality improvement initiatives, ensuring that healthcare services remain safe, effective, and aligned with best practices." + ] + NO_RESPONSE = "0" + + follow_up_questions_prompt_content = """Generate 3 very brief follow-up questions that the user would likely ask next. + Enclose the follow-up questions in double angle brackets. Example: + <> + <> + <> + Do no repeat questions that have already been asked. + Make sure the last question ends with ">>". + """ + + query_prompt_template = """Below is a history of the conversation so far, and a new question asked by the user that needs to be answered by searching in a knowledge base. + You have access to Azure AI Search index with 100's of documents. + Generate a search query based on the conversation and the new question. + Do not include cited source filenames and document names e.g info.txt or doc.pdf in the search query terms. + Do not include any text inside [] or <<>> in the search query terms. + Do not include any special characters like '+'. + If the question is not in English, translate the question to English before generating the search query. + If you cannot generate a search query, return just the number 0. + """ + + @property + @abstractmethod + def system_message_chat_conversation(self) -> str: + pass + + @abstractmethod + async def run_until_final_call(self, messages, overrides, auth_claims, should_stream) -> tuple: + pass + + def get_system_prompt(self, override_prompt: Optional[str], follow_up_questions_prompt: str) -> str: + if override_prompt is None: + return self.system_message_chat_conversation.format( + injected_prompt="", follow_up_questions_prompt=follow_up_questions_prompt + ) + elif override_prompt.startswith(">>>"): + return self.system_message_chat_conversation.format( + injected_prompt=override_prompt[3:] + "\n", follow_up_questions_prompt=follow_up_questions_prompt + ) + else: + return override_prompt.format(follow_up_questions_prompt=follow_up_questions_prompt) + + def get_search_query(self, chat_completion: ChatCompletion, user_query: str): + response_message = chat_completion.choices[0].message + + if response_message.tool_calls: + for tool in response_message.tool_calls: + if tool.type != "function": + continue + function = tool.function + if function.name == "search_sources": + arg = json.loads(function.arguments) + search_query = arg.get("search_query", self.NO_RESPONSE) + if search_query != self.NO_RESPONSE: + return search_query + elif query_text := response_message.content: + if query_text.strip() != self.NO_RESPONSE: + return query_text + return user_query + + def extract_followup_questions(self, content: str): + return content.split("<<")[0], re.findall(r"<<([^>>]+)>>", content) + + async def run_without_streaming( + self, + messages: list[ChatCompletionMessageParam], + overrides: dict[str, Any], + auth_claims: dict[str, Any], + session_state: Any = None, + ) -> dict[str, Any]: + extra_info, chat_coroutine = await self.run_until_final_call( + messages, overrides, auth_claims, should_stream=False + ) + chat_completion_response: ChatCompletion = await chat_coroutine + chat_resp = chat_completion_response.model_dump() # Convert to dict to make it JSON serializable + chat_resp = chat_resp["choices"][0] + chat_resp["context"] = extra_info + if overrides.get("suggest_followup_questions"): + content, followup_questions = self.extract_followup_questions(chat_resp["message"]["content"]) + chat_resp["message"]["content"] = content + chat_resp["context"]["followup_questions"] = followup_questions + chat_resp["session_state"] = session_state + return chat_resp + + async def run_with_streaming( + self, + messages: list[ChatCompletionMessageParam], + overrides: dict[str, Any], + auth_claims: dict[str, Any], + session_state: Any = None, + ) -> AsyncGenerator[dict, None]: + extra_info, chat_coroutine = await self.run_until_final_call( + messages, overrides, auth_claims, should_stream=True + ) + yield {"delta": {"role": "assistant"}, "context": extra_info, "session_state": session_state} + + followup_questions_started = False + followup_content = "" + async for event_chunk in await chat_coroutine: + # "2023-07-01-preview" API version has a bug where first response has empty choices + event = event_chunk.model_dump() # Convert pydantic model to dict + if event["choices"]: + completion = {"delta": event["choices"][0]["delta"]} + # if event contains << and not >>, it is start of follow-up question, truncate + content = completion["delta"].get("content") + content = content or "" # content may either not exist in delta, or explicitly be None + if overrides.get("suggest_followup_questions") and "<<" in content: + followup_questions_started = True + earlier_content = content[: content.index("<<")] + if earlier_content: + completion["delta"]["content"] = earlier_content + yield completion + followup_content += content[content.index("<<") :] + elif followup_questions_started: + followup_content += content + else: + yield completion + if followup_content: + _, followup_questions = self.extract_followup_questions(followup_content) + yield {"delta": {"role": "assistant"}, "context": {"followup_questions": followup_questions}} + + async def run( + self, + messages: list[ChatCompletionMessageParam], + session_state: Any = None, + context: dict[str, Any] = {}, + ) -> dict[str, Any]: + overrides = context.get("overrides", {}) + auth_claims = context.get("auth_claims", {}) + return await self.run_without_streaming(messages, overrides, auth_claims, session_state) + + async def run_stream( + self, + messages: list[ChatCompletionMessageParam], + session_state: Any = None, + context: dict[str, Any] = {}, + ) -> AsyncGenerator[dict[str, Any], None]: + overrides = context.get("overrides", {}) + auth_claims = context.get("auth_claims", {}) + return self.run_with_streaming(messages, overrides, auth_claims, session_state) diff --git a/app/backend/approaches/chatreadretrieveread.py b/app/backend/approaches/chatreadretrieveread.py index 7b3a7d2c3c..9c0fe722d8 100644 --- a/app/backend/approaches/chatreadretrieveread.py +++ b/app/backend/approaches/chatreadretrieveread.py @@ -1,241 +1,242 @@ -from typing import Any, Coroutine, List, Literal, Optional, Union, overload - -from azure.search.documents.aio import SearchClient -from azure.search.documents.models import VectorQuery -from openai import AsyncOpenAI, AsyncStream -from openai.types.chat import ( - ChatCompletion, - ChatCompletionChunk, - ChatCompletionMessageParam, - ChatCompletionToolParam, -) -from openai_messages_token_helper import build_messages, get_token_limit - -from approaches.approach import ThoughtStep -from approaches.chatapproach import ChatApproach -from core.authentication import AuthenticationHelper - - -class ChatReadRetrieveReadApproach(ChatApproach): - """ - A multi-step approach that first uses OpenAI to turn the user's question into a search query, - then uses Azure AI Search to retrieve relevant documents, and then sends the conversation history, - original user question, and search results to OpenAI to generate a response. - """ - - def __init__( - self, - *, - search_client: SearchClient, - auth_helper: AuthenticationHelper, - openai_client: AsyncOpenAI, - chatgpt_model: str, - chatgpt_deployment: Optional[str], # Not needed for non-Azure OpenAI - embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text" - embedding_model: str, - embedding_dimensions: int, - sourcepage_field: str, - content_field: str, - query_language: str, - query_speller: str, - ): - self.search_client = search_client - self.openai_client = openai_client - self.auth_helper = auth_helper - self.chatgpt_model = chatgpt_model - self.chatgpt_deployment = chatgpt_deployment - self.embedding_deployment = embedding_deployment - self.embedding_model = embedding_model - self.embedding_dimensions = embedding_dimensions - self.sourcepage_field = sourcepage_field - self.content_field = content_field - self.query_language = query_language - self.query_speller = query_speller - self.chatgpt_token_limit = get_token_limit(chatgpt_model) - - @property - def system_message_chat_conversation(self): - return """Assistant helps the company employees with their healthcare plan questions, and questions about the employee handbook. Be brief in your answers. - Answer ONLY with the facts listed in the list of sources below. If there isn't enough information below, say you don't know. Do not generate answers that don't use the sources below. If asking a clarifying question to the user would help, ask the question. - For tabular information return it as an html table. Do not return markdown format. If the question is not in English, answer in the language used in the question. - Each source has a name followed by colon and the actual information, always include the source name for each fact you use in the response. Use square brackets to reference the source, for example [info1.txt]. Don't combine sources, list each source separately, for example [info1.txt][info2.pdf]. - {follow_up_questions_prompt} - {injected_prompt} - """ - - @overload - async def run_until_final_call( - self, - messages: list[ChatCompletionMessageParam], - overrides: dict[str, Any], - auth_claims: dict[str, Any], - should_stream: Literal[False], - ) -> tuple[dict[str, Any], Coroutine[Any, Any, ChatCompletion]]: ... - - @overload - async def run_until_final_call( - self, - messages: list[ChatCompletionMessageParam], - overrides: dict[str, Any], - auth_claims: dict[str, Any], - should_stream: Literal[True], - ) -> tuple[dict[str, Any], Coroutine[Any, Any, AsyncStream[ChatCompletionChunk]]]: ... - - async def run_until_final_call( - self, - messages: list[ChatCompletionMessageParam], - overrides: dict[str, Any], - auth_claims: dict[str, Any], - should_stream: bool = False, - ) -> tuple[dict[str, Any], Coroutine[Any, Any, Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]]]: - use_text_search = overrides.get("retrieval_mode") in ["text", "hybrid", None] - use_vector_search = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] - use_semantic_ranker = True if overrides.get("semantic_ranker") else False - use_semantic_captions = True if overrides.get("semantic_captions") else False - top = overrides.get("top", 3) - minimum_search_score = overrides.get("minimum_search_score", 0.0) - minimum_reranker_score = overrides.get("minimum_reranker_score", 0.0) - filter = self.build_filter(overrides, auth_claims) - - original_user_query = messages[-1]["content"] - if not isinstance(original_user_query, str): - raise ValueError("The most recent message content must be a string.") - user_query_request = "Generate search query for: " + original_user_query - - tools: List[ChatCompletionToolParam] = [ - { - "type": "function", - "function": { - "name": "search_sources", - "description": "Retrieve sources from the Azure AI Search index", - "parameters": { - "type": "object", - "properties": { - "search_query": { - "type": "string", - "description": "Query string to retrieve documents from azure search eg: 'Health care plan'", - } - }, - "required": ["search_query"], - }, - }, - } - ] - - # STEP 1: Generate an optimized keyword search query based on the chat history and the last question - query_response_token_limit = 100 - query_messages = build_messages( - model=self.chatgpt_model, - system_prompt=self.query_prompt_template, - tools=tools, - few_shots=self.query_prompt_few_shots, - past_messages=messages[:-1], - new_user_content=user_query_request, - max_tokens=self.chatgpt_token_limit - query_response_token_limit, - ) - - chat_completion: ChatCompletion = await self.openai_client.chat.completions.create( - messages=query_messages, # type: ignore - # Azure OpenAI takes the deployment name as the model name - model=self.chatgpt_deployment if self.chatgpt_deployment else self.chatgpt_model, - temperature=0.0, # Minimize creativity for search query generation - max_tokens=query_response_token_limit, # Setting too low risks malformed JSON, setting too high may affect performance - n=1, - tools=tools, - ) - - query_text = self.get_search_query(chat_completion, original_user_query) - - # STEP 2: Retrieve relevant documents from the search index with the GPT optimized query - - # If retrieval mode includes vectors, compute an embedding for the query - vectors: list[VectorQuery] = [] - if use_vector_search: - vectors.append(await self.compute_text_embedding(query_text)) - - results = await self.search( - top, - query_text, - filter, - vectors, - use_text_search, - use_vector_search, - use_semantic_ranker, - use_semantic_captions, - minimum_search_score, - minimum_reranker_score, - ) - - sources_content = self.get_sources_content(results, use_semantic_captions, use_image_citation=False) - content = "\n".join(sources_content) - - # STEP 3: Generate a contextual and content specific answer using the search results and chat history - - # Allow client to replace the entire prompt, or to inject into the exiting prompt using >>> - system_message = self.get_system_prompt( - overrides.get("prompt_template"), - self.follow_up_questions_prompt_content if overrides.get("suggest_followup_questions") else "", - ) - - response_token_limit = 1024 - messages = build_messages( - model=self.chatgpt_model, - system_prompt=system_message, - past_messages=messages[:-1], - # Model does not handle lengthy system messages well. Moving sources to latest user conversation to solve follow up questions prompt. - new_user_content=original_user_query + "\n\nSources:\n" + content, - max_tokens=self.chatgpt_token_limit - response_token_limit, - ) - - data_points = {"text": sources_content} - - extra_info = { - "data_points": data_points, - "thoughts": [ - ThoughtStep( - "Prompt to generate search query", - [str(message) for message in query_messages], - ( - {"model": self.chatgpt_model, "deployment": self.chatgpt_deployment} - if self.chatgpt_deployment - else {"model": self.chatgpt_model} - ), - ), - ThoughtStep( - "Search using generated search query", - query_text, - { - "use_semantic_captions": use_semantic_captions, - "use_semantic_ranker": use_semantic_ranker, - "top": top, - "filter": filter, - "use_vector_search": use_vector_search, - "use_text_search": use_text_search, - }, - ), - ThoughtStep( - "Search results", - [result.serialize_for_results() for result in results], - ), - ThoughtStep( - "Prompt to generate answer", - [str(message) for message in messages], - ( - {"model": self.chatgpt_model, "deployment": self.chatgpt_deployment} - if self.chatgpt_deployment - else {"model": self.chatgpt_model} - ), - ), - ], - } - - chat_coroutine = self.openai_client.chat.completions.create( - # Azure OpenAI takes the deployment name as the model name - model=self.chatgpt_deployment if self.chatgpt_deployment else self.chatgpt_model, - messages=messages, - temperature=overrides.get("temperature", 0.3), - max_tokens=response_token_limit, - n=1, - stream=should_stream, - ) - return (extra_info, chat_coroutine) +from typing import Any, Coroutine, List, Literal, Optional, Union, overload + +from azure.search.documents.aio import SearchClient +from azure.search.documents.models import VectorQuery +from openai import AsyncOpenAI, AsyncStream +from openai.types.chat import ( + ChatCompletion, + ChatCompletionChunk, + ChatCompletionMessageParam, + ChatCompletionToolParam, +) +from openai_messages_token_helper import build_messages, get_token_limit + +from approaches.approach import ThoughtStep +from approaches.chatapproach import ChatApproach +from core.authentication import AuthenticationHelper + + +class ChatReadRetrieveReadApproach(ChatApproach): + """ + A multi-step approach that first uses OpenAI to turn the user's question into a search query, + then uses Azure AI Search to retrieve relevant documents, and then sends the conversation history, + original user question, and search results to OpenAI to generate a response. + """ + + def __init__( + self, + *, + search_client: SearchClient, + auth_helper: AuthenticationHelper, + openai_client: AsyncOpenAI, + chatgpt_model: str, + chatgpt_deployment: Optional[str], # Not needed for non-Azure OpenAI + embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text" + embedding_model: str, + embedding_dimensions: int, + sourcepage_field: str, + content_field: str, + query_language: str, + query_speller: str, + ): + self.search_client = search_client + self.openai_client = openai_client + self.auth_helper = auth_helper + self.chatgpt_model = chatgpt_model + self.chatgpt_deployment = chatgpt_deployment + self.embedding_deployment = embedding_deployment + self.embedding_model = embedding_model + self.embedding_dimensions = embedding_dimensions + self.sourcepage_field = sourcepage_field + self.content_field = content_field + self.query_language = query_language + self.query_speller = query_speller + self.chatgpt_token_limit = get_token_limit(chatgpt_model) + + @property + def system_message_chat_conversation(self): + return """You are an AI assistant specialized in providing information on the policies and regulations provided by the Dubai Health Authority (DHA). These regulations and policies cover a wide range of areas such as clinical governance, dentistry, outpatient care and home healthcare, as well as other areas. + You will answer questions based on the content of the documents provided by DHA. + Use the information from the policies and regulations to provide accurate and relevant responses to the user's queries. + Answer ONLY with the data provided in all available indexed sources. If there isn't enough information below, say you don't know. Do not generate answers that don't use the sources below. If asking a clarifying question to the user would help, ask the question. + For tabular information return it as an html table. Do not return markdown format. If the question is not in English, answer in the language used in the question. + Each source has a name followed by colon and the actual information, always include the source name for each fact you use in the response. Use square brackets to reference the source, for example [info1.txt]. Don't combine sources, list each source separately, for example [info1.txt][info2.pdf]. + {injected_prompt} + """ + + @overload + async def run_until_final_call( + self, + messages: list[ChatCompletionMessageParam], + overrides: dict[str, Any], + auth_claims: dict[str, Any], + should_stream: Literal[False], + ) -> tuple[dict[str, Any], Coroutine[Any, Any, ChatCompletion]]: ... + + @overload + async def run_until_final_call( + self, + messages: list[ChatCompletionMessageParam], + overrides: dict[str, Any], + auth_claims: dict[str, Any], + should_stream: Literal[True], + ) -> tuple[dict[str, Any], Coroutine[Any, Any, AsyncStream[ChatCompletionChunk]]]: ... + + async def run_until_final_call( + self, + messages: list[ChatCompletionMessageParam], + overrides: dict[str, Any], + auth_claims: dict[str, Any], + should_stream: bool = False, + ) -> tuple[dict[str, Any], Coroutine[Any, Any, Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]]]: + use_text_search = overrides.get("retrieval_mode") in ["text", "hybrid", None] + use_vector_search = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] + use_semantic_ranker = True if overrides.get("semantic_ranker") else False + use_semantic_captions = True if overrides.get("semantic_captions") else False + top = overrides.get("top", 3) + minimum_search_score = overrides.get("minimum_search_score", 0.0) + minimum_reranker_score = overrides.get("minimum_reranker_score", 0.0) + filter = self.build_filter(overrides, auth_claims) + + original_user_query = messages[-1]["content"] + if not isinstance(original_user_query, str): + raise ValueError("The most recent message content must be a string.") + user_query_request = "Generate search query for: " + original_user_query + + tools: List[ChatCompletionToolParam] = [ + { + "type": "function", + "function": { + "name": "search_sources", + "description": "Retrieve sources from the Azure AI Search index", + "parameters": { + "type": "object", + "properties": { + "search_query": { + "type": "string", + "description": "Query string to retrieve documents from azure search eg: 'Health care plan'", + } + }, + "required": ["search_query"], + }, + }, + } + ] + + # STEP 1: Generate an optimized keyword search query based on the chat history and the last question + query_response_token_limit = 100 + query_messages = build_messages( + model=self.chatgpt_model, + system_prompt=self.query_prompt_template, + tools=tools, + few_shots=self.query_prompt_few_shots, + past_messages=messages[:-1], + new_user_content=user_query_request, + max_tokens=self.chatgpt_token_limit - query_response_token_limit, + ) + + chat_completion: ChatCompletion = await self.openai_client.chat.completions.create( + messages=query_messages, # type: ignore + # Azure OpenAI takes the deployment name as the model name + model=self.chatgpt_deployment if self.chatgpt_deployment else self.chatgpt_model, + temperature=0.0, # Minimize creativity for search query generation + max_tokens=query_response_token_limit, # Setting too low risks malformed JSON, setting too high may affect performance + n=1, + tools=tools, + ) + + query_text = self.get_search_query(chat_completion, original_user_query) + + # STEP 2: Retrieve relevant documents from the search index with the GPT optimized query + + # If retrieval mode includes vectors, compute an embedding for the query + vectors: list[VectorQuery] = [] + if use_vector_search: + vectors.append(await self.compute_text_embedding(query_text)) + + results = await self.search( + top, + query_text, + filter, + vectors, + use_text_search, + use_vector_search, + use_semantic_ranker, + use_semantic_captions, + minimum_search_score, + minimum_reranker_score, + ) + + sources_content = self.get_sources_content(results, use_semantic_captions, use_image_citation=False) + content = "\n".join(sources_content) + + # STEP 3: Generate a contextual and content specific answer using the search results and chat history + + # Allow client to replace the entire prompt, or to inject into the exiting prompt using >>> + system_message = self.get_system_prompt( + overrides.get("prompt_template"), + self.follow_up_questions_prompt_content if overrides.get("suggest_followup_questions") else "", + ) + + response_token_limit = 1024 + messages = build_messages( + model=self.chatgpt_model, + system_prompt=system_message, + past_messages=messages[:-1], + # Model does not handle lengthy system messages well. Moving sources to latest user conversation to solve follow up questions prompt. + new_user_content=original_user_query + "\n\nSources:\n" + content, + max_tokens=self.chatgpt_token_limit - response_token_limit, + ) + + data_points = {"text": sources_content} + + extra_info = { + "data_points": data_points, + "thoughts": [ + ThoughtStep( + "Prompt to generate search query", + [str(message) for message in query_messages], + ( + {"model": self.chatgpt_model, "deployment": self.chatgpt_deployment} + if self.chatgpt_deployment + else {"model": self.chatgpt_model} + ), + ), + ThoughtStep( + "Search using generated search query", + query_text, + { + "use_semantic_captions": use_semantic_captions, + "use_semantic_ranker": use_semantic_ranker, + "top": top, + "filter": filter, + "use_vector_search": use_vector_search, + "use_text_search": use_text_search, + }, + ), + ThoughtStep( + "Search results", + [result.serialize_for_results() for result in results], + ), + ThoughtStep( + "Prompt to generate answer", + [str(message) for message in messages], + ( + {"model": self.chatgpt_model, "deployment": self.chatgpt_deployment} + if self.chatgpt_deployment + else {"model": self.chatgpt_model} + ), + ), + ], + } + + chat_coroutine = self.openai_client.chat.completions.create( + # Azure OpenAI takes the deployment name as the model name + model=self.chatgpt_deployment if self.chatgpt_deployment else self.chatgpt_model, + messages=messages, + temperature=overrides.get("temperature", 0.3), + max_tokens=response_token_limit, + n=1, + stream=should_stream, + ) + return (extra_info, chat_coroutine) diff --git a/app/backend/approaches/chatreadretrievereadvision.py b/app/backend/approaches/chatreadretrievereadvision.py index 96664cd0c8..c49815f86d 100644 --- a/app/backend/approaches/chatreadretrievereadvision.py +++ b/app/backend/approaches/chatreadretrievereadvision.py @@ -1,245 +1,245 @@ -from typing import Any, Awaitable, Callable, Coroutine, Optional, Union - -from azure.search.documents.aio import SearchClient -from azure.storage.blob.aio import ContainerClient -from openai import AsyncOpenAI, AsyncStream -from openai.types.chat import ( - ChatCompletion, - ChatCompletionChunk, - ChatCompletionContentPartImageParam, - ChatCompletionContentPartParam, - ChatCompletionMessageParam, -) -from openai_messages_token_helper import build_messages, get_token_limit - -from approaches.approach import ThoughtStep -from approaches.chatapproach import ChatApproach -from core.authentication import AuthenticationHelper -from core.imageshelper import fetch_image - - -class ChatReadRetrieveReadVisionApproach(ChatApproach): - """ - A multi-step approach that first uses OpenAI to turn the user's question into a search query, - then uses Azure AI Search to retrieve relevant documents, and then sends the conversation history, - original user question, and search results to OpenAI to generate a response. - """ - - def __init__( - self, - *, - search_client: SearchClient, - blob_container_client: ContainerClient, - openai_client: AsyncOpenAI, - auth_helper: AuthenticationHelper, - chatgpt_model: str, - chatgpt_deployment: Optional[str], # Not needed for non-Azure OpenAI - gpt4v_deployment: Optional[str], # Not needed for non-Azure OpenAI - gpt4v_model: str, - embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text" - embedding_model: str, - embedding_dimensions: int, - sourcepage_field: str, - content_field: str, - query_language: str, - query_speller: str, - vision_endpoint: str, - vision_token_provider: Callable[[], Awaitable[str]] - ): - self.search_client = search_client - self.blob_container_client = blob_container_client - self.openai_client = openai_client - self.auth_helper = auth_helper - self.chatgpt_model = chatgpt_model - self.chatgpt_deployment = chatgpt_deployment - self.gpt4v_deployment = gpt4v_deployment - self.gpt4v_model = gpt4v_model - self.embedding_deployment = embedding_deployment - self.embedding_model = embedding_model - self.embedding_dimensions = embedding_dimensions - self.sourcepage_field = sourcepage_field - self.content_field = content_field - self.query_language = query_language - self.query_speller = query_speller - self.vision_endpoint = vision_endpoint - self.vision_token_provider = vision_token_provider - self.chatgpt_token_limit = get_token_limit(gpt4v_model) - - @property - def system_message_chat_conversation(self): - return """ - You are an intelligent assistant helping analyze the Annual Financial Report of Contoso Ltd., The documents contain text, graphs, tables and images. - Each image source has the file name in the top left corner of the image with coordinates (10,10) pixels and is in the format SourceFileName: - Each text source starts in a new line and has the file name followed by colon and the actual information - Always include the source name from the image or text for each fact you use in the response in the format: [filename] - Answer the following question using only the data provided in the sources below. - If asking a clarifying question to the user would help, ask the question. - Be brief in your answers. - For tabular information return it as an html table. Do not return markdown format. - The text and image source can be the same file name, don't use the image title when citing the image source, only use the file name as mentioned - If you cannot answer using the sources below, say you don't know. Return just the answer without any input texts. - {follow_up_questions_prompt} - {injected_prompt} - """ - - async def run_until_final_call( - self, - messages: list[ChatCompletionMessageParam], - overrides: dict[str, Any], - auth_claims: dict[str, Any], - should_stream: bool = False, - ) -> tuple[dict[str, Any], Coroutine[Any, Any, Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]]]: - use_text_search = overrides.get("retrieval_mode") in ["text", "hybrid", None] - use_vector_search = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] - use_semantic_ranker = True if overrides.get("semantic_ranker") else False - use_semantic_captions = True if overrides.get("semantic_captions") else False - top = overrides.get("top", 3) - minimum_search_score = overrides.get("minimum_search_score", 0.0) - minimum_reranker_score = overrides.get("minimum_reranker_score", 0.0) - filter = self.build_filter(overrides, auth_claims) - - vector_fields = overrides.get("vector_fields", ["embedding"]) - send_text_to_gptvision = overrides.get("gpt4v_input") in ["textAndImages", "texts", None] - send_images_to_gptvision = overrides.get("gpt4v_input") in ["textAndImages", "images", None] - - original_user_query = messages[-1]["content"] - if not isinstance(original_user_query, str): - raise ValueError("The most recent message content must be a string.") - past_messages: list[ChatCompletionMessageParam] = messages[:-1] - - # STEP 1: Generate an optimized keyword search query based on the chat history and the last question - user_query_request = "Generate search query for: " + original_user_query - - query_response_token_limit = 100 - query_model = self.chatgpt_model - query_deployment = self.chatgpt_deployment - query_messages = build_messages( - model=query_model, - system_prompt=self.query_prompt_template, - few_shots=self.query_prompt_few_shots, - past_messages=past_messages, - new_user_content=user_query_request, - max_tokens=self.chatgpt_token_limit - query_response_token_limit, - ) - - chat_completion: ChatCompletion = await self.openai_client.chat.completions.create( - model=query_deployment if query_deployment else query_model, - messages=query_messages, - temperature=0.0, # Minimize creativity for search query generation - max_tokens=query_response_token_limit, - n=1, - ) - - query_text = self.get_search_query(chat_completion, original_user_query) - - # STEP 2: Retrieve relevant documents from the search index with the GPT optimized query - - # If retrieval mode includes vectors, compute an embedding for the query - vectors = [] - if use_vector_search: - for field in vector_fields: - vector = ( - await self.compute_text_embedding(query_text) - if field == "embedding" - else await self.compute_image_embedding(query_text) - ) - vectors.append(vector) - - results = await self.search( - top, - query_text, - filter, - vectors, - use_text_search, - use_vector_search, - use_semantic_ranker, - use_semantic_captions, - minimum_search_score, - minimum_reranker_score, - ) - sources_content = self.get_sources_content(results, use_semantic_captions, use_image_citation=True) - content = "\n".join(sources_content) - - # STEP 3: Generate a contextual and content specific answer using the search results and chat history - - # Allow client to replace the entire prompt, or to inject into the existing prompt using >>> - system_message = self.get_system_prompt( - overrides.get("prompt_template"), - self.follow_up_questions_prompt_content if overrides.get("suggest_followup_questions") else "", - ) - - user_content: list[ChatCompletionContentPartParam] = [{"text": original_user_query, "type": "text"}] - image_list: list[ChatCompletionContentPartImageParam] = [] - - if send_text_to_gptvision: - user_content.append({"text": "\n\nSources:\n" + content, "type": "text"}) - if send_images_to_gptvision: - for result in results: - url = await fetch_image(self.blob_container_client, result) - if url: - image_list.append({"image_url": url, "type": "image_url"}) - user_content.extend(image_list) - - response_token_limit = 1024 - messages = build_messages( - model=self.gpt4v_model, - system_prompt=system_message, - past_messages=messages[:-1], - new_user_content=user_content, - max_tokens=self.chatgpt_token_limit - response_token_limit, - ) - - data_points = { - "text": sources_content, - "images": [d["image_url"] for d in image_list], - } - - extra_info = { - "data_points": data_points, - "thoughts": [ - ThoughtStep( - "Prompt to generate search query", - [str(message) for message in query_messages], - ( - {"model": query_model, "deployment": query_deployment} - if query_deployment - else {"model": query_model} - ), - ), - ThoughtStep( - "Search using generated search query", - query_text, - { - "use_semantic_captions": use_semantic_captions, - "use_semantic_ranker": use_semantic_ranker, - "top": top, - "filter": filter, - "vector_fields": vector_fields, - "use_text_search": use_text_search, - }, - ), - ThoughtStep( - "Search results", - [result.serialize_for_results() for result in results], - ), - ThoughtStep( - "Prompt to generate answer", - [str(message) for message in messages], - ( - {"model": self.gpt4v_model, "deployment": self.gpt4v_deployment} - if self.gpt4v_deployment - else {"model": self.gpt4v_model} - ), - ), - ], - } - - chat_coroutine = self.openai_client.chat.completions.create( - model=self.gpt4v_deployment if self.gpt4v_deployment else self.gpt4v_model, - messages=messages, - temperature=overrides.get("temperature", 0.3), - max_tokens=response_token_limit, - n=1, - stream=should_stream, - ) - return (extra_info, chat_coroutine) +from typing import Any, Awaitable, Callable, Coroutine, Optional, Union + +from azure.search.documents.aio import SearchClient +from azure.storage.blob.aio import ContainerClient +from openai import AsyncOpenAI, AsyncStream +from openai.types.chat import ( + ChatCompletion, + ChatCompletionChunk, + ChatCompletionContentPartImageParam, + ChatCompletionContentPartParam, + ChatCompletionMessageParam, +) +from openai_messages_token_helper import build_messages, get_token_limit + +from approaches.approach import ThoughtStep +from approaches.chatapproach import ChatApproach +from core.authentication import AuthenticationHelper +from core.imageshelper import fetch_image + + +class ChatReadRetrieveReadVisionApproach(ChatApproach): + """ + A multi-step approach that first uses OpenAI to turn the user's question into a search query, + then uses Azure AI Search to retrieve relevant documents, and then sends the conversation history, + original user question, and search results to OpenAI to generate a response. + """ + + def __init__( + self, + *, + search_client: SearchClient, + blob_container_client: ContainerClient, + openai_client: AsyncOpenAI, + auth_helper: AuthenticationHelper, + chatgpt_model: str, + chatgpt_deployment: Optional[str], # Not needed for non-Azure OpenAI + gpt4v_deployment: Optional[str], # Not needed for non-Azure OpenAI + gpt4v_model: str, + embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text" + embedding_model: str, + embedding_dimensions: int, + sourcepage_field: str, + content_field: str, + query_language: str, + query_speller: str, + vision_endpoint: str, + vision_token_provider: Callable[[], Awaitable[str]] + ): + self.search_client = search_client + self.blob_container_client = blob_container_client + self.openai_client = openai_client + self.auth_helper = auth_helper + self.chatgpt_model = chatgpt_model + self.chatgpt_deployment = chatgpt_deployment + self.gpt4v_deployment = gpt4v_deployment + self.gpt4v_model = gpt4v_model + self.embedding_deployment = embedding_deployment + self.embedding_model = embedding_model + self.embedding_dimensions = embedding_dimensions + self.sourcepage_field = sourcepage_field + self.content_field = content_field + self.query_language = query_language + self.query_speller = query_speller + self.vision_endpoint = vision_endpoint + self.vision_token_provider = vision_token_provider + self.chatgpt_token_limit = get_token_limit(gpt4v_model) + + @property + def system_message_chat_conversation(self): + return """ + You are an intelligent assistant helping users understand the policies and regulations at the Dubai Health Authority. The documents contain text, graphs, tables and images + Each image source has the file name in the top left corner of the image with coordinates (10,10) pixels and is in the format SourceFileName: + Each text source starts in a new line and has the file name followed by colon and the actual information + Always include the source name from the image or text for each fact you use in the response in the format: [filename] + Answer the following question using only the data provided in the sources below. + If asking a clarifying question to the user would help, ask the question. + Be brief in your answers. + For tabular information return it as an html table. Do not return markdown format. + The text and image source can be the same file name, don't use the image title when citing the image source, only use the file name as mentioned + If you cannot answer using the sources below, say you don't know. Return just the answer without any input texts. + {follow_up_questions_prompt} + {injected_prompt} + """ + + async def run_until_final_call( + self, + messages: list[ChatCompletionMessageParam], + overrides: dict[str, Any], + auth_claims: dict[str, Any], + should_stream: bool = False, + ) -> tuple[dict[str, Any], Coroutine[Any, Any, Union[ChatCompletion, AsyncStream[ChatCompletionChunk]]]]: + use_text_search = overrides.get("retrieval_mode") in ["text", "hybrid", None] + use_vector_search = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] + use_semantic_ranker = True if overrides.get("semantic_ranker") else False + use_semantic_captions = True if overrides.get("semantic_captions") else False + top = overrides.get("top", 3) + minimum_search_score = overrides.get("minimum_search_score", 0.0) + minimum_reranker_score = overrides.get("minimum_reranker_score", 0.0) + filter = self.build_filter(overrides, auth_claims) + + vector_fields = overrides.get("vector_fields", ["embedding"]) + send_text_to_gptvision = overrides.get("gpt4v_input") in ["textAndImages", "texts", None] + send_images_to_gptvision = overrides.get("gpt4v_input") in ["textAndImages", "images", None] + + original_user_query = messages[-1]["content"] + if not isinstance(original_user_query, str): + raise ValueError("The most recent message content must be a string.") + past_messages: list[ChatCompletionMessageParam] = messages[:-1] + + # STEP 1: Generate an optimized keyword search query based on the chat history and the last question + user_query_request = "Generate search query for: " + original_user_query + + query_response_token_limit = 100 + query_model = self.chatgpt_model + query_deployment = self.chatgpt_deployment + query_messages = build_messages( + model=query_model, + system_prompt=self.query_prompt_template, + few_shots=self.query_prompt_few_shots, + past_messages=past_messages, + new_user_content=user_query_request, + max_tokens=self.chatgpt_token_limit - query_response_token_limit, + ) + + chat_completion: ChatCompletion = await self.openai_client.chat.completions.create( + model=query_deployment if query_deployment else query_model, + messages=query_messages, + temperature=0.0, # Minimize creativity for search query generation + max_tokens=query_response_token_limit, + n=1, + ) + + query_text = self.get_search_query(chat_completion, original_user_query) + + # STEP 2: Retrieve relevant documents from the search index with the GPT optimized query + + # If retrieval mode includes vectors, compute an embedding for the query + vectors = [] + if use_vector_search: + for field in vector_fields: + vector = ( + await self.compute_text_embedding(query_text) + if field == "embedding" + else await self.compute_image_embedding(query_text) + ) + vectors.append(vector) + + results = await self.search( + top, + query_text, + filter, + vectors, + use_text_search, + use_vector_search, + use_semantic_ranker, + use_semantic_captions, + minimum_search_score, + minimum_reranker_score, + ) + sources_content = self.get_sources_content(results, use_semantic_captions, use_image_citation=True) + content = "\n".join(sources_content) + + # STEP 3: Generate a contextual and content specific answer using the search results and chat history + + # Allow client to replace the entire prompt, or to inject into the existing prompt using >>> + system_message = self.get_system_prompt( + overrides.get("prompt_template"), + self.follow_up_questions_prompt_content if overrides.get("suggest_followup_questions") else "", + ) + + user_content: list[ChatCompletionContentPartParam] = [{"text": original_user_query, "type": "text"}] + image_list: list[ChatCompletionContentPartImageParam] = [] + + if send_text_to_gptvision: + user_content.append({"text": "\n\nSources:\n" + content, "type": "text"}) + if send_images_to_gptvision: + for result in results: + url = await fetch_image(self.blob_container_client, result) + if url: + image_list.append({"image_url": url, "type": "image_url"}) + user_content.extend(image_list) + + response_token_limit = 1024 + messages = build_messages( + model=self.gpt4v_model, + system_prompt=system_message, + past_messages=messages[:-1], + new_user_content=user_content, + max_tokens=self.chatgpt_token_limit - response_token_limit, + ) + + data_points = { + "text": sources_content, + "images": [d["image_url"] for d in image_list], + } + + extra_info = { + "data_points": data_points, + "thoughts": [ + ThoughtStep( + "Prompt to generate search query", + [str(message) for message in query_messages], + ( + {"model": query_model, "deployment": query_deployment} + if query_deployment + else {"model": query_model} + ), + ), + ThoughtStep( + "Search using generated search query", + query_text, + { + "use_semantic_captions": use_semantic_captions, + "use_semantic_ranker": use_semantic_ranker, + "top": top, + "filter": filter, + "vector_fields": vector_fields, + "use_text_search": use_text_search, + }, + ), + ThoughtStep( + "Search results", + [result.serialize_for_results() for result in results], + ), + ThoughtStep( + "Prompt to generate answer", + [str(message) for message in messages], + ( + {"model": self.gpt4v_model, "deployment": self.gpt4v_deployment} + if self.gpt4v_deployment + else {"model": self.gpt4v_model} + ), + ), + ], + } + + chat_coroutine = self.openai_client.chat.completions.create( + model=self.gpt4v_deployment if self.gpt4v_deployment else self.gpt4v_model, + messages=messages, + temperature=overrides.get("temperature", 0.3), + max_tokens=response_token_limit, + n=1, + stream=should_stream, + ) + return (extra_info, chat_coroutine) diff --git a/app/backend/approaches/retrievethenread.py b/app/backend/approaches/retrievethenread.py index 649b2f84fd..00737ca12e 100644 --- a/app/backend/approaches/retrievethenread.py +++ b/app/backend/approaches/retrievethenread.py @@ -1,173 +1,171 @@ -from typing import Any, Optional - -from azure.search.documents.aio import SearchClient -from azure.search.documents.models import VectorQuery -from openai import AsyncOpenAI -from openai.types.chat import ChatCompletionMessageParam -from openai_messages_token_helper import build_messages, get_token_limit - -from approaches.approach import Approach, ThoughtStep -from core.authentication import AuthenticationHelper - - -class RetrieveThenReadApproach(Approach): - """ - Simple retrieve-then-read implementation, using the AI Search and OpenAI APIs directly. It first retrieves - top documents from search, then constructs a prompt with them, and then uses OpenAI to generate an completion - (answer) with that prompt. - """ - - system_chat_template = ( - "You are an intelligent assistant helping Contoso Inc employees with their healthcare plan questions and employee handbook questions. " - + "Use 'you' to refer to the individual asking the questions even if they ask with 'I'. " - + "Answer the following question using only the data provided in the sources below. " - + "For tabular information return it as an html table. Do not return markdown format. " - + "Each source has a name followed by colon and the actual information, always include the source name for each fact you use in the response. " - + "If you cannot answer using the sources below, say you don't know. Use below example to answer" - ) - - # shots/sample conversation - question = """ -'What is the deductible for the employee plan for a visit to Overlake in Bellevue?' - -Sources: -info1.txt: deductibles depend on whether you are in-network or out-of-network. In-network deductibles are $500 for employee and $1000 for family. Out-of-network deductibles are $1000 for employee and $2000 for family. -info2.pdf: Overlake is in-network for the employee plan. -info3.pdf: Overlake is the name of the area that includes a park and ride near Bellevue. -info4.pdf: In-network institutions include Overlake, Swedish and others in the region -""" - answer = "In-network deductibles are $500 for employee and $1000 for family [info1.txt] and Overlake is in-network for the employee plan [info2.pdf][info4.pdf]." - - def __init__( - self, - *, - search_client: SearchClient, - auth_helper: AuthenticationHelper, - openai_client: AsyncOpenAI, - chatgpt_model: str, - chatgpt_deployment: Optional[str], # Not needed for non-Azure OpenAI - embedding_model: str, - embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text" - embedding_dimensions: int, - sourcepage_field: str, - content_field: str, - query_language: str, - query_speller: str, - ): - self.search_client = search_client - self.chatgpt_deployment = chatgpt_deployment - self.openai_client = openai_client - self.auth_helper = auth_helper - self.chatgpt_model = chatgpt_model - self.embedding_model = embedding_model - self.embedding_dimensions = embedding_dimensions - self.chatgpt_deployment = chatgpt_deployment - self.embedding_deployment = embedding_deployment - self.sourcepage_field = sourcepage_field - self.content_field = content_field - self.query_language = query_language - self.query_speller = query_speller - self.chatgpt_token_limit = get_token_limit(chatgpt_model) - - async def run( - self, - messages: list[ChatCompletionMessageParam], - session_state: Any = None, - context: dict[str, Any] = {}, - ) -> dict[str, Any]: - q = messages[-1]["content"] - if not isinstance(q, str): - raise ValueError("The most recent message content must be a string.") - overrides = context.get("overrides", {}) - auth_claims = context.get("auth_claims", {}) - use_text_search = overrides.get("retrieval_mode") in ["text", "hybrid", None] - use_vector_search = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] - use_semantic_ranker = True if overrides.get("semantic_ranker") else False - use_semantic_captions = True if overrides.get("semantic_captions") else False - top = overrides.get("top", 3) - minimum_search_score = overrides.get("minimum_search_score", 0.0) - minimum_reranker_score = overrides.get("minimum_reranker_score", 0.0) - filter = self.build_filter(overrides, auth_claims) - - # If retrieval mode includes vectors, compute an embedding for the query - vectors: list[VectorQuery] = [] - if use_vector_search: - vectors.append(await self.compute_text_embedding(q)) - - results = await self.search( - top, - q, - filter, - vectors, - use_text_search, - use_vector_search, - use_semantic_ranker, - use_semantic_captions, - minimum_search_score, - minimum_reranker_score, - ) - - # Process results - sources_content = self.get_sources_content(results, use_semantic_captions, use_image_citation=False) - - # Append user message - content = "\n".join(sources_content) - user_content = q + "\n" + f"Sources:\n {content}" - - response_token_limit = 1024 - updated_messages = build_messages( - model=self.chatgpt_model, - system_prompt=overrides.get("prompt_template", self.system_chat_template), - few_shots=[{"role": "user", "content": self.question}, {"role": "assistant", "content": self.answer}], - new_user_content=user_content, - max_tokens=self.chatgpt_token_limit - response_token_limit, - ) - - chat_completion = ( - await self.openai_client.chat.completions.create( - # Azure OpenAI takes the deployment name as the model name - model=self.chatgpt_deployment if self.chatgpt_deployment else self.chatgpt_model, - messages=updated_messages, - temperature=overrides.get("temperature", 0.3), - max_tokens=response_token_limit, - n=1, - ) - ).model_dump() - - data_points = {"text": sources_content} - extra_info = { - "data_points": data_points, - "thoughts": [ - ThoughtStep( - "Search using user query", - q, - { - "use_semantic_captions": use_semantic_captions, - "use_semantic_ranker": use_semantic_ranker, - "top": top, - "filter": filter, - "use_vector_search": use_vector_search, - "use_text_search": use_text_search, - }, - ), - ThoughtStep( - "Search results", - [result.serialize_for_results() for result in results], - ), - ThoughtStep( - "Prompt to generate answer", - [str(message) for message in updated_messages], - ( - {"model": self.chatgpt_model, "deployment": self.chatgpt_deployment} - if self.chatgpt_deployment - else {"model": self.chatgpt_model} - ), - ), - ], - } - - completion = {} - completion["message"] = chat_completion["choices"][0]["message"] - completion["context"] = extra_info - completion["session_state"] = session_state - return completion +from typing import Any, Optional + +from azure.search.documents.aio import SearchClient +from azure.search.documents.models import VectorQuery +from openai import AsyncOpenAI +from openai.types.chat import ChatCompletionMessageParam +from openai_messages_token_helper import build_messages, get_token_limit + +from approaches.approach import Approach, ThoughtStep +from core.authentication import AuthenticationHelper + + +class RetrieveThenReadApproach(Approach): + """ + Simple retrieve-then-read implementation, using the AI Search and OpenAI APIs directly. It first retrieves + top documents from search, then constructs a prompt with them, and then uses OpenAI to generate an completion + (answer) with that prompt. + """ + + system_chat_template = ( + "You are an intelligent assistant helping health facilities and health professionals understand their policies and regulations as health entities licensed by the DHA." + + "Use 'you' to refer to the individual asking the questions even if they ask with 'I'. " + + "Answer the following question using the data provided in all available indexed sources. " + + "For tabular information return it as an html table. Do not return markdown format. " + + "Each source has a name followed by colon and the actual information, always include the source name for each fact you use in the response. " + + "If you cannot answer using the available sources, say you don't know. Use the example below to answer." + ) + + # shots/sample conversation + question = """ +'What are the main requirements for outpatient care facilities to ensure patient safety and quality of care according to DHA regulations? + +Sources: +info1.pdf: Main requirements include maintaining accurate and secure patient records, adhering to stringent infection control measures, ensuring healthcare professionals meet licensure and competency standards, and complying with facility design and operational guidelines. +info2.pdf: Regular inspections and compliance reviews are conducted to ensure ongoing adherence to DHA standards. +""" + answer = "Main requirements include maintaining accurate and secure patient records, adhering to stringent infection control measures, ensuring healthcare professionals meet licensure and competency standards, and complying with facility design and operational guidelines. Regular inspections and compliance reviews are conducted to ensure ongoing adherence to DHA standards [info1.pdf][info2.pdf]." + + def __init__( + self, + *, + search_client: SearchClient, + auth_helper: AuthenticationHelper, + openai_client: AsyncOpenAI, + chatgpt_model: str, + chatgpt_deployment: Optional[str], # Not needed for non-Azure OpenAI + embedding_model: str, + embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text" + embedding_dimensions: int, + sourcepage_field: str, + content_field: str, + query_language: str, + query_speller: str, + ): + self.search_client = search_client + self.chatgpt_deployment = chatgpt_deployment + self.openai_client = openai_client + self.auth_helper = auth_helper + self.chatgpt_model = chatgpt_model + self.embedding_model = embedding_model + self.embedding_dimensions = embedding_dimensions + self.chatgpt_deployment = chatgpt_deployment + self.embedding_deployment = embedding_deployment + self.sourcepage_field = sourcepage_field + self.content_field = content_field + self.query_language = query_language + self.query_speller = query_speller + self.chatgpt_token_limit = get_token_limit(chatgpt_model) + + async def run( + self, + messages: list[ChatCompletionMessageParam], + session_state: Any = None, + context: dict[str, Any] = {}, + ) -> dict[str, Any]: + q = messages[-1]["content"] + if not isinstance(q, str): + raise ValueError("The most recent message content must be a string.") + overrides = context.get("overrides", {}) + auth_claims = context.get("auth_claims", {}) + use_text_search = overrides.get("retrieval_mode") in ["text", "hybrid", None] + use_vector_search = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] + use_semantic_ranker = True if overrides.get("semantic_ranker") else False + use_semantic_captions = True if overrides.get("semantic_captions") else False + top = overrides.get("top", 3) + minimum_search_score = overrides.get("minimum_search_score", 0.0) + minimum_reranker_score = overrides.get("minimum_reranker_score", 0.0) + filter = self.build_filter(overrides, auth_claims) + + # If retrieval mode includes vectors, compute an embedding for the query + vectors: list[VectorQuery] = [] + if use_vector_search: + vectors.append(await self.compute_text_embedding(q)) + + results = await self.search( + top, + q, + filter, + vectors, + use_text_search, + use_vector_search, + use_semantic_ranker, + use_semantic_captions, + minimum_search_score, + minimum_reranker_score, + ) + + # Process results + sources_content = self.get_sources_content(results, use_semantic_captions, use_image_citation=False) + + # Append user message + content = "\n".join(sources_content) + user_content = q + "\n" + f"Sources:\n {content}" + + response_token_limit = 1024 + updated_messages = build_messages( + model=self.chatgpt_model, + system_prompt=overrides.get("prompt_template", self.system_chat_template), + few_shots=[{"role": "user", "content": self.question}, {"role": "assistant", "content": self.answer}], + new_user_content=user_content, + max_tokens=self.chatgpt_token_limit - response_token_limit, + ) + + chat_completion = ( + await self.openai_client.chat.completions.create( + # Azure OpenAI takes the deployment name as the model name + model=self.chatgpt_deployment if self.chatgpt_deployment else self.chatgpt_model, + messages=updated_messages, + temperature=overrides.get("temperature", 0.3), + max_tokens=response_token_limit, + n=1, + ) + ).model_dump() + + data_points = {"text": sources_content} + extra_info = { + "data_points": data_points, + "thoughts": [ + ThoughtStep( + "Search using user query", + q, + { + "use_semantic_captions": use_semantic_captions, + "use_semantic_ranker": use_semantic_ranker, + "top": top, + "filter": filter, + "use_vector_search": use_vector_search, + "use_text_search": use_text_search, + }, + ), + ThoughtStep( + "Search results", + [result.serialize_for_results() for result in results], + ), + ThoughtStep( + "Prompt to generate answer", + [str(message) for message in updated_messages], + ( + {"model": self.chatgpt_model, "deployment": self.chatgpt_deployment} + if self.chatgpt_deployment + else {"model": self.chatgpt_model} + ), + ), + ], + } + + completion = {} + completion["message"] = chat_completion["choices"][0]["message"] + completion["context"] = extra_info + completion["session_state"] = session_state + return completion diff --git a/app/backend/approaches/retrievethenreadvision.py b/app/backend/approaches/retrievethenreadvision.py index b4b4fb85e9..706ec76974 100644 --- a/app/backend/approaches/retrievethenreadvision.py +++ b/app/backend/approaches/retrievethenreadvision.py @@ -1,195 +1,195 @@ -from typing import Any, Awaitable, Callable, Optional - -from azure.search.documents.aio import SearchClient -from azure.storage.blob.aio import ContainerClient -from openai import AsyncOpenAI -from openai.types.chat import ( - ChatCompletionContentPartImageParam, - ChatCompletionContentPartParam, - ChatCompletionMessageParam, -) -from openai_messages_token_helper import build_messages, get_token_limit - -from approaches.approach import Approach, ThoughtStep -from core.authentication import AuthenticationHelper -from core.imageshelper import fetch_image - - -class RetrieveThenReadVisionApproach(Approach): - """ - Simple retrieve-then-read implementation, using the AI Search and OpenAI APIs directly. It first retrieves - top documents including images from search, then constructs a prompt with them, and then uses OpenAI to generate an completion - (answer) with that prompt. - """ - - system_chat_template_gpt4v = ( - "You are an intelligent assistant helping analyze the Annual Financial Report of Contoso Ltd., The documents contain text, graphs, tables and images. " - + "Each image source has the file name in the top left corner of the image with coordinates (10,10) pixels and is in the format SourceFileName: " - + "Each text source starts in a new line and has the file name followed by colon and the actual information " - + "Always include the source name from the image or text for each fact you use in the response in the format: [filename] " - + "Answer the following question using only the data provided in the sources below. " - + "For tabular information return it as an html table. Do not return markdown format. " - + "The text and image source can be the same file name, don't use the image title when citing the image source, only use the file name as mentioned " - + "If you cannot answer using the sources below, say you don't know. Return just the answer without any input texts " - ) - - def __init__( - self, - *, - search_client: SearchClient, - blob_container_client: ContainerClient, - openai_client: AsyncOpenAI, - auth_helper: AuthenticationHelper, - gpt4v_deployment: Optional[str], - gpt4v_model: str, - embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text" - embedding_model: str, - embedding_dimensions: int, - sourcepage_field: str, - content_field: str, - query_language: str, - query_speller: str, - vision_endpoint: str, - vision_token_provider: Callable[[], Awaitable[str]] - ): - self.search_client = search_client - self.blob_container_client = blob_container_client - self.openai_client = openai_client - self.auth_helper = auth_helper - self.embedding_model = embedding_model - self.embedding_deployment = embedding_deployment - self.embedding_dimensions = embedding_dimensions - self.sourcepage_field = sourcepage_field - self.content_field = content_field - self.gpt4v_deployment = gpt4v_deployment - self.gpt4v_model = gpt4v_model - self.query_language = query_language - self.query_speller = query_speller - self.vision_endpoint = vision_endpoint - self.vision_token_provider = vision_token_provider - self.gpt4v_token_limit = get_token_limit(gpt4v_model) - - async def run( - self, - messages: list[ChatCompletionMessageParam], - session_state: Any = None, - context: dict[str, Any] = {}, - ) -> dict[str, Any]: - q = messages[-1]["content"] - if not isinstance(q, str): - raise ValueError("The most recent message content must be a string.") - - overrides = context.get("overrides", {}) - auth_claims = context.get("auth_claims", {}) - use_text_search = overrides.get("retrieval_mode") in ["text", "hybrid", None] - use_vector_search = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] - use_semantic_ranker = True if overrides.get("semantic_ranker") else False - use_semantic_captions = True if overrides.get("semantic_captions") else False - top = overrides.get("top", 3) - minimum_search_score = overrides.get("minimum_search_score", 0.0) - minimum_reranker_score = overrides.get("minimum_reranker_score", 0.0) - filter = self.build_filter(overrides, auth_claims) - - vector_fields = overrides.get("vector_fields", ["embedding"]) - send_text_to_gptvision = overrides.get("gpt4v_input") in ["textAndImages", "texts", None] - send_images_to_gptvision = overrides.get("gpt4v_input") in ["textAndImages", "images", None] - - # If retrieval mode includes vectors, compute an embedding for the query - vectors = [] - if use_vector_search: - for field in vector_fields: - vector = ( - await self.compute_text_embedding(q) - if field == "embedding" - else await self.compute_image_embedding(q) - ) - vectors.append(vector) - - results = await self.search( - top, - q, - filter, - vectors, - use_text_search, - use_vector_search, - use_semantic_ranker, - use_semantic_captions, - minimum_search_score, - minimum_reranker_score, - ) - - image_list: list[ChatCompletionContentPartImageParam] = [] - user_content: list[ChatCompletionContentPartParam] = [{"text": q, "type": "text"}] - - # Process results - sources_content = self.get_sources_content(results, use_semantic_captions, use_image_citation=True) - - if send_text_to_gptvision: - content = "\n".join(sources_content) - user_content.append({"text": content, "type": "text"}) - if send_images_to_gptvision: - for result in results: - url = await fetch_image(self.blob_container_client, result) - if url: - image_list.append({"image_url": url, "type": "image_url"}) - user_content.extend(image_list) - - response_token_limit = 1024 - updated_messages = build_messages( - model=self.gpt4v_model, - system_prompt=overrides.get("prompt_template", self.system_chat_template_gpt4v), - new_user_content=user_content, - max_tokens=self.gpt4v_token_limit - response_token_limit, - ) - chat_completion = ( - await self.openai_client.chat.completions.create( - model=self.gpt4v_deployment if self.gpt4v_deployment else self.gpt4v_model, - messages=updated_messages, - temperature=overrides.get("temperature", 0.3), - max_tokens=response_token_limit, - n=1, - ) - ).model_dump() - - data_points = { - "text": sources_content, - "images": [d["image_url"] for d in image_list], - } - - extra_info = { - "data_points": data_points, - "thoughts": [ - ThoughtStep( - "Search using user query", - q, - { - "use_semantic_captions": use_semantic_captions, - "use_semantic_ranker": use_semantic_ranker, - "top": top, - "filter": filter, - "vector_fields": vector_fields, - "use_vector_search": use_vector_search, - "use_text_search": use_text_search, - }, - ), - ThoughtStep( - "Search results", - [result.serialize_for_results() for result in results], - ), - ThoughtStep( - "Prompt to generate answer", - [str(message) for message in updated_messages], - ( - {"model": self.gpt4v_model, "deployment": self.gpt4v_deployment} - if self.gpt4v_deployment - else {"model": self.gpt4v_model} - ), - ), - ], - } - - completion = {} - completion["message"] = chat_completion["choices"][0]["message"] - completion["context"] = extra_info - completion["session_state"] = session_state - return completion +from typing import Any, Awaitable, Callable, Optional + +from azure.search.documents.aio import SearchClient +from azure.storage.blob.aio import ContainerClient +from openai import AsyncOpenAI +from openai.types.chat import ( + ChatCompletionContentPartImageParam, + ChatCompletionContentPartParam, + ChatCompletionMessageParam, +) +from openai_messages_token_helper import build_messages, get_token_limit + +from approaches.approach import Approach, ThoughtStep +from core.authentication import AuthenticationHelper +from core.imageshelper import fetch_image + + +class RetrieveThenReadVisionApproach(Approach): + """ + Simple retrieve-then-read implementation, using the AI Search and OpenAI APIs directly. It first retrieves + top documents including images from search, then constructs a prompt with them, and then uses OpenAI to generate an completion + (answer) with that prompt. + """ + + system_chat_template_gpt4v = ( + "You are an intelligent assistant helping health facilities and health professionals understand their policies and regulations as health entities licensed by the DHA. The documents contain text, graphs, tables and images. " + + "Each image source has the file name in the top left corner of the image with coordinates (10,10) pixels and is in the format SourceFileName: " + + "Each text source starts in a new line and has the file name followed by colon and the actual information " + + "Always include the source name from the image or text for each fact you use in the response in the format: [filename] " + + "Answer the following question using only the data provided in the sources below. " + + "For tabular information return it as an html table. Do not return markdown format. " + + "The text and image source can be the same file name, don't use the image title when citing the image source, only use the file name as mentioned " + + "If you cannot answer using the sources below, say you don't know. Return just the answer without any input texts " + ) + + def __init__( + self, + *, + search_client: SearchClient, + blob_container_client: ContainerClient, + openai_client: AsyncOpenAI, + auth_helper: AuthenticationHelper, + gpt4v_deployment: Optional[str], + gpt4v_model: str, + embedding_deployment: Optional[str], # Not needed for non-Azure OpenAI or for retrieval_mode="text" + embedding_model: str, + embedding_dimensions: int, + sourcepage_field: str, + content_field: str, + query_language: str, + query_speller: str, + vision_endpoint: str, + vision_token_provider: Callable[[], Awaitable[str]] + ): + self.search_client = search_client + self.blob_container_client = blob_container_client + self.openai_client = openai_client + self.auth_helper = auth_helper + self.embedding_model = embedding_model + self.embedding_deployment = embedding_deployment + self.embedding_dimensions = embedding_dimensions + self.sourcepage_field = sourcepage_field + self.content_field = content_field + self.gpt4v_deployment = gpt4v_deployment + self.gpt4v_model = gpt4v_model + self.query_language = query_language + self.query_speller = query_speller + self.vision_endpoint = vision_endpoint + self.vision_token_provider = vision_token_provider + self.gpt4v_token_limit = get_token_limit(gpt4v_model) + + async def run( + self, + messages: list[ChatCompletionMessageParam], + session_state: Any = None, + context: dict[str, Any] = {}, + ) -> dict[str, Any]: + q = messages[-1]["content"] + if not isinstance(q, str): + raise ValueError("The most recent message content must be a string.") + + overrides = context.get("overrides", {}) + auth_claims = context.get("auth_claims", {}) + use_text_search = overrides.get("retrieval_mode") in ["text", "hybrid", None] + use_vector_search = overrides.get("retrieval_mode") in ["vectors", "hybrid", None] + use_semantic_ranker = True if overrides.get("semantic_ranker") else False + use_semantic_captions = True if overrides.get("semantic_captions") else False + top = overrides.get("top", 3) + minimum_search_score = overrides.get("minimum_search_score", 0.0) + minimum_reranker_score = overrides.get("minimum_reranker_score", 0.0) + filter = self.build_filter(overrides, auth_claims) + + vector_fields = overrides.get("vector_fields", ["embedding"]) + send_text_to_gptvision = overrides.get("gpt4v_input") in ["textAndImages", "texts", None] + send_images_to_gptvision = overrides.get("gpt4v_input") in ["textAndImages", "images", None] + + # If retrieval mode includes vectors, compute an embedding for the query + vectors = [] + if use_vector_search: + for field in vector_fields: + vector = ( + await self.compute_text_embedding(q) + if field == "embedding" + else await self.compute_image_embedding(q) + ) + vectors.append(vector) + + results = await self.search( + top, + q, + filter, + vectors, + use_text_search, + use_vector_search, + use_semantic_ranker, + use_semantic_captions, + minimum_search_score, + minimum_reranker_score, + ) + + image_list: list[ChatCompletionContentPartImageParam] = [] + user_content: list[ChatCompletionContentPartParam] = [{"text": q, "type": "text"}] + + # Process results + sources_content = self.get_sources_content(results, use_semantic_captions, use_image_citation=True) + + if send_text_to_gptvision: + content = "\n".join(sources_content) + user_content.append({"text": content, "type": "text"}) + if send_images_to_gptvision: + for result in results: + url = await fetch_image(self.blob_container_client, result) + if url: + image_list.append({"image_url": url, "type": "image_url"}) + user_content.extend(image_list) + + response_token_limit = 1024 + updated_messages = build_messages( + model=self.gpt4v_model, + system_prompt=overrides.get("prompt_template", self.system_chat_template_gpt4v), + new_user_content=user_content, + max_tokens=self.gpt4v_token_limit - response_token_limit, + ) + chat_completion = ( + await self.openai_client.chat.completions.create( + model=self.gpt4v_deployment if self.gpt4v_deployment else self.gpt4v_model, + messages=updated_messages, + temperature=overrides.get("temperature", 0.3), + max_tokens=response_token_limit, + n=1, + ) + ).model_dump() + + data_points = { + "text": sources_content, + "images": [d["image_url"] for d in image_list], + } + + extra_info = { + "data_points": data_points, + "thoughts": [ + ThoughtStep( + "Search using user query", + q, + { + "use_semantic_captions": use_semantic_captions, + "use_semantic_ranker": use_semantic_ranker, + "top": top, + "filter": filter, + "vector_fields": vector_fields, + "use_vector_search": use_vector_search, + "use_text_search": use_text_search, + }, + ), + ThoughtStep( + "Search results", + [result.serialize_for_results() for result in results], + ), + ThoughtStep( + "Prompt to generate answer", + [str(message) for message in updated_messages], + ( + {"model": self.gpt4v_model, "deployment": self.gpt4v_deployment} + if self.gpt4v_deployment + else {"model": self.gpt4v_model} + ), + ), + ], + } + + completion = {} + completion["message"] = chat_completion["choices"][0]["message"] + completion["context"] = extra_info + completion["session_state"] = session_state + return completion diff --git a/app/backend/config.py b/app/backend/config.py index da076bad1d..830177cdcd 100644 --- a/app/backend/config.py +++ b/app/backend/config.py @@ -1,23 +1,23 @@ -CONFIG_OPENAI_TOKEN = "openai_token" -CONFIG_CREDENTIAL = "azure_credential" -CONFIG_ASK_APPROACH = "ask_approach" -CONFIG_ASK_VISION_APPROACH = "ask_vision_approach" -CONFIG_CHAT_VISION_APPROACH = "chat_vision_approach" -CONFIG_CHAT_APPROACH = "chat_approach" -CONFIG_BLOB_CONTAINER_CLIENT = "blob_container_client" -CONFIG_USER_UPLOAD_ENABLED = "user_upload_enabled" -CONFIG_USER_BLOB_CONTAINER_CLIENT = "user_blob_container_client" -CONFIG_AUTH_CLIENT = "auth_client" -CONFIG_GPT4V_DEPLOYED = "gpt4v_deployed" -CONFIG_SEMANTIC_RANKER_DEPLOYED = "semantic_ranker_deployed" -CONFIG_VECTOR_SEARCH_ENABLED = "vector_search_enabled" -CONFIG_SEARCH_CLIENT = "search_client" -CONFIG_OPENAI_CLIENT = "openai_client" -CONFIG_INGESTER = "ingester" -CONFIG_SPEECH_INPUT_ENABLED = "speech_input_enabled" -CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED = "speech_output_browser_enabled" -CONFIG_SPEECH_OUTPUT_AZURE_ENABLED = "speech_output_azure_enabled" -CONFIG_SPEECH_SERVICE_ID = "speech_service_id" -CONFIG_SPEECH_SERVICE_LOCATION = "speech_service_location" -CONFIG_SPEECH_SERVICE_TOKEN = "speech_service_token" -CONFIG_SPEECH_SERVICE_VOICE = "speech_service_voice" +CONFIG_OPENAI_TOKEN = "openai_token" +CONFIG_CREDENTIAL = "azure_credential" +CONFIG_ASK_APPROACH = "ask_approach" +CONFIG_ASK_VISION_APPROACH = "ask_vision_approach" +CONFIG_CHAT_VISION_APPROACH = "chat_vision_approach" +CONFIG_CHAT_APPROACH = "chat_approach" +CONFIG_BLOB_CONTAINER_CLIENT = "blob_container_client" +CONFIG_USER_UPLOAD_ENABLED = "user_upload_enabled" +CONFIG_USER_BLOB_CONTAINER_CLIENT = "user_blob_container_client" +CONFIG_AUTH_CLIENT = "auth_client" +CONFIG_GPT4V_DEPLOYED = "gpt4v_deployed" +CONFIG_SEMANTIC_RANKER_DEPLOYED = "semantic_ranker_deployed" +CONFIG_VECTOR_SEARCH_ENABLED = "vector_search_enabled" +CONFIG_SEARCH_CLIENT = "search_client" +CONFIG_OPENAI_CLIENT = "openai_client" +CONFIG_INGESTER = "ingester" +CONFIG_SPEECH_INPUT_ENABLED = "speech_input_enabled" +CONFIG_SPEECH_OUTPUT_BROWSER_ENABLED = "speech_output_browser_enabled" +CONFIG_SPEECH_OUTPUT_AZURE_ENABLED = "speech_output_azure_enabled" +CONFIG_SPEECH_SERVICE_ID = "speech_service_id" +CONFIG_SPEECH_SERVICE_LOCATION = "speech_service_location" +CONFIG_SPEECH_SERVICE_TOKEN = "speech_service_token" +CONFIG_SPEECH_SERVICE_VOICE = "speech_service_voice" diff --git a/app/backend/core/authentication.py b/app/backend/core/authentication.py index 5d299f58f9..f15aee5936 100644 --- a/app/backend/core/authentication.py +++ b/app/backend/core/authentication.py @@ -1,354 +1,354 @@ -# Refactored from https://github.com/Azure-Samples/ms-identity-python-on-behalf-of - -import json -import logging -from typing import Any, Optional - -import aiohttp -from azure.search.documents.aio import SearchClient -from azure.search.documents.indexes.models import SearchIndex -from jose import jwt -from jose.exceptions import ExpiredSignatureError, JWTClaimsError -from msal import ConfidentialClientApplication -from msal.token_cache import TokenCache -from tenacity import ( - AsyncRetrying, - retry_if_exception_type, - stop_after_attempt, - wait_random_exponential, -) - - -# AuthError is raised when the authentication token sent by the client UI cannot be parsed or there is an authentication error accessing the graph API -class AuthError(Exception): - def __init__(self, error, status_code): - self.error = error - self.status_code = status_code - - def __str__(self) -> str: - return self.error or "" - - -class AuthenticationHelper: - scope: str = "https://graph.microsoft.com/.default" - - def __init__( - self, - search_index: Optional[SearchIndex], - use_authentication: bool, - server_app_id: Optional[str], - server_app_secret: Optional[str], - client_app_id: Optional[str], - tenant_id: Optional[str], - require_access_control: bool = False, - enable_global_documents: bool = False, - enable_unauthenticated_access: bool = False, - ): - self.use_authentication = use_authentication - self.server_app_id = server_app_id - self.server_app_secret = server_app_secret - self.client_app_id = client_app_id - self.tenant_id = tenant_id - self.authority = f"https://login.microsoftonline.com/{tenant_id}" - # Depending on if requestedAccessTokenVersion is 1 or 2, the issuer and audience of the token may be different - # See https://learn.microsoft.com/graph/api/resources/apiapplication - self.valid_issuers = [ - f"https://sts.windows.net/{tenant_id}/", - f"https://login.microsoftonline.com/{tenant_id}/v2.0", - ] - self.valid_audiences = [f"api://{server_app_id}", str(server_app_id)] - # See https://learn.microsoft.com/entra/identity-platform/access-tokens#validate-the-issuer for more information on token validation - self.key_url = f"{self.authority}/discovery/v2.0/keys" - - if self.use_authentication: - field_names = [field.name for field in search_index.fields] if search_index else [] - self.has_auth_fields = "oids" in field_names and "groups" in field_names - self.require_access_control = require_access_control - self.enable_global_documents = enable_global_documents - self.enable_unauthenticated_access = enable_unauthenticated_access - self.confidential_client = ConfidentialClientApplication( - server_app_id, authority=self.authority, client_credential=server_app_secret, token_cache=TokenCache() - ) - else: - self.has_auth_fields = False - self.require_access_control = False - self.enable_global_documents = True - self.enable_unauthenticated_access = True - - def get_auth_setup_for_client(self) -> dict[str, Any]: - # returns MSAL.js settings used by the client app - return { - "useLogin": self.use_authentication, # Whether or not login elements are enabled on the UI - "requireAccessControl": self.require_access_control, # Whether or not access control is required to access documents with access control lists - "enableUnauthenticatedAccess": self.enable_unauthenticated_access, # Whether or not the user can access the app without login - "msalConfig": { - "auth": { - "clientId": self.client_app_id, # Client app id used for login - "authority": self.authority, # Directory to use for login https://learn.microsoft.com/entra/identity-platform/msal-client-application-configuration#authority - "redirectUri": "/redirect", # Points to window.location.origin. You must register this URI on Azure Portal/App Registration. - "postLogoutRedirectUri": "/", # Indicates the page to navigate after logout. - "navigateToLoginRequestUrl": False, # If "true", will navigate back to the original request location before processing the auth code response. - }, - "cache": { - # Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs. - "cacheLocation": "localStorage", - # Set this to "true" if you are having issues on IE11 or Edge - "storeAuthStateInCookie": False, - }, - }, - "loginRequest": { - # Scopes you add here will be prompted for user consent during sign-in. - # By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. - # For more information about OIDC scopes, visit: - # https://learn.microsoft.com/entra/identity-platform/permissions-consent-overview#openid-connect-scopes - "scopes": [".default"], - # Uncomment the following line to cause a consent dialog to appear on every login - # For more information, please visit https://learn.microsoft.com/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-authorization-code - # "prompt": "consent" - }, - "tokenRequest": { - "scopes": [f"api://{self.server_app_id}/access_as_user"], - }, - } - - @staticmethod - def get_token_auth_header(headers: dict) -> str: - # Obtains the Access Token from the Authorization Header - auth = headers.get("Authorization") - if auth: - parts = auth.split() - - if parts[0].lower() != "bearer": - raise AuthError(error="Authorization header must start with Bearer", status_code=401) - elif len(parts) == 1: - raise AuthError(error="Token not found", status_code=401) - elif len(parts) > 2: - raise AuthError(error="Authorization header must be Bearer token", status_code=401) - - token = parts[1] - return token - - # App services built-in authentication passes the access token directly as a header - # To learn more, please visit https://learn.microsoft.com/azure/app-service/configure-authentication-oauth-tokens - token = headers.get("x-ms-token-aad-access-token") - if token: - return token - - raise AuthError(error="Authorization header is expected", status_code=401) - - def build_security_filters(self, overrides: dict[str, Any], auth_claims: dict[str, Any]): - # Build different permutations of the oid or groups security filter using OData filters - # https://learn.microsoft.com/azure/search/search-security-trimming-for-azure-search - # https://learn.microsoft.com/azure/search/search-query-odata-filter - use_oid_security_filter = self.require_access_control or overrides.get("use_oid_security_filter") - use_groups_security_filter = self.require_access_control or overrides.get("use_groups_security_filter") - - if (use_oid_security_filter or use_groups_security_filter) and not self.has_auth_fields: - raise AuthError( - error="oids and groups must be defined in the search index to use authentication", status_code=400 - ) - - oid_security_filter = ( - "oids/any(g:search.in(g, '{}'))".format(auth_claims.get("oid", "")) if use_oid_security_filter else None - ) - groups_security_filter = ( - "groups/any(g:search.in(g, '{}'))".format(", ".join(auth_claims.get("groups", []))) - if use_groups_security_filter - else None - ) - - # If only one security filter is specified, use that filter - # If both security filters are specified, combine them with "or" so only 1 security filter needs to pass - # If no security filters are specified, don't return any filter - security_filter = None - if oid_security_filter and not groups_security_filter: - security_filter = f"{oid_security_filter}" - elif groups_security_filter and not oid_security_filter: - security_filter = f"{groups_security_filter}" - elif oid_security_filter and groups_security_filter: - security_filter = f"({oid_security_filter} or {groups_security_filter})" - - # If global documents are allowed, append the public global filter - if self.enable_global_documents: - global_documents_filter = "(not oids/any() and not groups/any())" - if security_filter: - security_filter = f"({security_filter} or {global_documents_filter})" - - return security_filter - - @staticmethod - async def list_groups(graph_resource_access_token: dict) -> list[str]: - headers = {"Authorization": "Bearer " + graph_resource_access_token["access_token"]} - groups = [] - async with aiohttp.ClientSession(headers=headers) as session: - resp_json = None - resp_status = None - async with session.get(url="https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$select=id") as resp: - resp_json = await resp.json() - resp_status = resp.status - if resp_status != 200: - raise AuthError(error=json.dumps(resp_json), status_code=resp_status) - - while resp_status == 200: - value = resp_json["value"] - for group in value: - groups.append(group["id"]) - next_link = resp_json.get("@odata.nextLink") - if next_link: - async with session.get(url=next_link) as resp: - resp_json = await resp.json() - resp_status = resp.status - else: - break - if resp_status != 200: - raise AuthError(error=json.dumps(resp_json), status_code=resp_status) - - return groups - - async def get_auth_claims_if_enabled(self, headers: dict) -> dict[str, Any]: - if not self.use_authentication: - return {} - try: - # Read the authentication token from the authorization header and exchange it using the On Behalf Of Flow - # The scope is set to the Microsoft Graph API, which may need to be called for more authorization information - # https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow - auth_token = AuthenticationHelper.get_token_auth_header(headers) - # Validate the token before use - await self.validate_access_token(auth_token) - - # Use the on-behalf-of-flow to acquire another token for use with Microsoft Graph - # See https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow for more information - graph_resource_access_token = self.confidential_client.acquire_token_on_behalf_of( - user_assertion=auth_token, scopes=["https://graph.microsoft.com/.default"] - ) - if "error" in graph_resource_access_token: - raise AuthError(error=str(graph_resource_access_token), status_code=401) - - # Read the claims from the response. The oid and groups claims are used for security filtering - # https://learn.microsoft.com/entra/identity-platform/id-token-claims-reference - id_token_claims = graph_resource_access_token["id_token_claims"] - auth_claims = {"oid": id_token_claims["oid"], "groups": id_token_claims.get("groups", [])} - - # A groups claim may have been omitted either because it was not added in the application manifest for the API application, - # or a groups overage claim may have been emitted. - # https://learn.microsoft.com/entra/identity-platform/id-token-claims-reference#groups-overage-claim - missing_groups_claim = "groups" not in id_token_claims - has_group_overage_claim = ( - missing_groups_claim - and "_claim_names" in id_token_claims - and "groups" in id_token_claims["_claim_names"] - ) - if missing_groups_claim or has_group_overage_claim: - # Read the user's groups from Microsoft Graph - auth_claims["groups"] = await AuthenticationHelper.list_groups(graph_resource_access_token) - return auth_claims - except AuthError as e: - logging.exception("Exception getting authorization information - " + json.dumps(e.error)) - if self.require_access_control and not self.enable_unauthenticated_access: - raise - return {} - except Exception: - logging.exception("Exception getting authorization information") - if self.require_access_control and not self.enable_unauthenticated_access: - raise - return {} - - async def check_path_auth(self, path: str, auth_claims: dict[str, Any], search_client: SearchClient) -> bool: - # Start with the standard security filter for all queries - security_filter = self.build_security_filters(overrides={}, auth_claims=auth_claims) - # If there was no security filter or no path, then the path is allowed - if not security_filter or len(path) == 0: - return True - - # Remove any fragment string from the path before checking - fragment_index = path.find("#") - if fragment_index != -1: - path = path[:fragment_index] - - # Filter down to only chunks that are from the specific source file - # Sourcepage is used for GPT-4V - # Replace ' with '' to escape the single quote for the filter - # https://learn.microsoft.com/azure/search/query-odata-filter-orderby-syntax#escaping-special-characters-in-string-constants - path_for_filter = path.replace("'", "''") - filter = f"{security_filter} and ((sourcefile eq '{path_for_filter}') or (sourcepage eq '{path_for_filter}'))" - - # If the filter returns any results, the user is allowed to access the document - # Otherwise, access is denied - results = await search_client.search(search_text="*", top=1, filter=filter) - allowed = False - async for _ in results: - allowed = True - break - - return allowed - - # See https://github.com/Azure-Samples/ms-identity-python-on-behalf-of/blob/939be02b11f1604814532fdacc2c2eccd198b755/FlaskAPI/helpers/authorization.py#L44 - async def validate_access_token(self, token: str): - """ - Validate an access token is issued by Entra - """ - jwks = None - async for attempt in AsyncRetrying( - retry=retry_if_exception_type(AuthError), - wait=wait_random_exponential(min=15, max=60), - stop=stop_after_attempt(5), - ): - with attempt: - async with aiohttp.ClientSession() as session: - async with session.get(url=self.key_url) as resp: - resp_status = resp.status - if resp_status in [500, 502, 503, 504]: - raise AuthError( - error=f"Failed to get keys info: {await resp.text()}", status_code=resp_status - ) - jwks = await resp.json() - - if not jwks or "keys" not in jwks: - raise AuthError({"code": "invalid_keys", "description": "Unable to get keys to validate auth token."}, 401) - - rsa_key = None - issuer = None - audience = None - try: - unverified_header = jwt.get_unverified_header(token) - unverified_claims = jwt.get_unverified_claims(token) - issuer = unverified_claims.get("iss") - audience = unverified_claims.get("aud") - for key in jwks["keys"]: - if key["kid"] == unverified_header["kid"]: - rsa_key = {"kty": key["kty"], "kid": key["kid"], "use": key["use"], "n": key["n"], "e": key["e"]} - break - except Exception as exc: - raise AuthError( - {"code": "invalid_header", "description": "Unable to parse authorization token."}, 401 - ) from exc - if not rsa_key: - raise AuthError({"code": "invalid_header", "description": "Unable to find appropriate key"}, 401) - - if issuer not in self.valid_issuers: - raise AuthError( - {"code": "invalid_header", "description": f"Issuer {issuer} not in {','.join(self.valid_issuers)}"}, 401 - ) - - if audience not in self.valid_audiences: - raise AuthError( - { - "code": "invalid_header", - "description": f"Audience {audience} not in {','.join(self.valid_audiences)}", - }, - 401, - ) - - try: - jwt.decode(token, rsa_key, algorithms=["RS256"], audience=audience, issuer=issuer) - except ExpiredSignatureError as jwt_expired_exc: - raise AuthError({"code": "token_expired", "description": "token is expired"}, 401) from jwt_expired_exc - except JWTClaimsError as jwt_claims_exc: - raise AuthError( - {"code": "invalid_claims", "description": "incorrect claims," "please check the audience and issuer"}, - 401, - ) from jwt_claims_exc - except Exception as exc: - raise AuthError( - {"code": "invalid_header", "description": "Unable to parse authorization token."}, 401 - ) from exc +# Refactored from https://github.com/Azure-Samples/ms-identity-python-on-behalf-of + +import json +import logging +from typing import Any, Optional + +import aiohttp +from azure.search.documents.aio import SearchClient +from azure.search.documents.indexes.models import SearchIndex +from jose import jwt +from jose.exceptions import ExpiredSignatureError, JWTClaimsError +from msal import ConfidentialClientApplication +from msal.token_cache import TokenCache +from tenacity import ( + AsyncRetrying, + retry_if_exception_type, + stop_after_attempt, + wait_random_exponential, +) + + +# AuthError is raised when the authentication token sent by the client UI cannot be parsed or there is an authentication error accessing the graph API +class AuthError(Exception): + def __init__(self, error, status_code): + self.error = error + self.status_code = status_code + + def __str__(self) -> str: + return self.error or "" + + +class AuthenticationHelper: + scope: str = "https://graph.microsoft.com/.default" + + def __init__( + self, + search_index: Optional[SearchIndex], + use_authentication: bool, + server_app_id: Optional[str], + server_app_secret: Optional[str], + client_app_id: Optional[str], + tenant_id: Optional[str], + require_access_control: bool = False, + enable_global_documents: bool = False, + enable_unauthenticated_access: bool = False, + ): + self.use_authentication = use_authentication + self.server_app_id = server_app_id + self.server_app_secret = server_app_secret + self.client_app_id = client_app_id + self.tenant_id = tenant_id + self.authority = f"https://login.microsoftonline.com/{tenant_id}" + # Depending on if requestedAccessTokenVersion is 1 or 2, the issuer and audience of the token may be different + # See https://learn.microsoft.com/graph/api/resources/apiapplication + self.valid_issuers = [ + f"https://sts.windows.net/{tenant_id}/", + f"https://login.microsoftonline.com/{tenant_id}/v2.0", + ] + self.valid_audiences = [f"api://{server_app_id}", str(server_app_id)] + # See https://learn.microsoft.com/entra/identity-platform/access-tokens#validate-the-issuer for more information on token validation + self.key_url = f"{self.authority}/discovery/v2.0/keys" + + if self.use_authentication: + field_names = [field.name for field in search_index.fields] if search_index else [] + self.has_auth_fields = "oids" in field_names and "groups" in field_names + self.require_access_control = require_access_control + self.enable_global_documents = enable_global_documents + self.enable_unauthenticated_access = enable_unauthenticated_access + self.confidential_client = ConfidentialClientApplication( + server_app_id, authority=self.authority, client_credential=server_app_secret, token_cache=TokenCache() + ) + else: + self.has_auth_fields = False + self.require_access_control = False + self.enable_global_documents = True + self.enable_unauthenticated_access = True + + def get_auth_setup_for_client(self) -> dict[str, Any]: + # returns MSAL.js settings used by the client app + return { + "useLogin": self.use_authentication, # Whether or not login elements are enabled on the UI + "requireAccessControl": self.require_access_control, # Whether or not access control is required to access documents with access control lists + "enableUnauthenticatedAccess": self.enable_unauthenticated_access, # Whether or not the user can access the app without login + "msalConfig": { + "auth": { + "clientId": self.client_app_id, # Client app id used for login + "authority": self.authority, # Directory to use for login https://learn.microsoft.com/entra/identity-platform/msal-client-application-configuration#authority + "redirectUri": "/redirect", # Points to window.location.origin. You must register this URI on Azure Portal/App Registration. + "postLogoutRedirectUri": "/", # Indicates the page to navigate after logout. + "navigateToLoginRequestUrl": False, # If "true", will navigate back to the original request location before processing the auth code response. + }, + "cache": { + # Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs. + "cacheLocation": "localStorage", + # Set this to "true" if you are having issues on IE11 or Edge + "storeAuthStateInCookie": False, + }, + }, + "loginRequest": { + # Scopes you add here will be prompted for user consent during sign-in. + # By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. + # For more information about OIDC scopes, visit: + # https://learn.microsoft.com/entra/identity-platform/permissions-consent-overview#openid-connect-scopes + "scopes": [".default"], + # Uncomment the following line to cause a consent dialog to appear on every login + # For more information, please visit https://learn.microsoft.com/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-authorization-code + # "prompt": "consent" + }, + "tokenRequest": { + "scopes": [f"api://{self.server_app_id}/access_as_user"], + }, + } + + @staticmethod + def get_token_auth_header(headers: dict) -> str: + # Obtains the Access Token from the Authorization Header + auth = headers.get("Authorization") + if auth: + parts = auth.split() + + if parts[0].lower() != "bearer": + raise AuthError(error="Authorization header must start with Bearer", status_code=401) + elif len(parts) == 1: + raise AuthError(error="Token not found", status_code=401) + elif len(parts) > 2: + raise AuthError(error="Authorization header must be Bearer token", status_code=401) + + token = parts[1] + return token + + # App services built-in authentication passes the access token directly as a header + # To learn more, please visit https://learn.microsoft.com/azure/app-service/configure-authentication-oauth-tokens + token = headers.get("x-ms-token-aad-access-token") + if token: + return token + + raise AuthError(error="Authorization header is expected", status_code=401) + + def build_security_filters(self, overrides: dict[str, Any], auth_claims: dict[str, Any]): + # Build different permutations of the oid or groups security filter using OData filters + # https://learn.microsoft.com/azure/search/search-security-trimming-for-azure-search + # https://learn.microsoft.com/azure/search/search-query-odata-filter + use_oid_security_filter = self.require_access_control or overrides.get("use_oid_security_filter") + use_groups_security_filter = self.require_access_control or overrides.get("use_groups_security_filter") + + if (use_oid_security_filter or use_groups_security_filter) and not self.has_auth_fields: + raise AuthError( + error="oids and groups must be defined in the search index to use authentication", status_code=400 + ) + + oid_security_filter = ( + "oids/any(g:search.in(g, '{}'))".format(auth_claims.get("oid", "")) if use_oid_security_filter else None + ) + groups_security_filter = ( + "groups/any(g:search.in(g, '{}'))".format(", ".join(auth_claims.get("groups", []))) + if use_groups_security_filter + else None + ) + + # If only one security filter is specified, use that filter + # If both security filters are specified, combine them with "or" so only 1 security filter needs to pass + # If no security filters are specified, don't return any filter + security_filter = None + if oid_security_filter and not groups_security_filter: + security_filter = f"{oid_security_filter}" + elif groups_security_filter and not oid_security_filter: + security_filter = f"{groups_security_filter}" + elif oid_security_filter and groups_security_filter: + security_filter = f"({oid_security_filter} or {groups_security_filter})" + + # If global documents are allowed, append the public global filter + if self.enable_global_documents: + global_documents_filter = "(not oids/any() and not groups/any())" + if security_filter: + security_filter = f"({security_filter} or {global_documents_filter})" + + return security_filter + + @staticmethod + async def list_groups(graph_resource_access_token: dict) -> list[str]: + headers = {"Authorization": "Bearer " + graph_resource_access_token["access_token"]} + groups = [] + async with aiohttp.ClientSession(headers=headers) as session: + resp_json = None + resp_status = None + async with session.get(url="https://graph.microsoft.com/v1.0/me/transitiveMemberOf?$select=id") as resp: + resp_json = await resp.json() + resp_status = resp.status + if resp_status != 200: + raise AuthError(error=json.dumps(resp_json), status_code=resp_status) + + while resp_status == 200: + value = resp_json["value"] + for group in value: + groups.append(group["id"]) + next_link = resp_json.get("@odata.nextLink") + if next_link: + async with session.get(url=next_link) as resp: + resp_json = await resp.json() + resp_status = resp.status + else: + break + if resp_status != 200: + raise AuthError(error=json.dumps(resp_json), status_code=resp_status) + + return groups + + async def get_auth_claims_if_enabled(self, headers: dict) -> dict[str, Any]: + if not self.use_authentication: + return {} + try: + # Read the authentication token from the authorization header and exchange it using the On Behalf Of Flow + # The scope is set to the Microsoft Graph API, which may need to be called for more authorization information + # https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow + auth_token = AuthenticationHelper.get_token_auth_header(headers) + # Validate the token before use + await self.validate_access_token(auth_token) + + # Use the on-behalf-of-flow to acquire another token for use with Microsoft Graph + # See https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow for more information + graph_resource_access_token = self.confidential_client.acquire_token_on_behalf_of( + user_assertion=auth_token, scopes=["https://graph.microsoft.com/.default"] + ) + if "error" in graph_resource_access_token: + raise AuthError(error=str(graph_resource_access_token), status_code=401) + + # Read the claims from the response. The oid and groups claims are used for security filtering + # https://learn.microsoft.com/entra/identity-platform/id-token-claims-reference + id_token_claims = graph_resource_access_token["id_token_claims"] + auth_claims = {"oid": id_token_claims["oid"], "groups": id_token_claims.get("groups", [])} + + # A groups claim may have been omitted either because it was not added in the application manifest for the API application, + # or a groups overage claim may have been emitted. + # https://learn.microsoft.com/entra/identity-platform/id-token-claims-reference#groups-overage-claim + missing_groups_claim = "groups" not in id_token_claims + has_group_overage_claim = ( + missing_groups_claim + and "_claim_names" in id_token_claims + and "groups" in id_token_claims["_claim_names"] + ) + if missing_groups_claim or has_group_overage_claim: + # Read the user's groups from Microsoft Graph + auth_claims["groups"] = await AuthenticationHelper.list_groups(graph_resource_access_token) + return auth_claims + except AuthError as e: + logging.exception("Exception getting authorization information - " + json.dumps(e.error)) + if self.require_access_control and not self.enable_unauthenticated_access: + raise + return {} + except Exception: + logging.exception("Exception getting authorization information") + if self.require_access_control and not self.enable_unauthenticated_access: + raise + return {} + + async def check_path_auth(self, path: str, auth_claims: dict[str, Any], search_client: SearchClient) -> bool: + # Start with the standard security filter for all queries + security_filter = self.build_security_filters(overrides={}, auth_claims=auth_claims) + # If there was no security filter or no path, then the path is allowed + if not security_filter or len(path) == 0: + return True + + # Remove any fragment string from the path before checking + fragment_index = path.find("#") + if fragment_index != -1: + path = path[:fragment_index] + + # Filter down to only chunks that are from the specific source file + # Sourcepage is used for GPT-4V + # Replace ' with '' to escape the single quote for the filter + # https://learn.microsoft.com/azure/search/query-odata-filter-orderby-syntax#escaping-special-characters-in-string-constants + path_for_filter = path.replace("'", "''") + filter = f"{security_filter} and ((sourcefile eq '{path_for_filter}') or (sourcepage eq '{path_for_filter}'))" + + # If the filter returns any results, the user is allowed to access the document + # Otherwise, access is denied + results = await search_client.search(search_text="*", top=1, filter=filter) + allowed = False + async for _ in results: + allowed = True + break + + return allowed + + # See https://github.com/Azure-Samples/ms-identity-python-on-behalf-of/blob/939be02b11f1604814532fdacc2c2eccd198b755/FlaskAPI/helpers/authorization.py#L44 + async def validate_access_token(self, token: str): + """ + Validate an access token is issued by Entra + """ + jwks = None + async for attempt in AsyncRetrying( + retry=retry_if_exception_type(AuthError), + wait=wait_random_exponential(min=15, max=60), + stop=stop_after_attempt(5), + ): + with attempt: + async with aiohttp.ClientSession() as session: + async with session.get(url=self.key_url) as resp: + resp_status = resp.status + if resp_status in [500, 502, 503, 504]: + raise AuthError( + error=f"Failed to get keys info: {await resp.text()}", status_code=resp_status + ) + jwks = await resp.json() + + if not jwks or "keys" not in jwks: + raise AuthError({"code": "invalid_keys", "description": "Unable to get keys to validate auth token."}, 401) + + rsa_key = None + issuer = None + audience = None + try: + unverified_header = jwt.get_unverified_header(token) + unverified_claims = jwt.get_unverified_claims(token) + issuer = unverified_claims.get("iss") + audience = unverified_claims.get("aud") + for key in jwks["keys"]: + if key["kid"] == unverified_header["kid"]: + rsa_key = {"kty": key["kty"], "kid": key["kid"], "use": key["use"], "n": key["n"], "e": key["e"]} + break + except Exception as exc: + raise AuthError( + {"code": "invalid_header", "description": "Unable to parse authorization token."}, 401 + ) from exc + if not rsa_key: + raise AuthError({"code": "invalid_header", "description": "Unable to find appropriate key"}, 401) + + if issuer not in self.valid_issuers: + raise AuthError( + {"code": "invalid_header", "description": f"Issuer {issuer} not in {','.join(self.valid_issuers)}"}, 401 + ) + + if audience not in self.valid_audiences: + raise AuthError( + { + "code": "invalid_header", + "description": f"Audience {audience} not in {','.join(self.valid_audiences)}", + }, + 401, + ) + + try: + jwt.decode(token, rsa_key, algorithms=["RS256"], audience=audience, issuer=issuer) + except ExpiredSignatureError as jwt_expired_exc: + raise AuthError({"code": "token_expired", "description": "token is expired"}, 401) from jwt_expired_exc + except JWTClaimsError as jwt_claims_exc: + raise AuthError( + {"code": "invalid_claims", "description": "incorrect claims," "please check the audience and issuer"}, + 401, + ) from jwt_claims_exc + except Exception as exc: + raise AuthError( + {"code": "invalid_header", "description": "Unable to parse authorization token."}, 401 + ) from exc diff --git a/app/backend/core/imageshelper.py b/app/backend/core/imageshelper.py index d35f659766..050c9142ac 100644 --- a/app/backend/core/imageshelper.py +++ b/app/backend/core/imageshelper.py @@ -1,43 +1,43 @@ -import base64 -import logging -import os -from typing import Optional - -from azure.core.exceptions import ResourceNotFoundError -from azure.storage.blob.aio import ContainerClient -from typing_extensions import Literal, Required, TypedDict - -from approaches.approach import Document - - -class ImageURL(TypedDict, total=False): - url: Required[str] - """Either a URL of the image or the base64 encoded image data.""" - - detail: Literal["auto", "low", "high"] - """Specifies the detail level of the image.""" - - -async def download_blob_as_base64(blob_container_client: ContainerClient, file_path: str) -> Optional[str]: - base_name, _ = os.path.splitext(file_path) - image_filename = base_name + ".png" - try: - blob = await blob_container_client.get_blob_client(image_filename).download_blob() - if not blob.properties: - logging.warning(f"No blob exists for {image_filename}") - return None - img = base64.b64encode(await blob.readall()).decode("utf-8") - return f"data:image/png;base64,{img}" - except ResourceNotFoundError: - logging.warning(f"No blob exists for {image_filename}") - return None - - -async def fetch_image(blob_container_client: ContainerClient, result: Document) -> Optional[ImageURL]: - if result.sourcepage: - img = await download_blob_as_base64(blob_container_client, result.sourcepage) - if img: - return {"url": img, "detail": "auto"} - else: - return None - return None +import base64 +import logging +import os +from typing import Optional + +from azure.core.exceptions import ResourceNotFoundError +from azure.storage.blob.aio import ContainerClient +from typing_extensions import Literal, Required, TypedDict + +from approaches.approach import Document + + +class ImageURL(TypedDict, total=False): + url: Required[str] + """Either a URL of the image or the base64 encoded image data.""" + + detail: Literal["auto", "low", "high"] + """Specifies the detail level of the image.""" + + +async def download_blob_as_base64(blob_container_client: ContainerClient, file_path: str) -> Optional[str]: + base_name, _ = os.path.splitext(file_path) + image_filename = base_name + ".png" + try: + blob = await blob_container_client.get_blob_client(image_filename).download_blob() + if not blob.properties: + logging.warning(f"No blob exists for {image_filename}") + return None + img = base64.b64encode(await blob.readall()).decode("utf-8") + return f"data:image/png;base64,{img}" + except ResourceNotFoundError: + logging.warning(f"No blob exists for {image_filename}") + return None + + +async def fetch_image(blob_container_client: ContainerClient, result: Document) -> Optional[ImageURL]: + if result.sourcepage: + img = await download_blob_as_base64(blob_container_client, result.sourcepage) + if img: + return {"url": img, "detail": "auto"} + else: + return None + return None diff --git a/app/backend/decorators.py b/app/backend/decorators.py index 32f6b9a2b5..322acd048d 100644 --- a/app/backend/decorators.py +++ b/app/backend/decorators.py @@ -1,55 +1,55 @@ -import logging -from functools import wraps -from typing import Any, Callable, Dict - -from quart import abort, current_app, request - -from config import CONFIG_AUTH_CLIENT, CONFIG_SEARCH_CLIENT -from core.authentication import AuthError -from error import error_response - - -def authenticated_path(route_fn: Callable[[str, Dict[str, Any]], Any]): - """ - Decorator for routes that request a specific file that might require access control enforcement - """ - - @wraps(route_fn) - async def auth_handler(path=""): - # If authentication is enabled, validate the user can access the file - auth_helper = current_app.config[CONFIG_AUTH_CLIENT] - search_client = current_app.config[CONFIG_SEARCH_CLIENT] - authorized = False - try: - auth_claims = await auth_helper.get_auth_claims_if_enabled(request.headers) - authorized = await auth_helper.check_path_auth(path, auth_claims, search_client) - except AuthError: - abort(403) - except Exception as error: - logging.exception("Problem checking path auth %s", error) - return error_response(error, route="/content") - - if not authorized: - abort(403) - - return await route_fn(path, auth_claims) - - return auth_handler - - -def authenticated(route_fn: Callable[[Dict[str, Any]], Any]): - """ - Decorator for routes that might require access control. Unpacks Authorization header information into an auth_claims dictionary - """ - - @wraps(route_fn) - async def auth_handler(): - auth_helper = current_app.config[CONFIG_AUTH_CLIENT] - try: - auth_claims = await auth_helper.get_auth_claims_if_enabled(request.headers) - except AuthError: - abort(403) - - return await route_fn(auth_claims) - - return auth_handler +import logging +from functools import wraps +from typing import Any, Callable, Dict + +from quart import abort, current_app, request + +from config import CONFIG_AUTH_CLIENT, CONFIG_SEARCH_CLIENT +from core.authentication import AuthError +from error import error_response + + +def authenticated_path(route_fn: Callable[[str, Dict[str, Any]], Any]): + """ + Decorator for routes that request a specific file that might require access control enforcement + """ + + @wraps(route_fn) + async def auth_handler(path=""): + # If authentication is enabled, validate the user can access the file + auth_helper = current_app.config[CONFIG_AUTH_CLIENT] + search_client = current_app.config[CONFIG_SEARCH_CLIENT] + authorized = False + try: + auth_claims = await auth_helper.get_auth_claims_if_enabled(request.headers) + authorized = await auth_helper.check_path_auth(path, auth_claims, search_client) + except AuthError: + abort(403) + except Exception as error: + logging.exception("Problem checking path auth %s", error) + return error_response(error, route="/content") + + if not authorized: + abort(403) + + return await route_fn(path, auth_claims) + + return auth_handler + + +def authenticated(route_fn: Callable[[Dict[str, Any]], Any]): + """ + Decorator for routes that might require access control. Unpacks Authorization header information into an auth_claims dictionary + """ + + @wraps(route_fn) + async def auth_handler(): + auth_helper = current_app.config[CONFIG_AUTH_CLIENT] + try: + auth_claims = await auth_helper.get_auth_claims_if_enabled(request.headers) + except AuthError: + abort(403) + + return await route_fn(auth_claims) + + return auth_handler diff --git a/app/backend/error.py b/app/backend/error.py index 0a21afe6b7..b145d379ca 100644 --- a/app/backend/error.py +++ b/app/backend/error.py @@ -1,27 +1,27 @@ -import logging - -from openai import APIError -from quart import jsonify - -ERROR_MESSAGE = """The app encountered an error processing your request. -If you are an administrator of the app, view the full error in the logs. See aka.ms/appservice-logs for more information. -Error type: {error_type} -""" -ERROR_MESSAGE_FILTER = """Your message contains content that was flagged by the OpenAI content filter.""" - -ERROR_MESSAGE_LENGTH = """Your message exceeded the context length limit for this OpenAI model. Please shorten your message or change your settings to retrieve fewer search results.""" - - -def error_dict(error: Exception) -> dict: - if isinstance(error, APIError) and error.code == "content_filter": - return {"error": ERROR_MESSAGE_FILTER} - if isinstance(error, APIError) and error.code == "context_length_exceeded": - return {"error": ERROR_MESSAGE_LENGTH} - return {"error": ERROR_MESSAGE.format(error_type=type(error))} - - -def error_response(error: Exception, route: str, status_code: int = 500): - logging.exception("Exception in %s: %s", route, error) - if isinstance(error, APIError) and error.code == "content_filter": - status_code = 400 - return jsonify(error_dict(error)), status_code +import logging + +from openai import APIError +from quart import jsonify + +ERROR_MESSAGE = """The app encountered an error processing your request. +If you are an administrator of the app, view the full error in the logs. See aka.ms/appservice-logs for more information. +Error type: {error_type} +""" +ERROR_MESSAGE_FILTER = """Your message contains content that was flagged by the OpenAI content filter.""" + +ERROR_MESSAGE_LENGTH = """Your message exceeded the context length limit for this OpenAI model. Please shorten your message or change your settings to retrieve fewer search results.""" + + +def error_dict(error: Exception) -> dict: + if isinstance(error, APIError) and error.code == "content_filter": + return {"error": ERROR_MESSAGE_FILTER} + if isinstance(error, APIError) and error.code == "context_length_exceeded": + return {"error": ERROR_MESSAGE_LENGTH} + return {"error": ERROR_MESSAGE.format(error_type=type(error))} + + +def error_response(error: Exception, route: str, status_code: int = 500): + logging.exception("Exception in %s: %s", route, error) + if isinstance(error, APIError) and error.code == "content_filter": + status_code = 400 + return jsonify(error_dict(error)), status_code diff --git a/app/backend/gunicorn.conf.py b/app/backend/gunicorn.conf.py index f20da2286f..5e5f6a3d1a 100644 --- a/app/backend/gunicorn.conf.py +++ b/app/backend/gunicorn.conf.py @@ -1,18 +1,18 @@ -import multiprocessing -import os - -max_requests = 1000 -max_requests_jitter = 50 -log_file = "-" -bind = "0.0.0.0" - -timeout = 230 -# https://learn.microsoft.com/en-us/troubleshoot/azure/app-service/web-apps-performance-faqs#why-does-my-request-time-out-after-230-seconds - -num_cpus = multiprocessing.cpu_count() -if os.getenv("WEBSITE_SKU") == "LinuxFree": - # Free tier reports 2 CPUs but can't handle multiple workers - workers = 1 -else: - workers = (num_cpus * 2) + 1 -worker_class = "uvicorn.workers.UvicornWorker" +import multiprocessing +import os + +max_requests = 1000 +max_requests_jitter = 50 +log_file = "-" +bind = "0.0.0.0" + +timeout = 230 +# https://learn.microsoft.com/en-us/troubleshoot/azure/app-service/web-apps-performance-faqs#why-does-my-request-time-out-after-230-seconds + +num_cpus = multiprocessing.cpu_count() +if os.getenv("WEBSITE_SKU") == "LinuxFree": + # Free tier reports 2 CPUs but can't handle multiple workers + workers = 1 +else: + workers = (num_cpus * 2) + 1 +worker_class = "uvicorn.workers.UvicornWorker" diff --git a/app/backend/main.py b/app/backend/main.py index 0a23b5abbf..0acaeef799 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -1,3 +1,3 @@ -from app import create_app - -app = create_app() +from app import create_app + +app = create_app() diff --git a/app/backend/prepdocs.py b/app/backend/prepdocs.py index bd7cb22062..e28bdd6cdf 100644 --- a/app/backend/prepdocs.py +++ b/app/backend/prepdocs.py @@ -1,471 +1,471 @@ -import argparse -import asyncio -import logging -from typing import Optional, Union - -from azure.core.credentials import AzureKeyCredential -from azure.core.credentials_async import AsyncTokenCredential -from azure.identity.aio import AzureDeveloperCliCredential, get_bearer_token_provider - -from prepdocslib.blobmanager import BlobManager -from prepdocslib.embeddings import ( - AzureOpenAIEmbeddingService, - ImageEmbeddings, - OpenAIEmbeddingService, -) -from prepdocslib.fileprocessor import FileProcessor -from prepdocslib.filestrategy import FileStrategy -from prepdocslib.htmlparser import LocalHTMLParser -from prepdocslib.integratedvectorizerstrategy import ( - IntegratedVectorizerStrategy, -) -from prepdocslib.jsonparser import JsonParser -from prepdocslib.listfilestrategy import ( - ADLSGen2ListFileStrategy, - ListFileStrategy, - LocalListFileStrategy, -) -from prepdocslib.parser import Parser -from prepdocslib.pdfparser import DocumentAnalysisParser, LocalPdfParser -from prepdocslib.strategy import DocumentAction, SearchInfo, Strategy -from prepdocslib.textparser import TextParser -from prepdocslib.textsplitter import SentenceTextSplitter, SimpleTextSplitter - -logger = logging.getLogger("ingester") - - -def clean_key_if_exists(key: Union[str, None]) -> Union[str, None]: - """Remove leading and trailing whitespace from a key if it exists. If the key is empty, return None.""" - if key is not None and key.strip() != "": - return key.strip() - return None - - -async def setup_search_info( - search_service: str, index_name: str, azure_credential: AsyncTokenCredential, search_key: Union[str, None] = None -) -> SearchInfo: - search_creds: Union[AsyncTokenCredential, AzureKeyCredential] = ( - azure_credential if search_key is None else AzureKeyCredential(search_key) - ) - - return SearchInfo( - endpoint=f"https://{search_service}.search.windows.net/", - credential=search_creds, - index_name=index_name, - ) - - -def setup_blob_manager( - azure_credential: AsyncTokenCredential, - storage_account: str, - storage_container: str, - storage_resource_group: str, - subscription_id: str, - search_images: bool, - storage_key: Union[str, None] = None, -): - storage_creds: Union[AsyncTokenCredential, str] = azure_credential if storage_key is None else storage_key - return BlobManager( - endpoint=f"https://{storage_account}.blob.core.windows.net", - container=storage_container, - account=storage_account, - credential=storage_creds, - resourceGroup=storage_resource_group, - subscriptionId=subscription_id, - store_page_images=search_images, - ) - - -def setup_list_file_strategy( - azure_credential: AsyncTokenCredential, - local_files: Union[str, None], - datalake_storage_account: Union[str, None], - datalake_filesystem: Union[str, None], - datalake_path: Union[str, None], - datalake_key: Union[str, None], -): - list_file_strategy: ListFileStrategy - if datalake_storage_account: - if datalake_filesystem is None or datalake_path is None: - raise ValueError("DataLake file system and path are required when using Azure Data Lake Gen2") - adls_gen2_creds: Union[AsyncTokenCredential, str] = azure_credential if datalake_key is None else datalake_key - logger.info("Using Data Lake Gen2 Storage Account: %s", datalake_storage_account) - list_file_strategy = ADLSGen2ListFileStrategy( - data_lake_storage_account=datalake_storage_account, - data_lake_filesystem=datalake_filesystem, - data_lake_path=datalake_path, - credential=adls_gen2_creds, - ) - elif local_files: - logger.info("Using local files: %s", local_files) - list_file_strategy = LocalListFileStrategy(path_pattern=local_files) - else: - raise ValueError("Either local_files or datalake_storage_account must be provided.") - return list_file_strategy - - -def setup_embeddings_service( - azure_credential: AsyncTokenCredential, - openai_host: str, - openai_model_name: str, - openai_service: Union[str, None], - openai_deployment: Union[str, None], - openai_dimensions: int, - openai_key: Union[str, None], - openai_org: Union[str, None], - disable_vectors: bool = False, - disable_batch_vectors: bool = False, -): - if disable_vectors: - logger.info("Not setting up embeddings service") - return None - - if openai_host != "openai": - azure_open_ai_credential: Union[AsyncTokenCredential, AzureKeyCredential] = ( - azure_credential if openai_key is None else AzureKeyCredential(openai_key) - ) - return AzureOpenAIEmbeddingService( - open_ai_service=openai_service, - open_ai_deployment=openai_deployment, - open_ai_model_name=openai_model_name, - open_ai_dimensions=openai_dimensions, - credential=azure_open_ai_credential, - disable_batch=disable_batch_vectors, - ) - else: - if openai_key is None: - raise ValueError("OpenAI key is required when using the non-Azure OpenAI API") - return OpenAIEmbeddingService( - open_ai_model_name=openai_model_name, - open_ai_dimensions=openai_dimensions, - credential=openai_key, - organization=openai_org, - disable_batch=disable_batch_vectors, - ) - - -def setup_file_processors( - azure_credential: AsyncTokenCredential, - document_intelligence_service: Union[str, None], - document_intelligence_key: Union[str, None] = None, - local_pdf_parser: bool = False, - local_html_parser: bool = False, - search_images: bool = False, -): - html_parser: Parser - pdf_parser: Parser - doc_int_parser: DocumentAnalysisParser - - # check if Azure Document Intelligence credentials are provided - if document_intelligence_service is not None: - documentintelligence_creds: Union[AsyncTokenCredential, AzureKeyCredential] = ( - azure_credential if document_intelligence_key is None else AzureKeyCredential(document_intelligence_key) - ) - doc_int_parser = DocumentAnalysisParser( - endpoint=f"https://{document_intelligence_service}.cognitiveservices.azure.com/", - credential=documentintelligence_creds, - ) - if local_pdf_parser or document_intelligence_service is None: - pdf_parser = LocalPdfParser() - else: - pdf_parser = doc_int_parser - if local_html_parser or document_intelligence_service is None: - html_parser = LocalHTMLParser() - else: - html_parser = doc_int_parser - sentence_text_splitter = SentenceTextSplitter(has_image_embeddings=search_images) - return { - ".pdf": FileProcessor(pdf_parser, sentence_text_splitter), - ".html": FileProcessor(html_parser, sentence_text_splitter), - ".json": FileProcessor(JsonParser(), SimpleTextSplitter()), - ".docx": FileProcessor(doc_int_parser, sentence_text_splitter), - ".pptx": FileProcessor(doc_int_parser, sentence_text_splitter), - ".xlsx": FileProcessor(doc_int_parser, sentence_text_splitter), - ".png": FileProcessor(doc_int_parser, sentence_text_splitter), - ".jpg": FileProcessor(doc_int_parser, sentence_text_splitter), - ".jpeg": FileProcessor(doc_int_parser, sentence_text_splitter), - ".tiff": FileProcessor(doc_int_parser, sentence_text_splitter), - ".bmp": FileProcessor(doc_int_parser, sentence_text_splitter), - ".heic": FileProcessor(doc_int_parser, sentence_text_splitter), - ".md": FileProcessor(TextParser(), sentence_text_splitter), - ".txt": FileProcessor(TextParser(), sentence_text_splitter), - } - - -def setup_image_embeddings_service( - azure_credential: AsyncTokenCredential, vision_endpoint: Union[str, None], search_images: bool -) -> Union[ImageEmbeddings, None]: - image_embeddings_service: Optional[ImageEmbeddings] = None - if search_images: - if vision_endpoint is None: - raise ValueError("A computer vision endpoint is required when GPT-4-vision is enabled.") - image_embeddings_service = ImageEmbeddings( - endpoint=vision_endpoint, - token_provider=get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default"), - ) - return image_embeddings_service - - -async def main(strategy: Strategy, setup_index: bool = True): - if setup_index: - await strategy.setup() - - await strategy.run() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Prepare documents by extracting content from PDFs, splitting content into sections, uploading to blob storage, and indexing in a search index.", - epilog="Example: prepdocs.py '.\\data\*' --storageaccount myaccount --container mycontainer --searchservice mysearch --index myindex -v", - ) - parser.add_argument("files", nargs="?", help="Files to be processed") - parser.add_argument( - "--datalakestorageaccount", required=False, help="Optional. Azure Data Lake Storage Gen2 Account name" - ) - parser.add_argument( - "--datalakefilesystem", - required=False, - default="gptkbcontainer", - help="Optional. Azure Data Lake Storage Gen2 filesystem name", - ) - parser.add_argument( - "--datalakepath", - required=False, - help="Optional. Azure Data Lake Storage Gen2 filesystem path containing files to index. If omitted, index the entire filesystem", - ) - parser.add_argument( - "--datalakekey", required=False, help="Optional. Use this key when authenticating to Azure Data Lake Gen2" - ) - parser.add_argument( - "--useacls", action="store_true", help="Store ACLs from Azure Data Lake Gen2 Filesystem in the search index" - ) - parser.add_argument( - "--category", help="Value for the category field in the search index for all sections indexed in this run" - ) - parser.add_argument( - "--skipblobs", action="store_true", help="Skip uploading individual pages to Azure Blob Storage" - ) - parser.add_argument("--storageaccount", help="Azure Blob Storage account name") - parser.add_argument("--container", help="Azure Blob Storage container name") - parser.add_argument("--storageresourcegroup", help="Azure blob storage resource group") - parser.add_argument( - "--storagekey", - required=False, - help="Optional. Use this Azure Blob Storage account key instead of the current user identity to login (use az login to set current user for Azure)", - ) - parser.add_argument( - "--tenantid", required=False, help="Optional. Use this to define the Azure directory where to authenticate)" - ) - parser.add_argument( - "--subscriptionid", - required=False, - help="Optional. Use this to define managed identity connection string in integrated vectorization", - ) - parser.add_argument( - "--searchservice", - help="Name of the Azure AI Search service where content should be indexed (must exist already)", - ) - parser.add_argument( - "--searchserviceassignedid", - required=False, - help="Search service system assigned Identity (Managed identity) (used for integrated vectorization)", - ) - parser.add_argument( - "--index", - help="Name of the Azure AI Search index where content should be indexed (will be created if it doesn't exist)", - ) - parser.add_argument( - "--searchkey", - required=False, - help="Optional. Use this Azure AI Search account key instead of the current user identity to login (use az login to set current user for Azure)", - ) - parser.add_argument( - "--searchanalyzername", - required=False, - default="en.microsoft", - help="Optional. Name of the Azure AI Search analyzer to use for the content field in the index", - ) - parser.add_argument("--openaihost", help="Host of the API used to compute embeddings ('azure' or 'openai')") - parser.add_argument("--openaiservice", help="Name of the Azure OpenAI service used to compute embeddings") - parser.add_argument( - "--openaideployment", - help="Name of the Azure OpenAI model deployment for an embedding model ('text-embedding-ada-002' recommended)", - ) - parser.add_argument( - "--openaimodelname", help="Name of the Azure OpenAI embedding model ('text-embedding-ada-002' recommended)" - ) - parser.add_argument( - "--openaidimensions", - required=False, - default=1536, - type=int, - help="Dimensions for the embedding model (defaults to 1536 for 'text-embedding-ada-002')", - ) - parser.add_argument( - "--novectors", - action="store_true", - help="Don't compute embeddings for the sections (e.g. don't call the OpenAI embeddings API during indexing)", - ) - parser.add_argument( - "--disablebatchvectors", action="store_true", help="Don't compute embeddings in batch for the sections" - ) - parser.add_argument( - "--openaikey", - required=False, - help="Optional. Use this Azure OpenAI account key instead of the current user identity to login (use az login to set current user for Azure). This is required only when using non-Azure endpoints.", - ) - parser.add_argument("--openaiorg", required=False, help="This is required only when using non-Azure endpoints.") - parser.add_argument( - "--remove", - action="store_true", - help="Remove references to this document from blob storage and the search index", - ) - parser.add_argument( - "--removeall", - action="store_true", - help="Remove all blobs from blob storage and documents from the search index", - ) - parser.add_argument( - "--localpdfparser", - action="store_true", - help="Use PyPdf local PDF parser (supports only digital PDFs) instead of Azure Document Intelligence service to extract text, tables and layout from the documents", - ) - parser.add_argument( - "--localhtmlparser", - action="store_true", - help="Use Beautiful soap local HTML parser instead of Azure Document Intelligence service to extract text, tables and layout from the documents", - ) - parser.add_argument( - "--documentintelligenceservice", - required=False, - help="Optional. Name of the Azure Document Intelligence service which will be used to extract text, tables and layout from the documents (must exist already)", - ) - parser.add_argument( - "--documentintelligencekey", - required=False, - help="Optional. Use this Azure Document Intelligence account key instead of the current user identity to login (use az login to set current user for Azure)", - ) - parser.add_argument( - "--searchimages", - action="store_true", - required=False, - help="Optional. Generate image embeddings to enable each page to be searched as an image", - ) - parser.add_argument( - "--visionendpoint", - required=False, - help="Optional, required if --searchimages is specified. Endpoint of Azure AI Vision service to use when embedding images.", - ) - parser.add_argument( - "--useintvectorization", - required=False, - help="Required if --useintvectorization is specified. Enable Integrated vectorizer indexer support which is in preview)", - ) - parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") - args = parser.parse_args() - - if args.verbose: - logging.basicConfig(format="%(message)s") - # We only set the level to INFO for our logger, - # to avoid seeing the noisy INFO level logs from the Azure SDKs - logger.setLevel(logging.INFO) - - use_int_vectorization = args.useintvectorization and args.useintvectorization.lower() == "true" - - # Use the current user identity to connect to Azure services unless a key is explicitly set for any of them - azd_credential = ( - AzureDeveloperCliCredential() - if args.tenantid is None - else AzureDeveloperCliCredential(tenant_id=args.tenantid, process_timeout=60) - ) - - if args.removeall: - document_action = DocumentAction.RemoveAll - elif args.remove: - document_action = DocumentAction.Remove - else: - document_action = DocumentAction.Add - - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - search_info = loop.run_until_complete( - setup_search_info( - search_service=args.searchservice, - index_name=args.index, - azure_credential=azd_credential, - search_key=clean_key_if_exists(args.searchkey), - ) - ) - blob_manager = setup_blob_manager( - azure_credential=azd_credential, - storage_account=args.storageaccount, - storage_container=args.container, - storage_resource_group=args.storageresourcegroup, - subscription_id=args.subscriptionid, - search_images=args.searchimages, - storage_key=clean_key_if_exists(args.storagekey), - ) - list_file_strategy = setup_list_file_strategy( - azure_credential=azd_credential, - local_files=args.files, - datalake_storage_account=args.datalakestorageaccount, - datalake_filesystem=args.datalakefilesystem, - datalake_path=args.datalakepath, - datalake_key=clean_key_if_exists(args.datalakekey), - ) - openai_embeddings_service = setup_embeddings_service( - azure_credential=azd_credential, - openai_host=args.openaihost, - openai_model_name=args.openaimodelname, - openai_service=args.openaiservice, - openai_deployment=args.openaideployment, - openai_dimensions=args.openaidimensions, - openai_key=clean_key_if_exists(args.openaikey), - openai_org=args.openaiorg, - disable_vectors=args.novectors, - disable_batch_vectors=args.disablebatchvectors, - ) - - ingestion_strategy: Strategy - if use_int_vectorization: - ingestion_strategy = IntegratedVectorizerStrategy( - search_info=search_info, - list_file_strategy=list_file_strategy, - blob_manager=blob_manager, - document_action=document_action, - embeddings=openai_embeddings_service, - subscription_id=args.subscriptionid, - search_service_user_assigned_id=args.searchserviceassignedid, - search_analyzer_name=args.searchanalyzername, - use_acls=args.useacls, - category=args.category, - ) - else: - file_processors = setup_file_processors( - azure_credential=azd_credential, - document_intelligence_service=args.documentintelligenceservice, - document_intelligence_key=clean_key_if_exists(args.documentintelligencekey), - local_pdf_parser=args.localpdfparser, - local_html_parser=args.localhtmlparser, - search_images=args.searchimages, - ) - image_embeddings_service = setup_image_embeddings_service( - azure_credential=azd_credential, vision_endpoint=args.visionendpoint, search_images=args.searchimages - ) - - ingestion_strategy = FileStrategy( - search_info=search_info, - list_file_strategy=list_file_strategy, - blob_manager=blob_manager, - file_processors=file_processors, - document_action=document_action, - embeddings=openai_embeddings_service, - image_embeddings=image_embeddings_service, - search_analyzer_name=args.searchanalyzername, - use_acls=args.useacls, - category=args.category, - ) - - loop.run_until_complete(main(ingestion_strategy, setup_index=not args.remove and not args.removeall)) - loop.close() +import argparse +import asyncio +import logging +from typing import Optional, Union + +from azure.core.credentials import AzureKeyCredential +from azure.core.credentials_async import AsyncTokenCredential +from azure.identity.aio import AzureDeveloperCliCredential, get_bearer_token_provider + +from prepdocslib.blobmanager import BlobManager +from prepdocslib.embeddings import ( + AzureOpenAIEmbeddingService, + ImageEmbeddings, + OpenAIEmbeddingService, +) +from prepdocslib.fileprocessor import FileProcessor +from prepdocslib.filestrategy import FileStrategy +from prepdocslib.htmlparser import LocalHTMLParser +from prepdocslib.integratedvectorizerstrategy import ( + IntegratedVectorizerStrategy, +) +from prepdocslib.jsonparser import JsonParser +from prepdocslib.listfilestrategy import ( + ADLSGen2ListFileStrategy, + ListFileStrategy, + LocalListFileStrategy, +) +from prepdocslib.parser import Parser +from prepdocslib.pdfparser import DocumentAnalysisParser, LocalPdfParser +from prepdocslib.strategy import DocumentAction, SearchInfo, Strategy +from prepdocslib.textparser import TextParser +from prepdocslib.textsplitter import SentenceTextSplitter, SimpleTextSplitter + +logger = logging.getLogger("ingester") + + +def clean_key_if_exists(key: Union[str, None]) -> Union[str, None]: + """Remove leading and trailing whitespace from a key if it exists. If the key is empty, return None.""" + if key is not None and key.strip() != "": + return key.strip() + return None + + +async def setup_search_info( + search_service: str, index_name: str, azure_credential: AsyncTokenCredential, search_key: Union[str, None] = None +) -> SearchInfo: + search_creds: Union[AsyncTokenCredential, AzureKeyCredential] = ( + azure_credential if search_key is None else AzureKeyCredential(search_key) + ) + + return SearchInfo( + endpoint=f"https://{search_service}.search.windows.net/", + credential=search_creds, + index_name=index_name, + ) + + +def setup_blob_manager( + azure_credential: AsyncTokenCredential, + storage_account: str, + storage_container: str, + storage_resource_group: str, + subscription_id: str, + search_images: bool, + storage_key: Union[str, None] = None, +): + storage_creds: Union[AsyncTokenCredential, str] = azure_credential if storage_key is None else storage_key + return BlobManager( + endpoint=f"https://{storage_account}.blob.core.windows.net", + container=storage_container, + account=storage_account, + credential=storage_creds, + resourceGroup=storage_resource_group, + subscriptionId=subscription_id, + store_page_images=search_images, + ) + + +def setup_list_file_strategy( + azure_credential: AsyncTokenCredential, + local_files: Union[str, None], + datalake_storage_account: Union[str, None], + datalake_filesystem: Union[str, None], + datalake_path: Union[str, None], + datalake_key: Union[str, None], +): + list_file_strategy: ListFileStrategy + if datalake_storage_account: + if datalake_filesystem is None or datalake_path is None: + raise ValueError("DataLake file system and path are required when using Azure Data Lake Gen2") + adls_gen2_creds: Union[AsyncTokenCredential, str] = azure_credential if datalake_key is None else datalake_key + logger.info("Using Data Lake Gen2 Storage Account: %s", datalake_storage_account) + list_file_strategy = ADLSGen2ListFileStrategy( + data_lake_storage_account=datalake_storage_account, + data_lake_filesystem=datalake_filesystem, + data_lake_path=datalake_path, + credential=adls_gen2_creds, + ) + elif local_files: + logger.info("Using local files: %s", local_files) + list_file_strategy = LocalListFileStrategy(path_pattern=local_files) + else: + raise ValueError("Either local_files or datalake_storage_account must be provided.") + return list_file_strategy + + +def setup_embeddings_service( + azure_credential: AsyncTokenCredential, + openai_host: str, + openai_model_name: str, + openai_service: Union[str, None], + openai_deployment: Union[str, None], + openai_dimensions: int, + openai_key: Union[str, None], + openai_org: Union[str, None], + disable_vectors: bool = False, + disable_batch_vectors: bool = False, +): + if disable_vectors: + logger.info("Not setting up embeddings service") + return None + + if openai_host != "openai": + azure_open_ai_credential: Union[AsyncTokenCredential, AzureKeyCredential] = ( + azure_credential if openai_key is None else AzureKeyCredential(openai_key) + ) + return AzureOpenAIEmbeddingService( + open_ai_service=openai_service, + open_ai_deployment=openai_deployment, + open_ai_model_name=openai_model_name, + open_ai_dimensions=openai_dimensions, + credential=azure_open_ai_credential, + disable_batch=disable_batch_vectors, + ) + else: + if openai_key is None: + raise ValueError("OpenAI key is required when using the non-Azure OpenAI API") + return OpenAIEmbeddingService( + open_ai_model_name=openai_model_name, + open_ai_dimensions=openai_dimensions, + credential=openai_key, + organization=openai_org, + disable_batch=disable_batch_vectors, + ) + + +def setup_file_processors( + azure_credential: AsyncTokenCredential, + document_intelligence_service: Union[str, None], + document_intelligence_key: Union[str, None] = None, + local_pdf_parser: bool = False, + local_html_parser: bool = False, + search_images: bool = False, +): + html_parser: Parser + pdf_parser: Parser + doc_int_parser: DocumentAnalysisParser + + # check if Azure Document Intelligence credentials are provided + if document_intelligence_service is not None: + documentintelligence_creds: Union[AsyncTokenCredential, AzureKeyCredential] = ( + azure_credential if document_intelligence_key is None else AzureKeyCredential(document_intelligence_key) + ) + doc_int_parser = DocumentAnalysisParser( + endpoint=f"https://{document_intelligence_service}.cognitiveservices.azure.com/", + credential=documentintelligence_creds, + ) + if local_pdf_parser or document_intelligence_service is None: + pdf_parser = LocalPdfParser() + else: + pdf_parser = doc_int_parser + if local_html_parser or document_intelligence_service is None: + html_parser = LocalHTMLParser() + else: + html_parser = doc_int_parser + sentence_text_splitter = SentenceTextSplitter(has_image_embeddings=search_images) + return { + ".pdf": FileProcessor(pdf_parser, sentence_text_splitter), + ".html": FileProcessor(html_parser, sentence_text_splitter), + ".json": FileProcessor(JsonParser(), SimpleTextSplitter()), + ".docx": FileProcessor(doc_int_parser, sentence_text_splitter), + ".pptx": FileProcessor(doc_int_parser, sentence_text_splitter), + ".xlsx": FileProcessor(doc_int_parser, sentence_text_splitter), + ".png": FileProcessor(doc_int_parser, sentence_text_splitter), + ".jpg": FileProcessor(doc_int_parser, sentence_text_splitter), + ".jpeg": FileProcessor(doc_int_parser, sentence_text_splitter), + ".tiff": FileProcessor(doc_int_parser, sentence_text_splitter), + ".bmp": FileProcessor(doc_int_parser, sentence_text_splitter), + ".heic": FileProcessor(doc_int_parser, sentence_text_splitter), + ".md": FileProcessor(TextParser(), sentence_text_splitter), + ".txt": FileProcessor(TextParser(), sentence_text_splitter), + } + + +def setup_image_embeddings_service( + azure_credential: AsyncTokenCredential, vision_endpoint: Union[str, None], search_images: bool +) -> Union[ImageEmbeddings, None]: + image_embeddings_service: Optional[ImageEmbeddings] = None + if search_images: + if vision_endpoint is None: + raise ValueError("A computer vision endpoint is required when GPT-4-vision is enabled.") + image_embeddings_service = ImageEmbeddings( + endpoint=vision_endpoint, + token_provider=get_bearer_token_provider(azure_credential, "https://cognitiveservices.azure.com/.default"), + ) + return image_embeddings_service + + +async def main(strategy: Strategy, setup_index: bool = True): + if setup_index: + await strategy.setup() + + await strategy.run() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Prepare documents by extracting content from PDFs, splitting content into sections, uploading to blob storage, and indexing in a search index.", + epilog="Example: prepdocs.py '.\\data\*' --storageaccount myaccount --container mycontainer --searchservice mysearch --index myindex -v", + ) + parser.add_argument("files", nargs="?", help="Files to be processed") + parser.add_argument( + "--datalakestorageaccount", required=False, help="Optional. Azure Data Lake Storage Gen2 Account name" + ) + parser.add_argument( + "--datalakefilesystem", + required=False, + default="gptkbcontainer", + help="Optional. Azure Data Lake Storage Gen2 filesystem name", + ) + parser.add_argument( + "--datalakepath", + required=False, + help="Optional. Azure Data Lake Storage Gen2 filesystem path containing files to index. If omitted, index the entire filesystem", + ) + parser.add_argument( + "--datalakekey", required=False, help="Optional. Use this key when authenticating to Azure Data Lake Gen2" + ) + parser.add_argument( + "--useacls", action="store_true", help="Store ACLs from Azure Data Lake Gen2 Filesystem in the search index" + ) + parser.add_argument( + "--category", help="Value for the category field in the search index for all sections indexed in this run" + ) + parser.add_argument( + "--skipblobs", action="store_true", help="Skip uploading individual pages to Azure Blob Storage" + ) + parser.add_argument("--storageaccount", help="Azure Blob Storage account name") + parser.add_argument("--container", help="Azure Blob Storage container name") + parser.add_argument("--storageresourcegroup", help="Azure blob storage resource group") + parser.add_argument( + "--storagekey", + required=False, + help="Optional. Use this Azure Blob Storage account key instead of the current user identity to login (use az login to set current user for Azure)", + ) + parser.add_argument( + "--tenantid", required=False, help="Optional. Use this to define the Azure directory where to authenticate)" + ) + parser.add_argument( + "--subscriptionid", + required=False, + help="Optional. Use this to define managed identity connection string in integrated vectorization", + ) + parser.add_argument( + "--searchservice", + help="Name of the Azure AI Search service where content should be indexed (must exist already)", + ) + parser.add_argument( + "--searchserviceassignedid", + required=False, + help="Search service system assigned Identity (Managed identity) (used for integrated vectorization)", + ) + parser.add_argument( + "--index", + help="Name of the Azure AI Search index where content should be indexed (will be created if it doesn't exist)", + ) + parser.add_argument( + "--searchkey", + required=False, + help="Optional. Use this Azure AI Search account key instead of the current user identity to login (use az login to set current user for Azure)", + ) + parser.add_argument( + "--searchanalyzername", + required=False, + default="en.microsoft", + help="Optional. Name of the Azure AI Search analyzer to use for the content field in the index", + ) + parser.add_argument("--openaihost", help="Host of the API used to compute embeddings ('azure' or 'openai')") + parser.add_argument("--openaiservice", help="Name of the Azure OpenAI service used to compute embeddings") + parser.add_argument( + "--openaideployment", + help="Name of the Azure OpenAI model deployment for an embedding model ('text-embedding-ada-002' recommended)", + ) + parser.add_argument( + "--openaimodelname", help="Name of the Azure OpenAI embedding model ('text-embedding-ada-002' recommended)" + ) + parser.add_argument( + "--openaidimensions", + required=False, + default=1536, + type=int, + help="Dimensions for the embedding model (defaults to 1536 for 'text-embedding-ada-002')", + ) + parser.add_argument( + "--novectors", + action="store_true", + help="Don't compute embeddings for the sections (e.g. don't call the OpenAI embeddings API during indexing)", + ) + parser.add_argument( + "--disablebatchvectors", action="store_true", help="Don't compute embeddings in batch for the sections" + ) + parser.add_argument( + "--openaikey", + required=False, + help="Optional. Use this Azure OpenAI account key instead of the current user identity to login (use az login to set current user for Azure). This is required only when using non-Azure endpoints.", + ) + parser.add_argument("--openaiorg", required=False, help="This is required only when using non-Azure endpoints.") + parser.add_argument( + "--remove", + action="store_true", + help="Remove references to this document from blob storage and the search index", + ) + parser.add_argument( + "--removeall", + action="store_true", + help="Remove all blobs from blob storage and documents from the search index", + ) + parser.add_argument( + "--localpdfparser", + action="store_true", + help="Use PyPdf local PDF parser (supports only digital PDFs) instead of Azure Document Intelligence service to extract text, tables and layout from the documents", + ) + parser.add_argument( + "--localhtmlparser", + action="store_true", + help="Use Beautiful soap local HTML parser instead of Azure Document Intelligence service to extract text, tables and layout from the documents", + ) + parser.add_argument( + "--documentintelligenceservice", + required=False, + help="Optional. Name of the Azure Document Intelligence service which will be used to extract text, tables and layout from the documents (must exist already)", + ) + parser.add_argument( + "--documentintelligencekey", + required=False, + help="Optional. Use this Azure Document Intelligence account key instead of the current user identity to login (use az login to set current user for Azure)", + ) + parser.add_argument( + "--searchimages", + action="store_true", + required=False, + help="Optional. Generate image embeddings to enable each page to be searched as an image", + ) + parser.add_argument( + "--visionendpoint", + required=False, + help="Optional, required if --searchimages is specified. Endpoint of Azure AI Vision service to use when embedding images.", + ) + parser.add_argument( + "--useintvectorization", + required=False, + help="Required if --useintvectorization is specified. Enable Integrated vectorizer indexer support which is in preview)", + ) + parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") + args = parser.parse_args() + + if args.verbose: + logging.basicConfig(format="%(message)s") + # We only set the level to INFO for our logger, + # to avoid seeing the noisy INFO level logs from the Azure SDKs + logger.setLevel(logging.INFO) + + use_int_vectorization = args.useintvectorization and args.useintvectorization.lower() == "true" + + # Use the current user identity to connect to Azure services unless a key is explicitly set for any of them + azd_credential = ( + AzureDeveloperCliCredential() + if args.tenantid is None + else AzureDeveloperCliCredential(tenant_id=args.tenantid, process_timeout=60) + ) + + if args.removeall: + document_action = DocumentAction.RemoveAll + elif args.remove: + document_action = DocumentAction.Remove + else: + document_action = DocumentAction.Add + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + search_info = loop.run_until_complete( + setup_search_info( + search_service=args.searchservice, + index_name=args.index, + azure_credential=azd_credential, + search_key=clean_key_if_exists(args.searchkey), + ) + ) + blob_manager = setup_blob_manager( + azure_credential=azd_credential, + storage_account=args.storageaccount, + storage_container=args.container, + storage_resource_group=args.storageresourcegroup, + subscription_id=args.subscriptionid, + search_images=args.searchimages, + storage_key=clean_key_if_exists(args.storagekey), + ) + list_file_strategy = setup_list_file_strategy( + azure_credential=azd_credential, + local_files=args.files, + datalake_storage_account=args.datalakestorageaccount, + datalake_filesystem=args.datalakefilesystem, + datalake_path=args.datalakepath, + datalake_key=clean_key_if_exists(args.datalakekey), + ) + openai_embeddings_service = setup_embeddings_service( + azure_credential=azd_credential, + openai_host=args.openaihost, + openai_model_name=args.openaimodelname, + openai_service=args.openaiservice, + openai_deployment=args.openaideployment, + openai_dimensions=args.openaidimensions, + openai_key=clean_key_if_exists(args.openaikey), + openai_org=args.openaiorg, + disable_vectors=args.novectors, + disable_batch_vectors=args.disablebatchvectors, + ) + + ingestion_strategy: Strategy + if use_int_vectorization: + ingestion_strategy = IntegratedVectorizerStrategy( + search_info=search_info, + list_file_strategy=list_file_strategy, + blob_manager=blob_manager, + document_action=document_action, + embeddings=openai_embeddings_service, + subscription_id=args.subscriptionid, + search_service_user_assigned_id=args.searchserviceassignedid, + search_analyzer_name=args.searchanalyzername, + use_acls=args.useacls, + category=args.category, + ) + else: + file_processors = setup_file_processors( + azure_credential=azd_credential, + document_intelligence_service=args.documentintelligenceservice, + document_intelligence_key=clean_key_if_exists(args.documentintelligencekey), + local_pdf_parser=args.localpdfparser, + local_html_parser=args.localhtmlparser, + search_images=args.searchimages, + ) + image_embeddings_service = setup_image_embeddings_service( + azure_credential=azd_credential, vision_endpoint=args.visionendpoint, search_images=args.searchimages + ) + + ingestion_strategy = FileStrategy( + search_info=search_info, + list_file_strategy=list_file_strategy, + blob_manager=blob_manager, + file_processors=file_processors, + document_action=document_action, + embeddings=openai_embeddings_service, + image_embeddings=image_embeddings_service, + search_analyzer_name=args.searchanalyzername, + use_acls=args.useacls, + category=args.category, + ) + + loop.run_until_complete(main(ingestion_strategy, setup_index=not args.remove and not args.removeall)) + loop.close() diff --git a/app/backend/prepdocslib/blobmanager.py b/app/backend/prepdocslib/blobmanager.py index b9ada05f10..24ed865b06 100644 --- a/app/backend/prepdocslib/blobmanager.py +++ b/app/backend/prepdocslib/blobmanager.py @@ -1,178 +1,178 @@ -import datetime -import io -import logging -import os -import re -from typing import List, Optional, Union - -import fitz # type: ignore -from azure.core.credentials_async import AsyncTokenCredential -from azure.storage.blob import ( - BlobSasPermissions, - UserDelegationKey, - generate_blob_sas, -) -from azure.storage.blob.aio import BlobServiceClient, ContainerClient -from PIL import Image, ImageDraw, ImageFont -from pypdf import PdfReader - -from .listfilestrategy import File - -logger = logging.getLogger("ingester") - - -class BlobManager: - """ - Class to manage uploading and deleting blobs containing citation information from a blob storage account - """ - - def __init__( - self, - endpoint: str, - container: str, - account: str, - credential: Union[AsyncTokenCredential, str], - resourceGroup: str, - subscriptionId: str, - store_page_images: bool = False, - ): - self.endpoint = endpoint - self.credential = credential - self.account = account - self.container = container - self.store_page_images = store_page_images - self.resourceGroup = resourceGroup - self.subscriptionId = subscriptionId - self.user_delegation_key: Optional[UserDelegationKey] = None - - async def upload_blob(self, file: File) -> Optional[List[str]]: - async with BlobServiceClient( - account_url=self.endpoint, credential=self.credential, max_single_put_size=4 * 1024 * 1024 - ) as service_client, service_client.get_container_client(self.container) as container_client: - if not await container_client.exists(): - await container_client.create_container() - - # Re-open and upload the original file - if file.url is None: - with open(file.content.name, "rb") as reopened_file: - blob_name = BlobManager.blob_name_from_file_name(file.content.name) - logger.info("Uploading blob for whole file -> %s", blob_name) - blob_client = await container_client.upload_blob(blob_name, reopened_file, overwrite=True) - file.url = blob_client.url - - if self.store_page_images: - if os.path.splitext(file.content.name)[1].lower() == ".pdf": - return await self.upload_pdf_blob_images(service_client, container_client, file) - else: - logger.info("File %s is not a PDF, skipping image upload", file.content.name) - - return None - - def get_managedidentity_connectionstring(self): - return f"ResourceId=/subscriptions/{self.subscriptionId}/resourceGroups/{self.resourceGroup}/providers/Microsoft.Storage/storageAccounts/{self.account};" - - async def upload_pdf_blob_images( - self, service_client: BlobServiceClient, container_client: ContainerClient, file: File - ) -> List[str]: - with open(file.content.name, "rb") as reopened_file: - reader = PdfReader(reopened_file) - page_count = len(reader.pages) - doc = fitz.open(file.content.name) - sas_uris = [] - start_time = datetime.datetime.now(datetime.timezone.utc) - expiry_time = start_time + datetime.timedelta(days=1) - - font = None - try: - font = ImageFont.truetype("arial.ttf", 20) - except OSError: - try: - font = ImageFont.truetype("/usr/share/fonts/truetype/freefont/FreeMono.ttf", 20) - except OSError: - logger.info("Unable to find arial.ttf or FreeMono.ttf, using default font") - - for i in range(page_count): - blob_name = BlobManager.blob_image_name_from_file_page(file.content.name, i) - logger.info("Converting page %s to image and uploading -> %s", i, blob_name) - - doc = fitz.open(file.content.name) - page = doc.load_page(i) - pix = page.get_pixmap() - original_img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) # type: ignore - - # Create a new image with additional space for text - text_height = 40 # Height of the text area - new_img = Image.new("RGB", (original_img.width, original_img.height + text_height), "white") - - # Paste the original image onto the new image - new_img.paste(original_img, (0, text_height)) - - # Draw the text on the white area - draw = ImageDraw.Draw(new_img) - text = f"SourceFileName:{blob_name}" - - # 10 pixels from the top and left of the image - x = 10 - y = 10 - draw.text((x, y), text, font=font, fill="black") - - output = io.BytesIO() - new_img.save(output, format="PNG") - output.seek(0) - - blob_client = await container_client.upload_blob(blob_name, output, overwrite=True) - if not self.user_delegation_key: - self.user_delegation_key = await service_client.get_user_delegation_key(start_time, expiry_time) - - if blob_client.account_name is not None: - sas_token = generate_blob_sas( - account_name=blob_client.account_name, - container_name=blob_client.container_name, - blob_name=blob_client.blob_name, - user_delegation_key=self.user_delegation_key, - permission=BlobSasPermissions(read=True), - expiry=expiry_time, - start=start_time, - ) - sas_uris.append(f"{blob_client.url}?{sas_token}") - - return sas_uris - - async def remove_blob(self, path: Optional[str] = None): - async with BlobServiceClient( - account_url=self.endpoint, credential=self.credential - ) as service_client, service_client.get_container_client(self.container) as container_client: - if not await container_client.exists(): - return - if path is None: - prefix = None - blobs = container_client.list_blob_names() - else: - prefix = os.path.splitext(os.path.basename(path))[0] - blobs = container_client.list_blob_names(name_starts_with=os.path.splitext(os.path.basename(prefix))[0]) - async for blob_path in blobs: - # This still supports PDFs split into individual pages, but we could remove in future to simplify code - if ( - prefix is not None - and ( - not re.match(rf"{prefix}-\d+\.pdf", blob_path) or not re.match(rf"{prefix}-\d+\.png", blob_path) - ) - ) or (path is not None and blob_path == os.path.basename(path)): - continue - logger.info("Removing blob %s", blob_path) - await container_client.delete_blob(blob_path) - - @classmethod - def sourcepage_from_file_page(cls, filename, page=0) -> str: - if os.path.splitext(filename)[1].lower() == ".pdf": - return f"{os.path.basename(filename)}#page={page+1}" - else: - return os.path.basename(filename) - - @classmethod - def blob_image_name_from_file_page(cls, filename, page=0) -> str: - return os.path.splitext(os.path.basename(filename))[0] + f"-{page}" + ".png" - - @classmethod - def blob_name_from_file_name(cls, filename) -> str: - return os.path.basename(filename) +import datetime +import io +import logging +import os +import re +from typing import List, Optional, Union + +import fitz # type: ignore +from azure.core.credentials_async import AsyncTokenCredential +from azure.storage.blob import ( + BlobSasPermissions, + UserDelegationKey, + generate_blob_sas, +) +from azure.storage.blob.aio import BlobServiceClient, ContainerClient +from PIL import Image, ImageDraw, ImageFont +from pypdf import PdfReader + +from .listfilestrategy import File + +logger = logging.getLogger("ingester") + + +class BlobManager: + """ + Class to manage uploading and deleting blobs containing citation information from a blob storage account + """ + + def __init__( + self, + endpoint: str, + container: str, + account: str, + credential: Union[AsyncTokenCredential, str], + resourceGroup: str, + subscriptionId: str, + store_page_images: bool = False, + ): + self.endpoint = endpoint + self.credential = credential + self.account = account + self.container = container + self.store_page_images = store_page_images + self.resourceGroup = resourceGroup + self.subscriptionId = subscriptionId + self.user_delegation_key: Optional[UserDelegationKey] = None + + async def upload_blob(self, file: File) -> Optional[List[str]]: + async with BlobServiceClient( + account_url=self.endpoint, credential=self.credential, max_single_put_size=4 * 1024 * 1024 + ) as service_client, service_client.get_container_client(self.container) as container_client: + if not await container_client.exists(): + await container_client.create_container() + + # Re-open and upload the original file + if file.url is None: + with open(file.content.name, "rb") as reopened_file: + blob_name = BlobManager.blob_name_from_file_name(file.content.name) + logger.info("Uploading blob for whole file -> %s", blob_name) + blob_client = await container_client.upload_blob(blob_name, reopened_file, overwrite=True) + file.url = blob_client.url + + if self.store_page_images: + if os.path.splitext(file.content.name)[1].lower() == ".pdf": + return await self.upload_pdf_blob_images(service_client, container_client, file) + else: + logger.info("File %s is not a PDF, skipping image upload", file.content.name) + + return None + + def get_managedidentity_connectionstring(self): + return f"ResourceId=/subscriptions/{self.subscriptionId}/resourceGroups/{self.resourceGroup}/providers/Microsoft.Storage/storageAccounts/{self.account};" + + async def upload_pdf_blob_images( + self, service_client: BlobServiceClient, container_client: ContainerClient, file: File + ) -> List[str]: + with open(file.content.name, "rb") as reopened_file: + reader = PdfReader(reopened_file) + page_count = len(reader.pages) + doc = fitz.open(file.content.name) + sas_uris = [] + start_time = datetime.datetime.now(datetime.timezone.utc) + expiry_time = start_time + datetime.timedelta(days=1) + + font = None + try: + font = ImageFont.truetype("arial.ttf", 20) + except OSError: + try: + font = ImageFont.truetype("/usr/share/fonts/truetype/freefont/FreeMono.ttf", 20) + except OSError: + logger.info("Unable to find arial.ttf or FreeMono.ttf, using default font") + + for i in range(page_count): + blob_name = BlobManager.blob_image_name_from_file_page(file.content.name, i) + logger.info("Converting page %s to image and uploading -> %s", i, blob_name) + + doc = fitz.open(file.content.name) + page = doc.load_page(i) + pix = page.get_pixmap() + original_img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) # type: ignore + + # Create a new image with additional space for text + text_height = 40 # Height of the text area + new_img = Image.new("RGB", (original_img.width, original_img.height + text_height), "white") + + # Paste the original image onto the new image + new_img.paste(original_img, (0, text_height)) + + # Draw the text on the white area + draw = ImageDraw.Draw(new_img) + text = f"SourceFileName:{blob_name}" + + # 10 pixels from the top and left of the image + x = 10 + y = 10 + draw.text((x, y), text, font=font, fill="black") + + output = io.BytesIO() + new_img.save(output, format="PNG") + output.seek(0) + + blob_client = await container_client.upload_blob(blob_name, output, overwrite=True) + if not self.user_delegation_key: + self.user_delegation_key = await service_client.get_user_delegation_key(start_time, expiry_time) + + if blob_client.account_name is not None: + sas_token = generate_blob_sas( + account_name=blob_client.account_name, + container_name=blob_client.container_name, + blob_name=blob_client.blob_name, + user_delegation_key=self.user_delegation_key, + permission=BlobSasPermissions(read=True), + expiry=expiry_time, + start=start_time, + ) + sas_uris.append(f"{blob_client.url}?{sas_token}") + + return sas_uris + + async def remove_blob(self, path: Optional[str] = None): + async with BlobServiceClient( + account_url=self.endpoint, credential=self.credential + ) as service_client, service_client.get_container_client(self.container) as container_client: + if not await container_client.exists(): + return + if path is None: + prefix = None + blobs = container_client.list_blob_names() + else: + prefix = os.path.splitext(os.path.basename(path))[0] + blobs = container_client.list_blob_names(name_starts_with=os.path.splitext(os.path.basename(prefix))[0]) + async for blob_path in blobs: + # This still supports PDFs split into individual pages, but we could remove in future to simplify code + if ( + prefix is not None + and ( + not re.match(rf"{prefix}-\d+\.pdf", blob_path) or not re.match(rf"{prefix}-\d+\.png", blob_path) + ) + ) or (path is not None and blob_path == os.path.basename(path)): + continue + logger.info("Removing blob %s", blob_path) + await container_client.delete_blob(blob_path) + + @classmethod + def sourcepage_from_file_page(cls, filename, page=0) -> str: + if os.path.splitext(filename)[1].lower() == ".pdf": + return f"{os.path.basename(filename)}#page={page+1}" + else: + return os.path.basename(filename) + + @classmethod + def blob_image_name_from_file_page(cls, filename, page=0) -> str: + return os.path.splitext(os.path.basename(filename))[0] + f"-{page}" + ".png" + + @classmethod + def blob_name_from_file_name(cls, filename) -> str: + return os.path.basename(filename) diff --git a/app/backend/prepdocslib/embeddings.py b/app/backend/prepdocslib/embeddings.py index a895f72373..5b17d98e14 100644 --- a/app/backend/prepdocslib/embeddings.py +++ b/app/backend/prepdocslib/embeddings.py @@ -1,253 +1,253 @@ -import logging -from abc import ABC -from typing import Awaitable, Callable, List, Optional, Union -from urllib.parse import urljoin - -import aiohttp -import tiktoken -from azure.core.credentials import AzureKeyCredential -from azure.core.credentials_async import AsyncTokenCredential -from azure.identity.aio import get_bearer_token_provider -from openai import AsyncAzureOpenAI, AsyncOpenAI, RateLimitError -from tenacity import ( - AsyncRetrying, - retry_if_exception_type, - stop_after_attempt, - wait_random_exponential, -) -from typing_extensions import TypedDict - -logger = logging.getLogger("ingester") - - -class EmbeddingBatch: - """ - Represents a batch of text that is going to be embedded - """ - - def __init__(self, texts: List[str], token_length: int): - self.texts = texts - self.token_length = token_length - - -class ExtraArgs(TypedDict, total=False): - dimensions: int - - -class OpenAIEmbeddings(ABC): - """ - Contains common logic across both OpenAI and Azure OpenAI embedding services - Can split source text into batches for more efficient embedding calls - """ - - SUPPORTED_BATCH_AOAI_MODEL = { - "text-embedding-ada-002": {"token_limit": 8100, "max_batch_size": 16}, - "text-embedding-3-small": {"token_limit": 8100, "max_batch_size": 16}, - "text-embedding-3-large": {"token_limit": 8100, "max_batch_size": 16}, - } - SUPPORTED_DIMENSIONS_MODEL = { - "text-embedding-ada-002": False, - "text-embedding-3-small": True, - "text-embedding-3-large": True, - } - - def __init__(self, open_ai_model_name: str, open_ai_dimensions: int, disable_batch: bool = False): - self.open_ai_model_name = open_ai_model_name - self.open_ai_dimensions = open_ai_dimensions - self.disable_batch = disable_batch - - async def create_client(self) -> AsyncOpenAI: - raise NotImplementedError - - def before_retry_sleep(self, retry_state): - logger.info("Rate limited on the OpenAI embeddings API, sleeping before retrying...") - - def calculate_token_length(self, text: str): - encoding = tiktoken.encoding_for_model(self.open_ai_model_name) - return len(encoding.encode(text)) - - def split_text_into_batches(self, texts: List[str]) -> List[EmbeddingBatch]: - batch_info = OpenAIEmbeddings.SUPPORTED_BATCH_AOAI_MODEL.get(self.open_ai_model_name) - if not batch_info: - raise NotImplementedError( - f"Model {self.open_ai_model_name} is not supported with batch embedding operations" - ) - - batch_token_limit = batch_info["token_limit"] - batch_max_size = batch_info["max_batch_size"] - batches: List[EmbeddingBatch] = [] - batch: List[str] = [] - batch_token_length = 0 - for text in texts: - text_token_length = self.calculate_token_length(text) - if batch_token_length + text_token_length >= batch_token_limit and len(batch) > 0: - batches.append(EmbeddingBatch(batch, batch_token_length)) - batch = [] - batch_token_length = 0 - - batch.append(text) - batch_token_length = batch_token_length + text_token_length - if len(batch) == batch_max_size: - batches.append(EmbeddingBatch(batch, batch_token_length)) - batch = [] - batch_token_length = 0 - - if len(batch) > 0: - batches.append(EmbeddingBatch(batch, batch_token_length)) - - return batches - - async def create_embedding_batch(self, texts: List[str], dimensions_args: ExtraArgs) -> List[List[float]]: - batches = self.split_text_into_batches(texts) - embeddings = [] - client = await self.create_client() - for batch in batches: - async for attempt in AsyncRetrying( - retry=retry_if_exception_type(RateLimitError), - wait=wait_random_exponential(min=15, max=60), - stop=stop_after_attempt(15), - before_sleep=self.before_retry_sleep, - ): - with attempt: - emb_response = await client.embeddings.create( - model=self.open_ai_model_name, input=batch.texts, **dimensions_args - ) - embeddings.extend([data.embedding for data in emb_response.data]) - logger.info( - "Computed embeddings in batch. Batch size: %d, Token count: %d", - len(batch.texts), - batch.token_length, - ) - - return embeddings - - async def create_embedding_single(self, text: str, dimensions_args: ExtraArgs) -> List[float]: - client = await self.create_client() - async for attempt in AsyncRetrying( - retry=retry_if_exception_type(RateLimitError), - wait=wait_random_exponential(min=15, max=60), - stop=stop_after_attempt(15), - before_sleep=self.before_retry_sleep, - ): - with attempt: - emb_response = await client.embeddings.create( - model=self.open_ai_model_name, input=text, **dimensions_args - ) - logger.info("Computed embedding for text section. Character count: %d", len(text)) - - return emb_response.data[0].embedding - - async def create_embeddings(self, texts: List[str]) -> List[List[float]]: - - dimensions_args: ExtraArgs = ( - {"dimensions": self.open_ai_dimensions} - if OpenAIEmbeddings.SUPPORTED_DIMENSIONS_MODEL.get(self.open_ai_model_name) - else {} - ) - - if not self.disable_batch and self.open_ai_model_name in OpenAIEmbeddings.SUPPORTED_BATCH_AOAI_MODEL: - return await self.create_embedding_batch(texts, dimensions_args) - - return [await self.create_embedding_single(text, dimensions_args) for text in texts] - - -class AzureOpenAIEmbeddingService(OpenAIEmbeddings): - """ - Class for using Azure OpenAI embeddings - To learn more please visit https://learn.microsoft.com/azure/ai-services/openai/concepts/understand-embeddings - """ - - def __init__( - self, - open_ai_service: Union[str, None], - open_ai_deployment: Union[str, None], - open_ai_model_name: str, - open_ai_dimensions: int, - credential: Union[AsyncTokenCredential, AzureKeyCredential], - disable_batch: bool = False, - ): - super().__init__(open_ai_model_name, open_ai_dimensions, disable_batch) - self.open_ai_service = open_ai_service - self.open_ai_deployment = open_ai_deployment - self.credential = credential - - async def create_client(self) -> AsyncOpenAI: - class AuthArgs(TypedDict, total=False): - api_key: str - azure_ad_token_provider: Callable[[], Union[str, Awaitable[str]]] - - auth_args = AuthArgs() - if isinstance(self.credential, AzureKeyCredential): - auth_args["api_key"] = self.credential.key - elif isinstance(self.credential, AsyncTokenCredential): - auth_args["azure_ad_token_provider"] = get_bearer_token_provider( - self.credential, "https://cognitiveservices.azure.com/.default" - ) - else: - raise TypeError("Invalid credential type") - - return AsyncAzureOpenAI( - azure_endpoint=f"https://{self.open_ai_service}.openai.azure.com", - azure_deployment=self.open_ai_deployment, - api_version="2023-05-15", - **auth_args, - ) - - -class OpenAIEmbeddingService(OpenAIEmbeddings): - """ - Class for using OpenAI embeddings - To learn more please visit https://platform.openai.com/docs/guides/embeddings - """ - - def __init__( - self, - open_ai_model_name: str, - open_ai_dimensions: int, - credential: str, - organization: Optional[str] = None, - disable_batch: bool = False, - ): - super().__init__(open_ai_model_name, open_ai_dimensions, disable_batch) - self.credential = credential - self.organization = organization - - async def create_client(self) -> AsyncOpenAI: - return AsyncOpenAI(api_key=self.credential, organization=self.organization) - - -class ImageEmbeddings: - """ - Class for using image embeddings from Azure AI Vision - To learn more, please visit https://learn.microsoft.com/azure/ai-services/computer-vision/how-to/image-retrieval#call-the-vectorize-image-api - """ - - def __init__(self, endpoint: str, token_provider: Callable[[], Awaitable[str]]): - self.token_provider = token_provider - self.endpoint = endpoint - - async def create_embeddings(self, blob_urls: List[str]) -> List[List[float]]: - endpoint = urljoin(self.endpoint, "computervision/retrieval:vectorizeImage") - headers = {"Content-Type": "application/json"} - params = {"api-version": "2023-02-01-preview", "modelVersion": "latest"} - headers["Authorization"] = "Bearer " + await self.token_provider() - - embeddings: List[List[float]] = [] - async with aiohttp.ClientSession(headers=headers) as session: - for blob_url in blob_urls: - async for attempt in AsyncRetrying( - retry=retry_if_exception_type(Exception), - wait=wait_random_exponential(min=15, max=60), - stop=stop_after_attempt(15), - before_sleep=self.before_retry_sleep, - ): - with attempt: - body = {"url": blob_url} - async with session.post(url=endpoint, params=params, json=body) as resp: - resp_json = await resp.json() - embeddings.append(resp_json["vector"]) - - return embeddings - - def before_retry_sleep(self, retry_state): - logger.info("Rate limited on the Vision embeddings API, sleeping before retrying...") +import logging +from abc import ABC +from typing import Awaitable, Callable, List, Optional, Union +from urllib.parse import urljoin + +import aiohttp +import tiktoken +from azure.core.credentials import AzureKeyCredential +from azure.core.credentials_async import AsyncTokenCredential +from azure.identity.aio import get_bearer_token_provider +from openai import AsyncAzureOpenAI, AsyncOpenAI, RateLimitError +from tenacity import ( + AsyncRetrying, + retry_if_exception_type, + stop_after_attempt, + wait_random_exponential, +) +from typing_extensions import TypedDict + +logger = logging.getLogger("ingester") + + +class EmbeddingBatch: + """ + Represents a batch of text that is going to be embedded + """ + + def __init__(self, texts: List[str], token_length: int): + self.texts = texts + self.token_length = token_length + + +class ExtraArgs(TypedDict, total=False): + dimensions: int + + +class OpenAIEmbeddings(ABC): + """ + Contains common logic across both OpenAI and Azure OpenAI embedding services + Can split source text into batches for more efficient embedding calls + """ + + SUPPORTED_BATCH_AOAI_MODEL = { + "text-embedding-ada-002": {"token_limit": 8100, "max_batch_size": 16}, + "text-embedding-3-small": {"token_limit": 8100, "max_batch_size": 16}, + "text-embedding-3-large": {"token_limit": 8100, "max_batch_size": 16}, + } + SUPPORTED_DIMENSIONS_MODEL = { + "text-embedding-ada-002": False, + "text-embedding-3-small": True, + "text-embedding-3-large": True, + } + + def __init__(self, open_ai_model_name: str, open_ai_dimensions: int, disable_batch: bool = False): + self.open_ai_model_name = open_ai_model_name + self.open_ai_dimensions = open_ai_dimensions + self.disable_batch = disable_batch + + async def create_client(self) -> AsyncOpenAI: + raise NotImplementedError + + def before_retry_sleep(self, retry_state): + logger.info("Rate limited on the OpenAI embeddings API, sleeping before retrying...") + + def calculate_token_length(self, text: str): + encoding = tiktoken.encoding_for_model(self.open_ai_model_name) + return len(encoding.encode(text)) + + def split_text_into_batches(self, texts: List[str]) -> List[EmbeddingBatch]: + batch_info = OpenAIEmbeddings.SUPPORTED_BATCH_AOAI_MODEL.get(self.open_ai_model_name) + if not batch_info: + raise NotImplementedError( + f"Model {self.open_ai_model_name} is not supported with batch embedding operations" + ) + + batch_token_limit = batch_info["token_limit"] + batch_max_size = batch_info["max_batch_size"] + batches: List[EmbeddingBatch] = [] + batch: List[str] = [] + batch_token_length = 0 + for text in texts: + text_token_length = self.calculate_token_length(text) + if batch_token_length + text_token_length >= batch_token_limit and len(batch) > 0: + batches.append(EmbeddingBatch(batch, batch_token_length)) + batch = [] + batch_token_length = 0 + + batch.append(text) + batch_token_length = batch_token_length + text_token_length + if len(batch) == batch_max_size: + batches.append(EmbeddingBatch(batch, batch_token_length)) + batch = [] + batch_token_length = 0 + + if len(batch) > 0: + batches.append(EmbeddingBatch(batch, batch_token_length)) + + return batches + + async def create_embedding_batch(self, texts: List[str], dimensions_args: ExtraArgs) -> List[List[float]]: + batches = self.split_text_into_batches(texts) + embeddings = [] + client = await self.create_client() + for batch in batches: + async for attempt in AsyncRetrying( + retry=retry_if_exception_type(RateLimitError), + wait=wait_random_exponential(min=15, max=60), + stop=stop_after_attempt(15), + before_sleep=self.before_retry_sleep, + ): + with attempt: + emb_response = await client.embeddings.create( + model=self.open_ai_model_name, input=batch.texts, **dimensions_args + ) + embeddings.extend([data.embedding for data in emb_response.data]) + logger.info( + "Computed embeddings in batch. Batch size: %d, Token count: %d", + len(batch.texts), + batch.token_length, + ) + + return embeddings + + async def create_embedding_single(self, text: str, dimensions_args: ExtraArgs) -> List[float]: + client = await self.create_client() + async for attempt in AsyncRetrying( + retry=retry_if_exception_type(RateLimitError), + wait=wait_random_exponential(min=15, max=60), + stop=stop_after_attempt(15), + before_sleep=self.before_retry_sleep, + ): + with attempt: + emb_response = await client.embeddings.create( + model=self.open_ai_model_name, input=text, **dimensions_args + ) + logger.info("Computed embedding for text section. Character count: %d", len(text)) + + return emb_response.data[0].embedding + + async def create_embeddings(self, texts: List[str]) -> List[List[float]]: + + dimensions_args: ExtraArgs = ( + {"dimensions": self.open_ai_dimensions} + if OpenAIEmbeddings.SUPPORTED_DIMENSIONS_MODEL.get(self.open_ai_model_name) + else {} + ) + + if not self.disable_batch and self.open_ai_model_name in OpenAIEmbeddings.SUPPORTED_BATCH_AOAI_MODEL: + return await self.create_embedding_batch(texts, dimensions_args) + + return [await self.create_embedding_single(text, dimensions_args) for text in texts] + + +class AzureOpenAIEmbeddingService(OpenAIEmbeddings): + """ + Class for using Azure OpenAI embeddings + To learn more please visit https://learn.microsoft.com/azure/ai-services/openai/concepts/understand-embeddings + """ + + def __init__( + self, + open_ai_service: Union[str, None], + open_ai_deployment: Union[str, None], + open_ai_model_name: str, + open_ai_dimensions: int, + credential: Union[AsyncTokenCredential, AzureKeyCredential], + disable_batch: bool = False, + ): + super().__init__(open_ai_model_name, open_ai_dimensions, disable_batch) + self.open_ai_service = open_ai_service + self.open_ai_deployment = open_ai_deployment + self.credential = credential + + async def create_client(self) -> AsyncOpenAI: + class AuthArgs(TypedDict, total=False): + api_key: str + azure_ad_token_provider: Callable[[], Union[str, Awaitable[str]]] + + auth_args = AuthArgs() + if isinstance(self.credential, AzureKeyCredential): + auth_args["api_key"] = self.credential.key + elif isinstance(self.credential, AsyncTokenCredential): + auth_args["azure_ad_token_provider"] = get_bearer_token_provider( + self.credential, "https://cognitiveservices.azure.com/.default" + ) + else: + raise TypeError("Invalid credential type") + + return AsyncAzureOpenAI( + azure_endpoint=f"https://{self.open_ai_service}.openai.azure.com", + azure_deployment=self.open_ai_deployment, + api_version="2023-05-15", + **auth_args, + ) + + +class OpenAIEmbeddingService(OpenAIEmbeddings): + """ + Class for using OpenAI embeddings + To learn more please visit https://platform.openai.com/docs/guides/embeddings + """ + + def __init__( + self, + open_ai_model_name: str, + open_ai_dimensions: int, + credential: str, + organization: Optional[str] = None, + disable_batch: bool = False, + ): + super().__init__(open_ai_model_name, open_ai_dimensions, disable_batch) + self.credential = credential + self.organization = organization + + async def create_client(self) -> AsyncOpenAI: + return AsyncOpenAI(api_key=self.credential, organization=self.organization) + + +class ImageEmbeddings: + """ + Class for using image embeddings from Azure AI Vision + To learn more, please visit https://learn.microsoft.com/azure/ai-services/computer-vision/how-to/image-retrieval#call-the-vectorize-image-api + """ + + def __init__(self, endpoint: str, token_provider: Callable[[], Awaitable[str]]): + self.token_provider = token_provider + self.endpoint = endpoint + + async def create_embeddings(self, blob_urls: List[str]) -> List[List[float]]: + endpoint = urljoin(self.endpoint, "computervision/retrieval:vectorizeImage") + headers = {"Content-Type": "application/json"} + params = {"api-version": "2023-02-01-preview", "modelVersion": "latest"} + headers["Authorization"] = "Bearer " + await self.token_provider() + + embeddings: List[List[float]] = [] + async with aiohttp.ClientSession(headers=headers) as session: + for blob_url in blob_urls: + async for attempt in AsyncRetrying( + retry=retry_if_exception_type(Exception), + wait=wait_random_exponential(min=15, max=60), + stop=stop_after_attempt(15), + before_sleep=self.before_retry_sleep, + ): + with attempt: + body = {"url": blob_url} + async with session.post(url=endpoint, params=params, json=body) as resp: + resp_json = await resp.json() + embeddings.append(resp_json["vector"]) + + return embeddings + + def before_retry_sleep(self, retry_state): + logger.info("Rate limited on the Vision embeddings API, sleeping before retrying...") diff --git a/app/backend/prepdocslib/fileprocessor.py b/app/backend/prepdocslib/fileprocessor.py index 3b58130db8..e40649708a 100644 --- a/app/backend/prepdocslib/fileprocessor.py +++ b/app/backend/prepdocslib/fileprocessor.py @@ -1,10 +1,10 @@ -from dataclasses import dataclass - -from .parser import Parser -from .textsplitter import TextSplitter - - -@dataclass(frozen=True) -class FileProcessor: - parser: Parser - splitter: TextSplitter +from dataclasses import dataclass + +from .parser import Parser +from .textsplitter import TextSplitter + + +@dataclass(frozen=True) +class FileProcessor: + parser: Parser + splitter: TextSplitter diff --git a/app/backend/prepdocslib/filestrategy.py b/app/backend/prepdocslib/filestrategy.py index e8cab16983..8241466eaf 100644 --- a/app/backend/prepdocslib/filestrategy.py +++ b/app/backend/prepdocslib/filestrategy.py @@ -1,133 +1,133 @@ -import logging -from typing import List, Optional - -from .blobmanager import BlobManager -from .embeddings import ImageEmbeddings, OpenAIEmbeddings -from .fileprocessor import FileProcessor -from .listfilestrategy import File, ListFileStrategy -from .searchmanager import SearchManager, Section -from .strategy import DocumentAction, SearchInfo, Strategy - -logger = logging.getLogger("ingester") - - -async def parse_file( - file: File, - file_processors: dict[str, FileProcessor], - category: Optional[str] = None, - image_embeddings: Optional[ImageEmbeddings] = None, -) -> List[Section]: - key = file.file_extension() - processor = file_processors.get(key) - if processor is None: - logger.info("Skipping '%s', no parser found.", file.filename()) - return [] - logger.info("Ingesting '%s'", file.filename()) - pages = [page async for page in processor.parser.parse(content=file.content)] - logger.info("Splitting '%s' into sections", file.filename()) - if image_embeddings: - logger.warning("Each page will be split into smaller chunks of text, but images will be of the entire page.") - sections = [ - Section(split_page, content=file, category=category) for split_page in processor.splitter.split_pages(pages) - ] - return sections - - -class FileStrategy(Strategy): - """ - Strategy for ingesting documents into a search service from files stored either locally or in a data lake storage account - """ - - def __init__( - self, - list_file_strategy: ListFileStrategy, - blob_manager: BlobManager, - search_info: SearchInfo, - file_processors: dict[str, FileProcessor], - document_action: DocumentAction = DocumentAction.Add, - embeddings: Optional[OpenAIEmbeddings] = None, - image_embeddings: Optional[ImageEmbeddings] = None, - search_analyzer_name: Optional[str] = None, - use_acls: bool = False, - category: Optional[str] = None, - ): - self.list_file_strategy = list_file_strategy - self.blob_manager = blob_manager - self.file_processors = file_processors - self.document_action = document_action - self.embeddings = embeddings - self.image_embeddings = image_embeddings - self.search_analyzer_name = search_analyzer_name - self.search_info = search_info - self.use_acls = use_acls - self.category = category - - async def setup(self): - search_manager = SearchManager( - self.search_info, - self.search_analyzer_name, - self.use_acls, - False, - self.embeddings, - search_images=self.image_embeddings is not None, - ) - await search_manager.create_index() - - async def run(self): - search_manager = SearchManager( - self.search_info, self.search_analyzer_name, self.use_acls, False, self.embeddings - ) - if self.document_action == DocumentAction.Add: - files = self.list_file_strategy.list() - async for file in files: - try: - sections = await parse_file(file, self.file_processors, self.category, self.image_embeddings) - if sections: - blob_sas_uris = await self.blob_manager.upload_blob(file) - blob_image_embeddings: Optional[List[List[float]]] = None - if self.image_embeddings and blob_sas_uris: - blob_image_embeddings = await self.image_embeddings.create_embeddings(blob_sas_uris) - await search_manager.update_content(sections, blob_image_embeddings, url=file.url) - finally: - if file: - file.close() - elif self.document_action == DocumentAction.Remove: - paths = self.list_file_strategy.list_paths() - async for path in paths: - await self.blob_manager.remove_blob(path) - await search_manager.remove_content(path) - elif self.document_action == DocumentAction.RemoveAll: - await self.blob_manager.remove_blob() - await search_manager.remove_content() - - -class UploadUserFileStrategy: - """ - Strategy for ingesting a file that has already been uploaded to a ADLS2 storage account - """ - - def __init__( - self, - search_info: SearchInfo, - file_processors: dict[str, FileProcessor], - embeddings: Optional[OpenAIEmbeddings] = None, - image_embeddings: Optional[ImageEmbeddings] = None, - ): - self.file_processors = file_processors - self.embeddings = embeddings - self.image_embeddings = image_embeddings - self.search_info = search_info - self.search_manager = SearchManager(self.search_info, None, True, False, self.embeddings) - - async def add_file(self, file: File): - if self.image_embeddings: - logging.warning("Image embeddings are not currently supported for the user upload feature") - sections = await parse_file(file, self.file_processors) - if sections: - await self.search_manager.update_content(sections, url=file.url) - - async def remove_file(self, filename: str, oid: str): - if filename is None or filename == "": - logging.warning("Filename is required to remove a file") - return - await self.search_manager.remove_content(filename, oid) +import logging +from typing import List, Optional + +from .blobmanager import BlobManager +from .embeddings import ImageEmbeddings, OpenAIEmbeddings +from .fileprocessor import FileProcessor +from .listfilestrategy import File, ListFileStrategy +from .searchmanager import SearchManager, Section +from .strategy import DocumentAction, SearchInfo, Strategy + +logger = logging.getLogger("ingester") + + +async def parse_file( + file: File, + file_processors: dict[str, FileProcessor], + category: Optional[str] = None, + image_embeddings: Optional[ImageEmbeddings] = None, +) -> List[Section]: + key = file.file_extension() + processor = file_processors.get(key) + if processor is None: + logger.info("Skipping '%s', no parser found.", file.filename()) + return [] + logger.info("Ingesting '%s'", file.filename()) + pages = [page async for page in processor.parser.parse(content=file.content)] + logger.info("Splitting '%s' into sections", file.filename()) + if image_embeddings: + logger.warning("Each page will be split into smaller chunks of text, but images will be of the entire page.") + sections = [ + Section(split_page, content=file, category=category) for split_page in processor.splitter.split_pages(pages) + ] + return sections + + +class FileStrategy(Strategy): + """ + Strategy for ingesting documents into a search service from files stored either locally or in a data lake storage account + """ + + def __init__( + self, + list_file_strategy: ListFileStrategy, + blob_manager: BlobManager, + search_info: SearchInfo, + file_processors: dict[str, FileProcessor], + document_action: DocumentAction = DocumentAction.Add, + embeddings: Optional[OpenAIEmbeddings] = None, + image_embeddings: Optional[ImageEmbeddings] = None, + search_analyzer_name: Optional[str] = None, + use_acls: bool = False, + category: Optional[str] = None, + ): + self.list_file_strategy = list_file_strategy + self.blob_manager = blob_manager + self.file_processors = file_processors + self.document_action = document_action + self.embeddings = embeddings + self.image_embeddings = image_embeddings + self.search_analyzer_name = search_analyzer_name + self.search_info = search_info + self.use_acls = use_acls + self.category = category + + async def setup(self): + search_manager = SearchManager( + self.search_info, + self.search_analyzer_name, + self.use_acls, + False, + self.embeddings, + search_images=self.image_embeddings is not None, + ) + await search_manager.create_index() + + async def run(self): + search_manager = SearchManager( + self.search_info, self.search_analyzer_name, self.use_acls, False, self.embeddings + ) + if self.document_action == DocumentAction.Add: + files = self.list_file_strategy.list() + async for file in files: + try: + sections = await parse_file(file, self.file_processors, self.category, self.image_embeddings) + if sections: + blob_sas_uris = await self.blob_manager.upload_blob(file) + blob_image_embeddings: Optional[List[List[float]]] = None + if self.image_embeddings and blob_sas_uris: + blob_image_embeddings = await self.image_embeddings.create_embeddings(blob_sas_uris) + await search_manager.update_content(sections, blob_image_embeddings, url=file.url) + finally: + if file: + file.close() + elif self.document_action == DocumentAction.Remove: + paths = self.list_file_strategy.list_paths() + async for path in paths: + await self.blob_manager.remove_blob(path) + await search_manager.remove_content(path) + elif self.document_action == DocumentAction.RemoveAll: + await self.blob_manager.remove_blob() + await search_manager.remove_content() + + +class UploadUserFileStrategy: + """ + Strategy for ingesting a file that has already been uploaded to a ADLS2 storage account + """ + + def __init__( + self, + search_info: SearchInfo, + file_processors: dict[str, FileProcessor], + embeddings: Optional[OpenAIEmbeddings] = None, + image_embeddings: Optional[ImageEmbeddings] = None, + ): + self.file_processors = file_processors + self.embeddings = embeddings + self.image_embeddings = image_embeddings + self.search_info = search_info + self.search_manager = SearchManager(self.search_info, None, True, False, self.embeddings) + + async def add_file(self, file: File): + if self.image_embeddings: + logging.warning("Image embeddings are not currently supported for the user upload feature") + sections = await parse_file(file, self.file_processors) + if sections: + await self.search_manager.update_content(sections, url=file.url) + + async def remove_file(self, filename: str, oid: str): + if filename is None or filename == "": + logging.warning("Filename is required to remove a file") + return + await self.search_manager.remove_content(filename, oid) diff --git a/app/backend/prepdocslib/htmlparser.py b/app/backend/prepdocslib/htmlparser.py index 0acf88b050..8ce10d50e5 100644 --- a/app/backend/prepdocslib/htmlparser.py +++ b/app/backend/prepdocslib/htmlparser.py @@ -1,49 +1,49 @@ -import logging -import re -from typing import IO, AsyncGenerator - -from bs4 import BeautifulSoup - -from .page import Page -from .parser import Parser - -logger = logging.getLogger("ingester") - - -def cleanup_data(data: str) -> str: - """Cleans up the given content using regexes - Args: - data: (str): The data to clean up. - Returns: - str: The cleaned up data. - """ - # match two or more newlines and replace them with one new line - output = re.sub(r"\n{2,}", "\n", data) - # match two or more spaces that are not newlines and replace them with one space - output = re.sub(r"[^\S\n]{2,}", " ", output) - # match two or more hyphens and replace them with two hyphens - output = re.sub(r"-{2,}", "--", output) - - return output.strip() - - -class LocalHTMLParser(Parser): - """Parses HTML text into Page objects.""" - - async def parse(self, content: IO) -> AsyncGenerator[Page, None]: - """Parses the given content. - To learn more, please visit https://pypi.org/project/beautifulsoup4/ - Args: - content (IO): The content to parse. - Returns: - Page: The parsed html Page. - """ - logger.info("Extracting text from '%s' using local HTML parser (BeautifulSoup)", content.name) - - data = content.read() - soup = BeautifulSoup(data, "html.parser") - - # Get text only from html file - result = soup.get_text() - - yield Page(0, 0, text=cleanup_data(result)) +import logging +import re +from typing import IO, AsyncGenerator + +from bs4 import BeautifulSoup + +from .page import Page +from .parser import Parser + +logger = logging.getLogger("ingester") + + +def cleanup_data(data: str) -> str: + """Cleans up the given content using regexes + Args: + data: (str): The data to clean up. + Returns: + str: The cleaned up data. + """ + # match two or more newlines and replace them with one new line + output = re.sub(r"\n{2,}", "\n", data) + # match two or more spaces that are not newlines and replace them with one space + output = re.sub(r"[^\S\n]{2,}", " ", output) + # match two or more hyphens and replace them with two hyphens + output = re.sub(r"-{2,}", "--", output) + + return output.strip() + + +class LocalHTMLParser(Parser): + """Parses HTML text into Page objects.""" + + async def parse(self, content: IO) -> AsyncGenerator[Page, None]: + """Parses the given content. + To learn more, please visit https://pypi.org/project/beautifulsoup4/ + Args: + content (IO): The content to parse. + Returns: + Page: The parsed html Page. + """ + logger.info("Extracting text from '%s' using local HTML parser (BeautifulSoup)", content.name) + + data = content.read() + soup = BeautifulSoup(data, "html.parser") + + # Get text only from html file + result = soup.get_text() + + yield Page(0, 0, text=cleanup_data(result)) diff --git a/app/backend/prepdocslib/integratedvectorizerstrategy.py b/app/backend/prepdocslib/integratedvectorizerstrategy.py index 0c475b9f52..be6a7ca5b1 100644 --- a/app/backend/prepdocslib/integratedvectorizerstrategy.py +++ b/app/backend/prepdocslib/integratedvectorizerstrategy.py @@ -1,204 +1,204 @@ -import logging -from typing import Optional - -from azure.search.documents.indexes._generated.models import ( - NativeBlobSoftDeleteDeletionDetectionPolicy, -) -from azure.search.documents.indexes.models import ( - AzureOpenAIEmbeddingSkill, - AzureOpenAIParameters, - AzureOpenAIVectorizer, - FieldMapping, - IndexProjectionMode, - InputFieldMappingEntry, - OutputFieldMappingEntry, - SearchIndexer, - SearchIndexerDataContainer, - SearchIndexerDataSourceConnection, - SearchIndexerIndexProjections, - SearchIndexerIndexProjectionSelector, - SearchIndexerIndexProjectionsParameters, - SearchIndexerSkillset, - SplitSkill, -) - -from .blobmanager import BlobManager -from .embeddings import AzureOpenAIEmbeddingService -from .listfilestrategy import ListFileStrategy -from .searchmanager import SearchManager -from .strategy import DocumentAction, SearchInfo, Strategy - -logger = logging.getLogger("ingester") - - -class IntegratedVectorizerStrategy(Strategy): - """ - Strategy for ingesting and vectorizing documents into a search service from files stored storage account - """ - - def __init__( - self, - list_file_strategy: ListFileStrategy, - blob_manager: BlobManager, - search_info: SearchInfo, - embeddings: Optional[AzureOpenAIEmbeddingService], - subscription_id: str, - search_service_user_assigned_id: str, - document_action: DocumentAction = DocumentAction.Add, - search_analyzer_name: Optional[str] = None, - use_acls: bool = False, - category: Optional[str] = None, - ): - if not embeddings or not isinstance(embeddings, AzureOpenAIEmbeddingService): - raise Exception("Expecting AzureOpenAI embedding service") - - self.list_file_strategy = list_file_strategy - self.blob_manager = blob_manager - self.document_action = document_action - self.embeddings = embeddings - self.subscription_id = subscription_id - self.search_user_assigned_identity = search_service_user_assigned_id - self.search_analyzer_name = search_analyzer_name - self.use_acls = use_acls - self.category = category - self.search_info = search_info - - async def create_embedding_skill(self, index_name: str): - skillset_name = f"{index_name}-skillset" - - split_skill = SplitSkill( - description="Split skill to chunk documents", - text_split_mode="pages", - context="/document", - maximum_page_length=2048, - page_overlap_length=20, - inputs=[ - InputFieldMappingEntry(name="text", source="/document/content"), - ], - outputs=[OutputFieldMappingEntry(name="textItems", target_name="pages")], - ) - - if self.embeddings is None: - raise ValueError("Expecting Azure Open AI instance") - - embedding_skill = AzureOpenAIEmbeddingSkill( - description="Skill to generate embeddings via Azure OpenAI", - context="/document/pages/*", - resource_uri=f"https://{self.embeddings.open_ai_service}.openai.azure.com", - deployment_id=self.embeddings.open_ai_deployment, - inputs=[ - InputFieldMappingEntry(name="text", source="/document/pages/*"), - ], - outputs=[OutputFieldMappingEntry(name="embedding", target_name="vector")], - ) - - index_projections = SearchIndexerIndexProjections( - selectors=[ - SearchIndexerIndexProjectionSelector( - target_index_name=index_name, - parent_key_field_name="parent_id", - source_context="/document/pages/*", - mappings=[ - InputFieldMappingEntry(name="content", source="/document/pages/*"), - InputFieldMappingEntry(name="embedding", source="/document/pages/*/vector"), - InputFieldMappingEntry(name="sourcepage", source="/document/metadata_storage_name"), - ], - ), - ], - parameters=SearchIndexerIndexProjectionsParameters( - projection_mode=IndexProjectionMode.SKIP_INDEXING_PARENT_DOCUMENTS - ), - ) - - skillset = SearchIndexerSkillset( - name=skillset_name, - description="Skillset to chunk documents and generate embeddings", - skills=[split_skill, embedding_skill], - index_projections=index_projections, - ) - - return skillset - - async def setup(self): - search_manager = SearchManager( - search_info=self.search_info, - search_analyzer_name=self.search_analyzer_name, - use_acls=self.use_acls, - use_int_vectorization=True, - embeddings=self.embeddings, - search_images=False, - ) - - if self.embeddings is None: - raise ValueError("Expecting Azure Open AI instance") - - await search_manager.create_index( - vectorizers=[ - AzureOpenAIVectorizer( - name=f"{self.search_info.index_name}-vectorizer", - kind="azureOpenAI", - azure_open_ai_parameters=AzureOpenAIParameters( - resource_uri=f"https://{self.embeddings.open_ai_service}.openai.azure.com", - deployment_id=self.embeddings.open_ai_deployment, - ), - ), - ] - ) - - # create indexer client - ds_client = self.search_info.create_search_indexer_client() - ds_container = SearchIndexerDataContainer(name=self.blob_manager.container) - data_source_connection = SearchIndexerDataSourceConnection( - name=f"{self.search_info.index_name}-blob", - type="azureblob", - connection_string=self.blob_manager.get_managedidentity_connectionstring(), - container=ds_container, - data_deletion_detection_policy=NativeBlobSoftDeleteDeletionDetectionPolicy(), - ) - - await ds_client.create_or_update_data_source_connection(data_source_connection) - logger.info("Search indexer data source connection updated.") - - embedding_skillset = await self.create_embedding_skill(self.search_info.index_name) - await ds_client.create_or_update_skillset(embedding_skillset) - await ds_client.close() - - async def run(self): - if self.document_action == DocumentAction.Add: - files = self.list_file_strategy.list() - async for file in files: - try: - await self.blob_manager.upload_blob(file) - finally: - if file: - file.close() - elif self.document_action == DocumentAction.Remove: - paths = self.list_file_strategy.list_paths() - async for path in paths: - await self.blob_manager.remove_blob(path) - elif self.document_action == DocumentAction.RemoveAll: - await self.blob_manager.remove_blob() - - # Create an indexer - indexer_name = f"{self.search_info.index_name}-indexer" - - indexer = SearchIndexer( - name=indexer_name, - description="Indexer to index documents and generate embeddings", - skillset_name=f"{self.search_info.index_name}-skillset", - target_index_name=self.search_info.index_name, - data_source_name=f"{self.search_info.index_name}-blob", - # Map the metadata_storage_name field to the title field in the index to display the PDF title in the search results - field_mappings=[FieldMapping(source_field_name="metadata_storage_name", target_field_name="title")], - ) - - indexer_client = self.search_info.create_search_indexer_client() - indexer_result = await indexer_client.create_or_update_indexer(indexer) - - # Run the indexer - await indexer_client.run_indexer(indexer_name) - await indexer_client.close() - - logger.info( - f"Successfully created index, indexer: {indexer_result.name}, and skillset. Please navigate to search service in Azure Portal to view the status of the indexer." - ) +import logging +from typing import Optional + +from azure.search.documents.indexes._generated.models import ( + NativeBlobSoftDeleteDeletionDetectionPolicy, +) +from azure.search.documents.indexes.models import ( + AzureOpenAIEmbeddingSkill, + AzureOpenAIParameters, + AzureOpenAIVectorizer, + FieldMapping, + IndexProjectionMode, + InputFieldMappingEntry, + OutputFieldMappingEntry, + SearchIndexer, + SearchIndexerDataContainer, + SearchIndexerDataSourceConnection, + SearchIndexerIndexProjections, + SearchIndexerIndexProjectionSelector, + SearchIndexerIndexProjectionsParameters, + SearchIndexerSkillset, + SplitSkill, +) + +from .blobmanager import BlobManager +from .embeddings import AzureOpenAIEmbeddingService +from .listfilestrategy import ListFileStrategy +from .searchmanager import SearchManager +from .strategy import DocumentAction, SearchInfo, Strategy + +logger = logging.getLogger("ingester") + + +class IntegratedVectorizerStrategy(Strategy): + """ + Strategy for ingesting and vectorizing documents into a search service from files stored storage account + """ + + def __init__( + self, + list_file_strategy: ListFileStrategy, + blob_manager: BlobManager, + search_info: SearchInfo, + embeddings: Optional[AzureOpenAIEmbeddingService], + subscription_id: str, + search_service_user_assigned_id: str, + document_action: DocumentAction = DocumentAction.Add, + search_analyzer_name: Optional[str] = None, + use_acls: bool = False, + category: Optional[str] = None, + ): + if not embeddings or not isinstance(embeddings, AzureOpenAIEmbeddingService): + raise Exception("Expecting AzureOpenAI embedding service") + + self.list_file_strategy = list_file_strategy + self.blob_manager = blob_manager + self.document_action = document_action + self.embeddings = embeddings + self.subscription_id = subscription_id + self.search_user_assigned_identity = search_service_user_assigned_id + self.search_analyzer_name = search_analyzer_name + self.use_acls = use_acls + self.category = category + self.search_info = search_info + + async def create_embedding_skill(self, index_name: str): + skillset_name = f"{index_name}-skillset" + + split_skill = SplitSkill( + description="Split skill to chunk documents", + text_split_mode="pages", + context="/document", + maximum_page_length=2048, + page_overlap_length=20, + inputs=[ + InputFieldMappingEntry(name="text", source="/document/content"), + ], + outputs=[OutputFieldMappingEntry(name="textItems", target_name="pages")], + ) + + if self.embeddings is None: + raise ValueError("Expecting Azure Open AI instance") + + embedding_skill = AzureOpenAIEmbeddingSkill( + description="Skill to generate embeddings via Azure OpenAI", + context="/document/pages/*", + resource_uri=f"https://{self.embeddings.open_ai_service}.openai.azure.com", + deployment_id=self.embeddings.open_ai_deployment, + inputs=[ + InputFieldMappingEntry(name="text", source="/document/pages/*"), + ], + outputs=[OutputFieldMappingEntry(name="embedding", target_name="vector")], + ) + + index_projections = SearchIndexerIndexProjections( + selectors=[ + SearchIndexerIndexProjectionSelector( + target_index_name=index_name, + parent_key_field_name="parent_id", + source_context="/document/pages/*", + mappings=[ + InputFieldMappingEntry(name="content", source="/document/pages/*"), + InputFieldMappingEntry(name="embedding", source="/document/pages/*/vector"), + InputFieldMappingEntry(name="sourcepage", source="/document/metadata_storage_name"), + ], + ), + ], + parameters=SearchIndexerIndexProjectionsParameters( + projection_mode=IndexProjectionMode.SKIP_INDEXING_PARENT_DOCUMENTS + ), + ) + + skillset = SearchIndexerSkillset( + name=skillset_name, + description="Skillset to chunk documents and generate embeddings", + skills=[split_skill, embedding_skill], + index_projections=index_projections, + ) + + return skillset + + async def setup(self): + search_manager = SearchManager( + search_info=self.search_info, + search_analyzer_name=self.search_analyzer_name, + use_acls=self.use_acls, + use_int_vectorization=True, + embeddings=self.embeddings, + search_images=False, + ) + + if self.embeddings is None: + raise ValueError("Expecting Azure Open AI instance") + + await search_manager.create_index( + vectorizers=[ + AzureOpenAIVectorizer( + name=f"{self.search_info.index_name}-vectorizer", + kind="azureOpenAI", + azure_open_ai_parameters=AzureOpenAIParameters( + resource_uri=f"https://{self.embeddings.open_ai_service}.openai.azure.com", + deployment_id=self.embeddings.open_ai_deployment, + ), + ), + ] + ) + + # create indexer client + ds_client = self.search_info.create_search_indexer_client() + ds_container = SearchIndexerDataContainer(name=self.blob_manager.container) + data_source_connection = SearchIndexerDataSourceConnection( + name=f"{self.search_info.index_name}-blob", + type="azureblob", + connection_string=self.blob_manager.get_managedidentity_connectionstring(), + container=ds_container, + data_deletion_detection_policy=NativeBlobSoftDeleteDeletionDetectionPolicy(), + ) + + await ds_client.create_or_update_data_source_connection(data_source_connection) + logger.info("Search indexer data source connection updated.") + + embedding_skillset = await self.create_embedding_skill(self.search_info.index_name) + await ds_client.create_or_update_skillset(embedding_skillset) + await ds_client.close() + + async def run(self): + if self.document_action == DocumentAction.Add: + files = self.list_file_strategy.list() + async for file in files: + try: + await self.blob_manager.upload_blob(file) + finally: + if file: + file.close() + elif self.document_action == DocumentAction.Remove: + paths = self.list_file_strategy.list_paths() + async for path in paths: + await self.blob_manager.remove_blob(path) + elif self.document_action == DocumentAction.RemoveAll: + await self.blob_manager.remove_blob() + + # Create an indexer + indexer_name = f"{self.search_info.index_name}-indexer" + + indexer = SearchIndexer( + name=indexer_name, + description="Indexer to index documents and generate embeddings", + skillset_name=f"{self.search_info.index_name}-skillset", + target_index_name=self.search_info.index_name, + data_source_name=f"{self.search_info.index_name}-blob", + # Map the metadata_storage_name field to the title field in the index to display the PDF title in the search results + field_mappings=[FieldMapping(source_field_name="metadata_storage_name", target_field_name="title")], + ) + + indexer_client = self.search_info.create_search_indexer_client() + indexer_result = await indexer_client.create_or_update_indexer(indexer) + + # Run the indexer + await indexer_client.run_indexer(indexer_name) + await indexer_client.close() + + logger.info( + f"Successfully created index, indexer: {indexer_result.name}, and skillset. Please navigate to search service in Azure Portal to view the status of the indexer." + ) diff --git a/app/backend/prepdocslib/jsonparser.py b/app/backend/prepdocslib/jsonparser.py index 48c3eac046..a7a596d8ce 100644 --- a/app/backend/prepdocslib/jsonparser.py +++ b/app/backend/prepdocslib/jsonparser.py @@ -1,23 +1,23 @@ -import json -from typing import IO, AsyncGenerator - -from .page import Page -from .parser import Parser - - -class JsonParser(Parser): - """ - Concrete parser that can parse JSON into Page objects. A top-level object becomes a single Page, while a top-level array becomes multiple Page objects. - """ - - async def parse(self, content: IO) -> AsyncGenerator[Page, None]: - offset = 0 - data = json.loads(content.read()) - if isinstance(data, list): - for i, obj in enumerate(data): - offset += 1 # For opening bracket or comma before object - page_text = json.dumps(obj) - yield Page(i, offset, page_text) - offset += len(page_text) - elif isinstance(data, dict): - yield Page(0, 0, json.dumps(data)) +import json +from typing import IO, AsyncGenerator + +from .page import Page +from .parser import Parser + + +class JsonParser(Parser): + """ + Concrete parser that can parse JSON into Page objects. A top-level object becomes a single Page, while a top-level array becomes multiple Page objects. + """ + + async def parse(self, content: IO) -> AsyncGenerator[Page, None]: + offset = 0 + data = json.loads(content.read()) + if isinstance(data, list): + for i, obj in enumerate(data): + offset += 1 # For opening bracket or comma before object + page_text = json.dumps(obj) + yield Page(i, offset, page_text) + offset += len(page_text) + elif isinstance(data, dict): + yield Page(0, 0, json.dumps(data)) diff --git a/app/backend/prepdocslib/listfilestrategy.py b/app/backend/prepdocslib/listfilestrategy.py index bd6a48d651..6f6bb2fb70 100644 --- a/app/backend/prepdocslib/listfilestrategy.py +++ b/app/backend/prepdocslib/listfilestrategy.py @@ -1,177 +1,176 @@ -import base64 -import hashlib -import logging -import os -import re -import tempfile -from abc import ABC -from glob import glob -from typing import IO, AsyncGenerator, Dict, List, Optional, Union - -from azure.core.credentials_async import AsyncTokenCredential -from azure.storage.filedatalake.aio import ( - DataLakeServiceClient, -) - -logger = logging.getLogger("ingester") - - -class File: - """ - Represents a file stored either locally or in a data lake storage account - This file might contain access control information about which users or groups can access it - """ - - def __init__(self, content: IO, acls: Optional[dict[str, list]] = None, url: Optional[str] = None): - self.content = content - self.acls = acls or {} - self.url = url - - def filename(self): - return os.path.basename(self.content.name) - - def file_extension(self): - return os.path.splitext(self.content.name)[1] - - def filename_to_id(self): - filename_ascii = re.sub("[^0-9a-zA-Z_-]", "_", self.filename()) - filename_hash = base64.b16encode(self.filename().encode("utf-8")).decode("ascii") - acls_hash = "" - if self.acls: - acls_hash = base64.b16encode(str(self.acls).encode("utf-8")).decode("ascii") - return f"file-{filename_ascii}-{filename_hash}{acls_hash}" - - def close(self): - if self.content: - self.content.close() - - -class ListFileStrategy(ABC): - """ - Abstract strategy for listing files that are located somewhere. For example, on a local computer or remotely in a storage account - """ - - async def list(self) -> AsyncGenerator[File, None]: - if False: # pragma: no cover - this is necessary for mypy to type check - yield - - async def list_paths(self) -> AsyncGenerator[str, None]: - if False: # pragma: no cover - this is necessary for mypy to type check - yield - - -class LocalListFileStrategy(ListFileStrategy): - """ - Concrete strategy for listing files that are located in a local filesystem - """ - - def __init__(self, path_pattern: str): - self.path_pattern = path_pattern - - async def list_paths(self) -> AsyncGenerator[str, None]: - async for p in self._list_paths(self.path_pattern): - yield p - - async def _list_paths(self, path_pattern: str) -> AsyncGenerator[str, None]: - for path in glob(path_pattern): - if os.path.isdir(path): - async for p in self._list_paths(f"{path}/*"): - yield p - else: - # Only list files, not directories - yield path - - async def list(self) -> AsyncGenerator[File, None]: - async for path in self.list_paths(): - if not self.check_md5(path): - yield File(content=open(path, mode="rb")) - - def check_md5(self, path: str) -> bool: - # if filename ends in .md5 skip - if path.endswith(".md5"): - return True - - # if there is a file called .md5 in this directory, see if its updated - stored_hash = None - with open(path, "rb") as file: - existing_hash = hashlib.md5(file.read()).hexdigest() - hash_path = f"{path}.md5" - if os.path.exists(hash_path): - with open(hash_path, encoding="utf-8") as md5_f: - stored_hash = md5_f.read() - - if stored_hash and stored_hash.strip() == existing_hash.strip(): - logger.info("Skipping %s, no changes detected.", path) - return True - - # Write the hash - with open(hash_path, "w", encoding="utf-8") as md5_f: - md5_f.write(existing_hash) - - return False - - -class ADLSGen2ListFileStrategy(ListFileStrategy): - """ - Concrete strategy for listing files that are located in a data lake storage account - """ - - def __init__( - self, - data_lake_storage_account: str, - data_lake_filesystem: str, - data_lake_path: str, - credential: Union[AsyncTokenCredential, str], - ): - self.data_lake_storage_account = data_lake_storage_account - self.data_lake_filesystem = data_lake_filesystem - self.data_lake_path = data_lake_path - self.credential = credential - - async def list_paths(self) -> AsyncGenerator[str, None]: - async with DataLakeServiceClient( - account_url=f"https://{self.data_lake_storage_account}.dfs.core.windows.net", credential=self.credential - ) as service_client, service_client.get_file_system_client(self.data_lake_filesystem) as filesystem_client: - async for path in filesystem_client.get_paths(path=self.data_lake_path, recursive=True): - if path.is_directory: - continue - - yield path.name - - async def list(self) -> AsyncGenerator[File, None]: - async with DataLakeServiceClient( - account_url=f"https://{self.data_lake_storage_account}.dfs.core.windows.net", credential=self.credential - ) as service_client, service_client.get_file_system_client(self.data_lake_filesystem) as filesystem_client: - async for path in self.list_paths(): - temp_file_path = os.path.join(tempfile.gettempdir(), os.path.basename(path)) - try: - async with filesystem_client.get_file_client(path) as file_client: - with open(temp_file_path, "wb") as temp_file: - downloader = await file_client.download_file() - await downloader.readinto(temp_file) - # Parse out user ids and group ids - acls: Dict[str, List[str]] = {"oids": [], "groups": []} - # https://learn.microsoft.com/python/api/azure-storage-file-datalake/azure.storage.filedatalake.datalakefileclient?view=azure-python#azure-storage-filedatalake-datalakefileclient-get-access-control - # Request ACLs as GUIDs - access_control = await file_client.get_access_control(upn=False) - acl_list = access_control["acl"] - # https://learn.microsoft.com/azure/storage/blobs/data-lake-storage-access-control - # ACL Format: user::rwx,group::r-x,other::r--,user:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:r-- - acl_list = acl_list.split(",") - for acl in acl_list: - acl_parts: list = acl.split(":") - if len(acl_parts) != 3: - continue - if len(acl_parts[1]) == 0: - continue - if acl_parts[0] == "user" and "r" in acl_parts[2]: - acls["oids"].append(acl_parts[1]) - if acl_parts[0] == "group" and "r" in acl_parts[2]: - acls["groups"].append(acl_parts[1]) - yield File(content=open(temp_file_path, "rb"), acls=acls, url=file_client.url) - except Exception as data_lake_exception: - logger.error(f"\tGot an error while reading {path} -> {data_lake_exception} --> skipping file") - try: - os.remove(temp_file_path) - except Exception as file_delete_exception: - logger.error(f"\tGot an error while deleting {temp_file_path} -> {file_delete_exception}") +import base64 +import hashlib +import logging +import os +import re +import tempfile +from abc import ABC +from glob import glob +from typing import IO, AsyncGenerator, Dict, List, Optional, Union + +from azure.core.credentials_async import AsyncTokenCredential +from azure.storage.filedatalake.aio import ( + DataLakeServiceClient, +) + +logger = logging.getLogger("ingester") + + +class File: + """ + Represents a file stored either locally or in a data lake storage account + This file might contain access control information about which users or groups can access it + """ + + def __init__(self, content: IO, acls: Optional[dict[str, list]] = None, url: Optional[str] = None): + self.content = content + self.acls = acls or {} + self.url = url + + def filename(self): + return os.path.basename(self.content.name) + + def file_extension(self): + return os.path.splitext(self.content.name)[1] + + def filename_to_id(self): + filename_ascii = re.sub("[^0-9a-zA-Z_-]", "_", self.filename()) + filename_hash = base64.b16encode(self.filename().encode("utf-8")).decode("ascii") + acls_hash = "" + if self.acls: + acls_hash = base64.b16encode(str(self.acls).encode("utf-8")).decode("ascii") + return f"file-{filename_ascii}-{filename_hash}{acls_hash}" + + def close(self): + if self.content: + self.content.close() + + +class ListFileStrategy(ABC): + """ + Abstract strategy for listing files that are located somewhere. For example, on a local computer or remotely in a storage account + """ + + async def list(self) -> AsyncGenerator[File, None]: + if False: # pragma: no cover - this is necessary for mypy to type check + yield + + async def list_paths(self) -> AsyncGenerator[str, None]: + if False: # pragma: no cover - this is necessary for mypy to type check + yield + + +class LocalListFileStrategy(ListFileStrategy): + """ + Concrete strategy for listing files that are located in a local filesystem + """ + + def __init__(self, path_pattern: str): + self.path_pattern = path_pattern + + async def list_paths(self) -> AsyncGenerator[str, None]: + async for p in self._list_paths(self.path_pattern): + yield p + + async def _list_paths(self, path_pattern: str) -> AsyncGenerator[str, None]: + for path in glob(path_pattern): + if os.path.isdir(path): + async for p in self._list_paths(f"{path}/*"): + yield p + else: + # Only list files, not directories + yield path + + async def list(self) -> AsyncGenerator[File, None]: + async for path in self.list_paths(): + yield File(content=open(path, mode="rb")) + + def check_md5(self, path: str) -> bool: + # if filename ends in .md5 skip + if path.endswith(".md5"): + return True + + # if there is a file called .md5 in this directory, see if its updated + stored_hash = None + with open(path, "rb") as file: + existing_hash = hashlib.md5(file.read()).hexdigest() + hash_path = f"{path}.md5" + if os.path.exists(hash_path): + with open(hash_path, encoding="utf-8") as md5_f: + stored_hash = md5_f.read() + + if stored_hash and stored_hash.strip() == existing_hash.strip(): + logger.info("Skipping %s, no changes detected.", path) + return True + + # Write the hash + with open(hash_path, "w", encoding="utf-8") as md5_f: + md5_f.write(existing_hash) + + return False + + +class ADLSGen2ListFileStrategy(ListFileStrategy): + """ + Concrete strategy for listing files that are located in a data lake storage account + """ + + def __init__( + self, + data_lake_storage_account: str, + data_lake_filesystem: str, + data_lake_path: str, + credential: Union[AsyncTokenCredential, str], + ): + self.data_lake_storage_account = data_lake_storage_account + self.data_lake_filesystem = data_lake_filesystem + self.data_lake_path = data_lake_path + self.credential = credential + + async def list_paths(self) -> AsyncGenerator[str, None]: + async with DataLakeServiceClient( + account_url=f"https://{self.data_lake_storage_account}.dfs.core.windows.net", credential=self.credential + ) as service_client, service_client.get_file_system_client(self.data_lake_filesystem) as filesystem_client: + async for path in filesystem_client.get_paths(path=self.data_lake_path, recursive=True): + if path.is_directory: + continue + + yield path.name + + async def list(self) -> AsyncGenerator[File, None]: + async with DataLakeServiceClient( + account_url=f"https://{self.data_lake_storage_account}.dfs.core.windows.net", credential=self.credential + ) as service_client, service_client.get_file_system_client(self.data_lake_filesystem) as filesystem_client: + async for path in self.list_paths(): + temp_file_path = os.path.join(tempfile.gettempdir(), os.path.basename(path)) + try: + async with filesystem_client.get_file_client(path) as file_client: + with open(temp_file_path, "wb") as temp_file: + downloader = await file_client.download_file() + await downloader.readinto(temp_file) + # Parse out user ids and group ids + acls: Dict[str, List[str]] = {"oids": [], "groups": []} + # https://learn.microsoft.com/python/api/azure-storage-file-datalake/azure.storage.filedatalake.datalakefileclient?view=azure-python#azure-storage-filedatalake-datalakefileclient-get-access-control + # Request ACLs as GUIDs + access_control = await file_client.get_access_control(upn=False) + acl_list = access_control["acl"] + # https://learn.microsoft.com/azure/storage/blobs/data-lake-storage-access-control + # ACL Format: user::rwx,group::r-x,other::r--,user:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:r-- + acl_list = acl_list.split(",") + for acl in acl_list: + acl_parts: list = acl.split(":") + if len(acl_parts) != 3: + continue + if len(acl_parts[1]) == 0: + continue + if acl_parts[0] == "user" and "r" in acl_parts[2]: + acls["oids"].append(acl_parts[1]) + if acl_parts[0] == "group" and "r" in acl_parts[2]: + acls["groups"].append(acl_parts[1]) + yield File(content=open(temp_file_path, "rb"), acls=acls, url=file_client.url) + except Exception as data_lake_exception: + logger.error(f"\tGot an error while reading {path} -> {data_lake_exception} --> skipping file") + try: + os.remove(temp_file_path) + except Exception as file_delete_exception: + logger.error(f"\tGot an error while deleting {temp_file_path} -> {file_delete_exception}") diff --git a/app/backend/prepdocslib/page.py b/app/backend/prepdocslib/page.py index f12fe70b94..956c0e22dc 100644 --- a/app/backend/prepdocslib/page.py +++ b/app/backend/prepdocslib/page.py @@ -1,24 +1,24 @@ -class Page: - """ - A single page from a document - - Attributes: - page_num (int): Page number - offset (int): If the text of the entire Document was concatenated into a single string, the index of the first character on the page. For example, if page 1 had the text "hello" and page 2 had the text "world", the offset of page 2 is 5 ("hellow") - text (str): The text of the page - """ - - def __init__(self, page_num: int, offset: int, text: str): - self.page_num = page_num - self.offset = offset - self.text = text - - -class SplitPage: - """ - A section of a page that has been split into a smaller chunk. - """ - - def __init__(self, page_num: int, text: str): - self.page_num = page_num - self.text = text +class Page: + """ + A single page from a document + + Attributes: + page_num (int): Page number + offset (int): If the text of the entire Document was concatenated into a single string, the index of the first character on the page. For example, if page 1 had the text "hello" and page 2 had the text "world", the offset of page 2 is 5 ("hellow") + text (str): The text of the page + """ + + def __init__(self, page_num: int, offset: int, text: str): + self.page_num = page_num + self.offset = offset + self.text = text + + +class SplitPage: + """ + A section of a page that has been split into a smaller chunk. + """ + + def __init__(self, page_num: int, text: str): + self.page_num = page_num + self.text = text diff --git a/app/backend/prepdocslib/parser.py b/app/backend/prepdocslib/parser.py index 09d12e0ad6..67c962decd 100644 --- a/app/backend/prepdocslib/parser.py +++ b/app/backend/prepdocslib/parser.py @@ -1,14 +1,14 @@ -from abc import ABC -from typing import IO, AsyncGenerator - -from .page import Page - - -class Parser(ABC): - """ - Abstract parser that parses content into Page objects - """ - - async def parse(self, content: IO) -> AsyncGenerator[Page, None]: - if False: - yield # pragma: no cover - this is necessary for mypy to type check +from abc import ABC +from typing import IO, AsyncGenerator + +from .page import Page + + +class Parser(ABC): + """ + Abstract parser that parses content into Page objects + """ + + async def parse(self, content: IO) -> AsyncGenerator[Page, None]: + if False: + yield # pragma: no cover - this is necessary for mypy to type check diff --git a/app/backend/prepdocslib/pdfparser.py b/app/backend/prepdocslib/pdfparser.py index 33335aadd6..71c7f3bb4f 100644 --- a/app/backend/prepdocslib/pdfparser.py +++ b/app/backend/prepdocslib/pdfparser.py @@ -1,111 +1,111 @@ -import html -import logging -from typing import IO, AsyncGenerator, Union - -from azure.ai.documentintelligence.aio import DocumentIntelligenceClient -from azure.ai.documentintelligence.models import DocumentTable -from azure.core.credentials import AzureKeyCredential -from azure.core.credentials_async import AsyncTokenCredential -from pypdf import PdfReader - -from .page import Page -from .parser import Parser - -logger = logging.getLogger("ingester") - - -class LocalPdfParser(Parser): - """ - Concrete parser backed by PyPDF that can parse PDFs into pages - To learn more, please visit https://pypi.org/project/pypdf/ - """ - - async def parse(self, content: IO) -> AsyncGenerator[Page, None]: - logger.info("Extracting text from '%s' using local PDF parser (pypdf)", content.name) - - reader = PdfReader(content) - pages = reader.pages - offset = 0 - for page_num, p in enumerate(pages): - page_text = p.extract_text() - yield Page(page_num=page_num, offset=offset, text=page_text) - offset += len(page_text) - - -class DocumentAnalysisParser(Parser): - """ - Concrete parser backed by Azure AI Document Intelligence that can parse many document formats into pages - To learn more, please visit https://learn.microsoft.com/azure/ai-services/document-intelligence/overview - """ - - def __init__( - self, endpoint: str, credential: Union[AsyncTokenCredential, AzureKeyCredential], model_id="prebuilt-layout" - ): - self.model_id = model_id - self.endpoint = endpoint - self.credential = credential - - async def parse(self, content: IO) -> AsyncGenerator[Page, None]: - logger.info("Extracting text from '%s' using Azure Document Intelligence", content.name) - - async with DocumentIntelligenceClient( - endpoint=self.endpoint, credential=self.credential - ) as document_intelligence_client: - poller = await document_intelligence_client.begin_analyze_document( - model_id=self.model_id, analyze_request=content, content_type="application/octet-stream" - ) - form_recognizer_results = await poller.result() - - offset = 0 - for page_num, page in enumerate(form_recognizer_results.pages): - tables_on_page = [ - table - for table in (form_recognizer_results.tables or []) - if table.bounding_regions and table.bounding_regions[0].page_number == page_num + 1 - ] - - # mark all positions of the table spans in the page - page_offset = page.spans[0].offset - page_length = page.spans[0].length - table_chars = [-1] * page_length - for table_id, table in enumerate(tables_on_page): - for span in table.spans: - # replace all table spans with "table_id" in table_chars array - for i in range(span.length): - idx = span.offset - page_offset + i - if idx >= 0 and idx < page_length: - table_chars[idx] = table_id - - # build page text by replacing characters in table spans with table html - page_text = "" - added_tables = set() - for idx, table_id in enumerate(table_chars): - if table_id == -1: - page_text += form_recognizer_results.content[page_offset + idx] - elif table_id not in added_tables: - page_text += DocumentAnalysisParser.table_to_html(tables_on_page[table_id]) - added_tables.add(table_id) - - yield Page(page_num=page_num, offset=offset, text=page_text) - offset += len(page_text) - - @classmethod - def table_to_html(cls, table: DocumentTable): - table_html = "" - rows = [ - sorted([cell for cell in table.cells if cell.row_index == i], key=lambda cell: cell.column_index) - for i in range(table.row_count) - ] - for row_cells in rows: - table_html += "" - for cell in row_cells: - tag = "th" if (cell.kind == "columnHeader" or cell.kind == "rowHeader") else "td" - cell_spans = "" - if cell.column_span is not None and cell.column_span > 1: - cell_spans += f" colSpan={cell.column_span}" - if cell.row_span is not None and cell.row_span > 1: - cell_spans += f" rowSpan={cell.row_span}" - table_html += f"<{tag}{cell_spans}>{html.escape(cell.content)}" - table_html += "" - table_html += "
" - return table_html +import html +import logging +from typing import IO, AsyncGenerator, Union + +from azure.ai.documentintelligence.aio import DocumentIntelligenceClient +from azure.ai.documentintelligence.models import DocumentTable +from azure.core.credentials import AzureKeyCredential +from azure.core.credentials_async import AsyncTokenCredential +from pypdf import PdfReader + +from .page import Page +from .parser import Parser + +logger = logging.getLogger("ingester") + + +class LocalPdfParser(Parser): + """ + Concrete parser backed by PyPDF that can parse PDFs into pages + To learn more, please visit https://pypi.org/project/pypdf/ + """ + + async def parse(self, content: IO) -> AsyncGenerator[Page, None]: + logger.info("Extracting text from '%s' using local PDF parser (pypdf)", content.name) + + reader = PdfReader(content) + pages = reader.pages + offset = 0 + for page_num, p in enumerate(pages): + page_text = p.extract_text() + yield Page(page_num=page_num, offset=offset, text=page_text) + offset += len(page_text) + + +class DocumentAnalysisParser(Parser): + """ + Concrete parser backed by Azure AI Document Intelligence that can parse many document formats into pages + To learn more, please visit https://learn.microsoft.com/azure/ai-services/document-intelligence/overview + """ + + def __init__( + self, endpoint: str, credential: Union[AsyncTokenCredential, AzureKeyCredential], model_id="prebuilt-layout" + ): + self.model_id = model_id + self.endpoint = endpoint + self.credential = credential + + async def parse(self, content: IO) -> AsyncGenerator[Page, None]: + logger.info("Extracting text from '%s' using Azure Document Intelligence", content.name) + + async with DocumentIntelligenceClient( + endpoint=self.endpoint, credential=self.credential + ) as document_intelligence_client: + poller = await document_intelligence_client.begin_analyze_document( + model_id=self.model_id, analyze_request=content, content_type="application/octet-stream" + ) + form_recognizer_results = await poller.result() + + offset = 0 + for page_num, page in enumerate(form_recognizer_results.pages): + tables_on_page = [ + table + for table in (form_recognizer_results.tables or []) + if table.bounding_regions and table.bounding_regions[0].page_number == page_num + 1 + ] + + # mark all positions of the table spans in the page + page_offset = page.spans[0].offset + page_length = page.spans[0].length + table_chars = [-1] * page_length + for table_id, table in enumerate(tables_on_page): + for span in table.spans: + # replace all table spans with "table_id" in table_chars array + for i in range(span.length): + idx = span.offset - page_offset + i + if idx >= 0 and idx < page_length: + table_chars[idx] = table_id + + # build page text by replacing characters in table spans with table html + page_text = "" + added_tables = set() + for idx, table_id in enumerate(table_chars): + if table_id == -1: + page_text += form_recognizer_results.content[page_offset + idx] + elif table_id not in added_tables: + page_text += DocumentAnalysisParser.table_to_html(tables_on_page[table_id]) + added_tables.add(table_id) + + yield Page(page_num=page_num, offset=offset, text=page_text) + offset += len(page_text) + + @classmethod + def table_to_html(cls, table: DocumentTable): + table_html = "" + rows = [ + sorted([cell for cell in table.cells if cell.row_index == i], key=lambda cell: cell.column_index) + for i in range(table.row_count) + ] + for row_cells in rows: + table_html += "" + for cell in row_cells: + tag = "th" if (cell.kind == "columnHeader" or cell.kind == "rowHeader") else "td" + cell_spans = "" + if cell.column_span is not None and cell.column_span > 1: + cell_spans += f" colSpan={cell.column_span}" + if cell.row_span is not None and cell.row_span > 1: + cell_spans += f" rowSpan={cell.row_span}" + table_html += f"<{tag}{cell_spans}>{html.escape(cell.content)}" + table_html += "" + table_html += "
" + return table_html diff --git a/app/backend/prepdocslib/searchmanager.py b/app/backend/prepdocslib/searchmanager.py index 496e5ca30a..109c48630d 100644 --- a/app/backend/prepdocslib/searchmanager.py +++ b/app/backend/prepdocslib/searchmanager.py @@ -1,279 +1,279 @@ -import asyncio -import logging -import os -from typing import List, Optional - -from azure.search.documents.indexes.models import ( - HnswAlgorithmConfiguration, - HnswParameters, - SearchableField, - SearchField, - SearchFieldDataType, - SearchIndex, - SemanticConfiguration, - SemanticField, - SemanticPrioritizedFields, - SemanticSearch, - SimpleField, - VectorSearch, - VectorSearchProfile, - VectorSearchVectorizer, -) - -from .blobmanager import BlobManager -from .embeddings import OpenAIEmbeddings -from .listfilestrategy import File -from .strategy import SearchInfo -from .textsplitter import SplitPage - -logger = logging.getLogger("ingester") - - -class Section: - """ - A section of a page that is stored in a search service. These sections are used as context by Azure OpenAI service - """ - - def __init__(self, split_page: SplitPage, content: File, category: Optional[str] = None): - self.split_page = split_page - self.content = content - self.category = category - - -class SearchManager: - """ - Class to manage a search service. It can create indexes, and update or remove sections stored in these indexes - To learn more, please visit https://learn.microsoft.com/azure/search/search-what-is-azure-search - """ - - def __init__( - self, - search_info: SearchInfo, - search_analyzer_name: Optional[str] = None, - use_acls: bool = False, - use_int_vectorization: bool = False, - embeddings: Optional[OpenAIEmbeddings] = None, - search_images: bool = False, - ): - self.search_info = search_info - self.search_analyzer_name = search_analyzer_name - self.use_acls = use_acls - self.use_int_vectorization = use_int_vectorization - self.embeddings = embeddings - # Integrated vectorization uses the ada-002 model with 1536 dimensions - self.embedding_dimensions = self.embeddings.open_ai_dimensions if self.embeddings else 1536 - self.search_images = search_images - - async def create_index(self, vectorizers: Optional[List[VectorSearchVectorizer]] = None): - logger.info("Ensuring search index %s exists", self.search_info.index_name) - - async with self.search_info.create_search_index_client() as search_index_client: - fields = [ - ( - SimpleField(name="id", type="Edm.String", key=True) - if not self.use_int_vectorization - else SearchField( - name="id", - type="Edm.String", - key=True, - sortable=True, - filterable=True, - facetable=True, - analyzer_name="keyword", - ) - ), - SearchableField( - name="content", - type="Edm.String", - analyzer_name=self.search_analyzer_name, - ), - SearchField( - name="embedding", - type=SearchFieldDataType.Collection(SearchFieldDataType.Single), - hidden=False, - searchable=True, - filterable=False, - sortable=False, - facetable=False, - vector_search_dimensions=self.embedding_dimensions, - vector_search_profile_name="embedding_config", - ), - SimpleField(name="category", type="Edm.String", filterable=True, facetable=True), - SimpleField( - name="sourcepage", - type="Edm.String", - filterable=True, - facetable=True, - ), - SimpleField( - name="sourcefile", - type="Edm.String", - filterable=True, - facetable=True, - ), - SimpleField( - name="storageUrl", - type="Edm.String", - filterable=True, - facetable=False, - ), - ] - if self.use_acls: - fields.append( - SimpleField( - name="oids", - type=SearchFieldDataType.Collection(SearchFieldDataType.String), - filterable=True, - ) - ) - fields.append( - SimpleField( - name="groups", - type=SearchFieldDataType.Collection(SearchFieldDataType.String), - filterable=True, - ) - ) - if self.use_int_vectorization: - fields.append(SearchableField(name="parent_id", type="Edm.String", filterable=True)) - if self.search_images: - fields.append( - SearchField( - name="imageEmbedding", - type=SearchFieldDataType.Collection(SearchFieldDataType.Single), - hidden=False, - searchable=True, - filterable=False, - sortable=False, - facetable=False, - vector_search_dimensions=1024, - vector_search_profile_name="embedding_config", - ), - ) - - index = SearchIndex( - name=self.search_info.index_name, - fields=fields, - semantic_search=SemanticSearch( - configurations=[ - SemanticConfiguration( - name="default", - prioritized_fields=SemanticPrioritizedFields( - title_field=None, content_fields=[SemanticField(field_name="content")] - ), - ) - ] - ), - vector_search=VectorSearch( - algorithms=[ - HnswAlgorithmConfiguration( - name="hnsw_config", - parameters=HnswParameters(metric="cosine"), - ) - ], - profiles=[ - VectorSearchProfile( - name="embedding_config", - algorithm_configuration_name="hnsw_config", - vectorizer=( - f"{self.search_info.index_name}-vectorizer" if self.use_int_vectorization else None - ), - ), - ], - vectorizers=vectorizers, - ), - ) - if self.search_info.index_name not in [name async for name in search_index_client.list_index_names()]: - logger.info("Creating %s search index", self.search_info.index_name) - await search_index_client.create_index(index) - else: - logger.info("Search index %s already exists", self.search_info.index_name) - index_definition = await search_index_client.get_index(self.search_info.index_name) - if not any(field.name == "storageUrl" for field in index_definition.fields): - logger.info("Adding storageUrl field to index %s", self.search_info.index_name) - index_definition.fields.append( - SimpleField( - name="storageUrl", - type="Edm.String", - filterable=True, - facetable=False, - ), - ) - await search_index_client.create_or_update_index(index_definition) - - async def update_content( - self, sections: List[Section], image_embeddings: Optional[List[List[float]]] = None, url: Optional[str] = None - ): - MAX_BATCH_SIZE = 1000 - section_batches = [sections[i : i + MAX_BATCH_SIZE] for i in range(0, len(sections), MAX_BATCH_SIZE)] - - async with self.search_info.create_search_client() as search_client: - for batch_index, batch in enumerate(section_batches): - documents = [ - { - "id": f"{section.content.filename_to_id()}-page-{section_index + batch_index * MAX_BATCH_SIZE}", - "content": section.split_page.text, - "category": section.category, - "sourcepage": ( - BlobManager.blob_image_name_from_file_page( - filename=section.content.filename(), - page=section.split_page.page_num, - ) - if image_embeddings - else BlobManager.sourcepage_from_file_page( - filename=section.content.filename(), - page=section.split_page.page_num, - ) - ), - "sourcefile": section.content.filename(), - **section.content.acls, - } - for section_index, section in enumerate(batch) - ] - if url: - for document in documents: - document["storageUrl"] = url - if self.embeddings: - embeddings = await self.embeddings.create_embeddings( - texts=[section.split_page.text for section in batch] - ) - for i, document in enumerate(documents): - document["embedding"] = embeddings[i] - if image_embeddings: - for i, (document, section) in enumerate(zip(documents, batch)): - document["imageEmbedding"] = image_embeddings[section.split_page.page_num] - - await search_client.upload_documents(documents) - - async def remove_content(self, path: Optional[str] = None, only_oid: Optional[str] = None): - logger.info( - "Removing sections from '{%s or ''}' from search index '%s'", path, self.search_info.index_name - ) - async with self.search_info.create_search_client() as search_client: - while True: - filter = None - if path is not None: - # Replace ' with '' to escape the single quote for the filter - # https://learn.microsoft.com/azure/search/query-odata-filter-orderby-syntax#escaping-special-characters-in-string-constants - path_for_filter = os.path.basename(path).replace("'", "''") - filter = f"sourcefile eq '{path_for_filter}'" - max_results = 1000 - result = await search_client.search( - search_text="", filter=filter, top=max_results, include_total_count=True - ) - result_count = await result.get_count() - if result_count == 0: - break - documents_to_remove = [] - async for document in result: - # If only_oid is set, only remove documents that have only this oid - if not only_oid or document.get("oids") == [only_oid]: - documents_to_remove.append({"id": document["id"]}) - if len(documents_to_remove) == 0: - if result_count < max_results: - break - else: - continue - removed_docs = await search_client.delete_documents(documents_to_remove) - logger.info("Removed %d sections from index", len(removed_docs)) - # It can take a few seconds for search results to reflect changes, so wait a bit - await asyncio.sleep(2) +import asyncio +import logging +import os +from typing import List, Optional + +from azure.search.documents.indexes.models import ( + HnswAlgorithmConfiguration, + HnswParameters, + SearchableField, + SearchField, + SearchFieldDataType, + SearchIndex, + SemanticConfiguration, + SemanticField, + SemanticPrioritizedFields, + SemanticSearch, + SimpleField, + VectorSearch, + VectorSearchProfile, + VectorSearchVectorizer, +) + +from .blobmanager import BlobManager +from .embeddings import OpenAIEmbeddings +from .listfilestrategy import File +from .strategy import SearchInfo +from .textsplitter import SplitPage + +logger = logging.getLogger("ingester") + + +class Section: + """ + A section of a page that is stored in a search service. These sections are used as context by Azure OpenAI service + """ + + def __init__(self, split_page: SplitPage, content: File, category: Optional[str] = None): + self.split_page = split_page + self.content = content + self.category = category + + +class SearchManager: + """ + Class to manage a search service. It can create indexes, and update or remove sections stored in these indexes + To learn more, please visit https://learn.microsoft.com/azure/search/search-what-is-azure-search + """ + + def __init__( + self, + search_info: SearchInfo, + search_analyzer_name: Optional[str] = None, + use_acls: bool = False, + use_int_vectorization: bool = False, + embeddings: Optional[OpenAIEmbeddings] = None, + search_images: bool = False, + ): + self.search_info = search_info + self.search_analyzer_name = search_analyzer_name + self.use_acls = use_acls + self.use_int_vectorization = use_int_vectorization + self.embeddings = embeddings + # Integrated vectorization uses the ada-002 model with 1536 dimensions + self.embedding_dimensions = self.embeddings.open_ai_dimensions if self.embeddings else 1536 + self.search_images = search_images + + async def create_index(self, vectorizers: Optional[List[VectorSearchVectorizer]] = None): + logger.info("Ensuring search index %s exists", self.search_info.index_name) + + async with self.search_info.create_search_index_client() as search_index_client: + fields = [ + ( + SimpleField(name="id", type="Edm.String", key=True) + if not self.use_int_vectorization + else SearchField( + name="id", + type="Edm.String", + key=True, + sortable=True, + filterable=True, + facetable=True, + analyzer_name="keyword", + ) + ), + SearchableField( + name="content", + type="Edm.String", + analyzer_name=self.search_analyzer_name, + ), + SearchField( + name="embedding", + type=SearchFieldDataType.Collection(SearchFieldDataType.Single), + hidden=False, + searchable=True, + filterable=False, + sortable=False, + facetable=False, + vector_search_dimensions=self.embedding_dimensions, + vector_search_profile_name="embedding_config", + ), + SimpleField(name="category", type="Edm.String", filterable=True, facetable=True), + SimpleField( + name="sourcepage", + type="Edm.String", + filterable=True, + facetable=True, + ), + SimpleField( + name="sourcefile", + type="Edm.String", + filterable=True, + facetable=True, + ), + SimpleField( + name="storageUrl", + type="Edm.String", + filterable=True, + facetable=False, + ), + ] + if self.use_acls: + fields.append( + SimpleField( + name="oids", + type=SearchFieldDataType.Collection(SearchFieldDataType.String), + filterable=True, + ) + ) + fields.append( + SimpleField( + name="groups", + type=SearchFieldDataType.Collection(SearchFieldDataType.String), + filterable=True, + ) + ) + if self.use_int_vectorization: + fields.append(SearchableField(name="parent_id", type="Edm.String", filterable=True)) + if self.search_images: + fields.append( + SearchField( + name="imageEmbedding", + type=SearchFieldDataType.Collection(SearchFieldDataType.Single), + hidden=False, + searchable=True, + filterable=False, + sortable=False, + facetable=False, + vector_search_dimensions=1024, + vector_search_profile_name="embedding_config", + ), + ) + + index = SearchIndex( + name=self.search_info.index_name, + fields=fields, + semantic_search=SemanticSearch( + configurations=[ + SemanticConfiguration( + name="default", + prioritized_fields=SemanticPrioritizedFields( + title_field=None, content_fields=[SemanticField(field_name="content")] + ), + ) + ] + ), + vector_search=VectorSearch( + algorithms=[ + HnswAlgorithmConfiguration( + name="hnsw_config", + parameters=HnswParameters(metric="cosine"), + ) + ], + profiles=[ + VectorSearchProfile( + name="embedding_config", + algorithm_configuration_name="hnsw_config", + vectorizer=( + f"{self.search_info.index_name}-vectorizer" if self.use_int_vectorization else None + ), + ), + ], + vectorizers=vectorizers, + ), + ) + if self.search_info.index_name not in [name async for name in search_index_client.list_index_names()]: + logger.info("Creating %s search index", self.search_info.index_name) + await search_index_client.create_index(index) + else: + logger.info("Search index %s already exists", self.search_info.index_name) + index_definition = await search_index_client.get_index(self.search_info.index_name) + if not any(field.name == "storageUrl" for field in index_definition.fields): + logger.info("Adding storageUrl field to index %s", self.search_info.index_name) + index_definition.fields.append( + SimpleField( + name="storageUrl", + type="Edm.String", + filterable=True, + facetable=False, + ), + ) + await search_index_client.create_or_update_index(index_definition) + + async def update_content( + self, sections: List[Section], image_embeddings: Optional[List[List[float]]] = None, url: Optional[str] = None + ): + MAX_BATCH_SIZE = 1000 + section_batches = [sections[i : i + MAX_BATCH_SIZE] for i in range(0, len(sections), MAX_BATCH_SIZE)] + + async with self.search_info.create_search_client() as search_client: + for batch_index, batch in enumerate(section_batches): + documents = [ + { + "id": f"{section.content.filename_to_id()}-page-{section_index + batch_index * MAX_BATCH_SIZE}", + "content": section.split_page.text, + "category": section.category, + "sourcepage": ( + BlobManager.blob_image_name_from_file_page( + filename=section.content.filename(), + page=section.split_page.page_num, + ) + if image_embeddings + else BlobManager.sourcepage_from_file_page( + filename=section.content.filename(), + page=section.split_page.page_num, + ) + ), + "sourcefile": section.content.filename(), + **section.content.acls, + } + for section_index, section in enumerate(batch) + ] + if url: + for document in documents: + document["storageUrl"] = url + if self.embeddings: + embeddings = await self.embeddings.create_embeddings( + texts=[section.split_page.text for section in batch] + ) + for i, document in enumerate(documents): + document["embedding"] = embeddings[i] + if image_embeddings: + for i, (document, section) in enumerate(zip(documents, batch)): + document["imageEmbedding"] = image_embeddings[section.split_page.page_num] + + await search_client.upload_documents(documents) + + async def remove_content(self, path: Optional[str] = None, only_oid: Optional[str] = None): + logger.info( + "Removing sections from '{%s or ''}' from search index '%s'", path, self.search_info.index_name + ) + async with self.search_info.create_search_client() as search_client: + while True: + filter = None + if path is not None: + # Replace ' with '' to escape the single quote for the filter + # https://learn.microsoft.com/azure/search/query-odata-filter-orderby-syntax#escaping-special-characters-in-string-constants + path_for_filter = os.path.basename(path).replace("'", "''") + filter = f"sourcefile eq '{path_for_filter}'" + max_results = 1000 + result = await search_client.search( + search_text="", filter=filter, top=max_results, include_total_count=True + ) + result_count = await result.get_count() + if result_count == 0: + break + documents_to_remove = [] + async for document in result: + # If only_oid is set, only remove documents that have only this oid + if not only_oid or document.get("oids") == [only_oid]: + documents_to_remove.append({"id": document["id"]}) + if len(documents_to_remove) == 0: + if result_count < max_results: + break + else: + continue + removed_docs = await search_client.delete_documents(documents_to_remove) + logger.info("Removed %d sections from index", len(removed_docs)) + # It can take a few seconds for search results to reflect changes, so wait a bit + await asyncio.sleep(2) diff --git a/app/backend/prepdocslib/strategy.py b/app/backend/prepdocslib/strategy.py index e194dd64bd..66f416467e 100644 --- a/app/backend/prepdocslib/strategy.py +++ b/app/backend/prepdocslib/strategy.py @@ -1,49 +1,49 @@ -from abc import ABC -from enum import Enum -from typing import Union - -from azure.core.credentials import AzureKeyCredential -from azure.core.credentials_async import AsyncTokenCredential -from azure.search.documents.aio import SearchClient -from azure.search.documents.indexes.aio import SearchIndexClient, SearchIndexerClient - -USER_AGENT = "azure-search-chat-demo/1.0.0" - - -class SearchInfo: - """ - Class representing a connection to a search service - To learn more, please visit https://learn.microsoft.com/azure/search/search-what-is-azure-search - """ - - def __init__(self, endpoint: str, credential: Union[AsyncTokenCredential, AzureKeyCredential], index_name: str): - self.endpoint = endpoint - self.credential = credential - self.index_name = index_name - - def create_search_client(self) -> SearchClient: - return SearchClient(endpoint=self.endpoint, index_name=self.index_name, credential=self.credential) - - def create_search_index_client(self) -> SearchIndexClient: - return SearchIndexClient(endpoint=self.endpoint, credential=self.credential) - - def create_search_indexer_client(self) -> SearchIndexerClient: - return SearchIndexerClient(endpoint=self.endpoint, credential=self.credential) - - -class DocumentAction(Enum): - Add = 0 - Remove = 1 - RemoveAll = 2 - - -class Strategy(ABC): - """ - Abstract strategy for ingesting documents into a search service. It has a single setup step to perform any required initialization, and then a run step that actually ingests documents into the search service. - """ - - async def setup(self): - raise NotImplementedError - - async def run(self): - raise NotImplementedError +from abc import ABC +from enum import Enum +from typing import Union + +from azure.core.credentials import AzureKeyCredential +from azure.core.credentials_async import AsyncTokenCredential +from azure.search.documents.aio import SearchClient +from azure.search.documents.indexes.aio import SearchIndexClient, SearchIndexerClient + +USER_AGENT = "azure-search-chat-demo/1.0.0" + + +class SearchInfo: + """ + Class representing a connection to a search service + To learn more, please visit https://learn.microsoft.com/azure/search/search-what-is-azure-search + """ + + def __init__(self, endpoint: str, credential: Union[AsyncTokenCredential, AzureKeyCredential], index_name: str): + self.endpoint = endpoint + self.credential = credential + self.index_name = index_name + + def create_search_client(self) -> SearchClient: + return SearchClient(endpoint=self.endpoint, index_name=self.index_name, credential=self.credential) + + def create_search_index_client(self) -> SearchIndexClient: + return SearchIndexClient(endpoint=self.endpoint, credential=self.credential) + + def create_search_indexer_client(self) -> SearchIndexerClient: + return SearchIndexerClient(endpoint=self.endpoint, credential=self.credential) + + +class DocumentAction(Enum): + Add = 0 + Remove = 1 + RemoveAll = 2 + + +class Strategy(ABC): + """ + Abstract strategy for ingesting documents into a search service. It has a single setup step to perform any required initialization, and then a run step that actually ingests documents into the search service. + """ + + async def setup(self): + raise NotImplementedError + + async def run(self): + raise NotImplementedError diff --git a/app/backend/prepdocslib/textparser.py b/app/backend/prepdocslib/textparser.py index f61201eef2..04ccff5ffa 100644 --- a/app/backend/prepdocslib/textparser.py +++ b/app/backend/prepdocslib/textparser.py @@ -1,30 +1,30 @@ -import re -from typing import IO, AsyncGenerator - -from .page import Page -from .parser import Parser - - -def cleanup_data(data: str) -> str: - """Cleans up the given content using regexes - Args: - data: (str): The data to clean up. - Returns: - str: The cleaned up data. - """ - # match two or more newlines and replace them with one new line - output = re.sub(r"\n{2,}", "\n", data) - # match two or more spaces that are not newlines and replace them with one space - output = re.sub(r"[^\S\n]{2,}", " ", output) - - return output.strip() - - -class TextParser(Parser): - """Parses simple text into a Page object.""" - - async def parse(self, content: IO) -> AsyncGenerator[Page, None]: - data = content.read() - decoded_data = data.decode("utf-8") - text = cleanup_data(decoded_data) - yield Page(0, 0, text=text) +import re +from typing import IO, AsyncGenerator + +from .page import Page +from .parser import Parser + + +def cleanup_data(data: str) -> str: + """Cleans up the given content using regexes + Args: + data: (str): The data to clean up. + Returns: + str: The cleaned up data. + """ + # match two or more newlines and replace them with one new line + output = re.sub(r"\n{2,}", "\n", data) + # match two or more spaces that are not newlines and replace them with one space + output = re.sub(r"[^\S\n]{2,}", " ", output) + + return output.strip() + + +class TextParser(Parser): + """Parses simple text into a Page object.""" + + async def parse(self, content: IO) -> AsyncGenerator[Page, None]: + data = content.read() + decoded_data = data.decode("utf-8") + text = cleanup_data(decoded_data) + yield Page(0, 0, text=text) diff --git a/app/backend/prepdocslib/textsplitter.py b/app/backend/prepdocslib/textsplitter.py index 5d899e691e..e6f57d6055 100644 --- a/app/backend/prepdocslib/textsplitter.py +++ b/app/backend/prepdocslib/textsplitter.py @@ -1,233 +1,233 @@ -import logging -from abc import ABC -from typing import Generator, List - -import tiktoken - -from .page import Page, SplitPage - -logger = logging.getLogger("ingester") - - -class TextSplitter(ABC): - """ - Splits a list of pages into smaller chunks - :param pages: The pages to split - :return: A generator of SplitPage - """ - - def split_pages(self, pages: List[Page]) -> Generator[SplitPage, None, None]: - if False: - yield # pragma: no cover - this is necessary for mypy to type check - - -ENCODING_MODEL = "text-embedding-ada-002" - -STANDARD_WORD_BREAKS = [",", ";", ":", " ", "(", ")", "[", "]", "{", "}", "\t", "\n"] - -# See W3C document https://www.w3.org/TR/jlreq/#cl-01 -CJK_WORD_BREAKS = [ - "、", - ",", - ";", - ":", - "(", - ")", - "【", - "】", - "「", - "」", - "『", - "』", - "〔", - "〕", - "〈", - "〉", - "《", - "》", - "〖", - "〗", - "〘", - "〙", - "〚", - "〛", - "〝", - "〞", - "〟", - "〰", - "–", - "—", - "‘", - "’", - "‚", - "‛", - "“", - "”", - "„", - "‟", - "‹", - "›", -] - -STANDARD_SENTENCE_ENDINGS = [".", "!", "?"] - -# See CL05 and CL06, based on JIS X 4051:2004 -# https://www.w3.org/TR/jlreq/#cl-04 -CJK_SENTENCE_ENDINGS = ["。", "!", "?", "‼", "⁇", "⁈", "⁉"] - -# NB: text-embedding-3-XX is the same BPE as text-embedding-ada-002 -bpe = tiktoken.encoding_for_model(ENCODING_MODEL) - -DEFAULT_OVERLAP_PERCENT = 10 # See semantic search article for 10% overlap performance -DEFAULT_SECTION_LENGTH = 1000 # Roughly 400-500 tokens for English - - -class SentenceTextSplitter(TextSplitter): - """ - Class that splits pages into smaller chunks. This is required because embedding models may not be able to analyze an entire page at once - """ - - def __init__(self, has_image_embeddings: bool, max_tokens_per_section: int = 500): - self.sentence_endings = STANDARD_SENTENCE_ENDINGS + CJK_SENTENCE_ENDINGS - self.word_breaks = STANDARD_WORD_BREAKS + CJK_WORD_BREAKS - self.max_section_length = DEFAULT_SECTION_LENGTH - self.sentence_search_limit = 100 - self.max_tokens_per_section = max_tokens_per_section - self.section_overlap = int(self.max_section_length * DEFAULT_OVERLAP_PERCENT / 100) - self.has_image_embeddings = has_image_embeddings - - def split_page_by_max_tokens(self, page_num: int, text: str) -> Generator[SplitPage, None, None]: - """ - Recursively splits page by maximum number of tokens to better handle languages with higher token/word ratios. - """ - tokens = bpe.encode(text) - if len(tokens) <= self.max_tokens_per_section: - # Section is already within max tokens, return - yield SplitPage(page_num=page_num, text=text) - else: - # Start from the center and try and find the closest sentence ending by spiralling outward. - # IF we get to the outer thirds, then just split in half with a 5% overlap - start = int(len(text) // 2) - pos = 0 - boundary = int(len(text) // 3) - split_position = -1 - while start - pos > boundary: - if text[start - pos] in self.sentence_endings: - split_position = start - pos - break - elif text[start + pos] in self.sentence_endings: - split_position = start + pos - break - else: - pos += 1 - - if split_position > 0: - first_half = text[: split_position + 1] - second_half = text[split_position + 1 :] - else: - # Split page in half and call function again - # Overlap first and second halves by DEFAULT_OVERLAP_PERCENT% - middle = int(len(text) // 2) - overlap = int(len(text) * (DEFAULT_OVERLAP_PERCENT / 100)) - first_half = text[: middle + overlap] - second_half = text[middle - overlap :] - yield from self.split_page_by_max_tokens(page_num, first_half) - yield from self.split_page_by_max_tokens(page_num, second_half) - - def split_pages(self, pages: List[Page]) -> Generator[SplitPage, None, None]: - def find_page(offset): - num_pages = len(pages) - for i in range(num_pages - 1): - if offset >= pages[i].offset and offset < pages[i + 1].offset: - return pages[i].page_num - return pages[num_pages - 1].page_num - - all_text = "".join(page.text for page in pages) - if len(all_text.strip()) == 0: - return - - length = len(all_text) - if length <= self.max_section_length: - yield from self.split_page_by_max_tokens(page_num=find_page(0), text=all_text) - return - - start = 0 - end = length - while start + self.section_overlap < length: - last_word = -1 - end = start + self.max_section_length - - if end > length: - end = length - else: - # Try to find the end of the sentence - while ( - end < length - and (end - start - self.max_section_length) < self.sentence_search_limit - and all_text[end] not in self.sentence_endings - ): - if all_text[end] in self.word_breaks: - last_word = end - end += 1 - if end < length and all_text[end] not in self.sentence_endings and last_word > 0: - end = last_word # Fall back to at least keeping a whole word - if end < length: - end += 1 - - # Try to find the start of the sentence or at least a whole word boundary - last_word = -1 - while ( - start > 0 - and start > end - self.max_section_length - 2 * self.sentence_search_limit - and all_text[start] not in self.sentence_endings - ): - if all_text[start] in self.word_breaks: - last_word = start - start -= 1 - if all_text[start] not in self.sentence_endings and last_word > 0: - start = last_word - if start > 0: - start += 1 - - section_text = all_text[start:end] - yield from self.split_page_by_max_tokens(page_num=find_page(start), text=section_text) - - last_table_start = section_text.rfind(" 2 * self.sentence_search_limit and last_table_start > section_text.rfind(" Generator[SplitPage, None, None]: - all_text = "".join(page.text for page in pages) - if len(all_text.strip()) == 0: - return - - length = len(all_text) - if length <= self.max_object_length: - yield SplitPage(page_num=0, text=all_text) - return - - # its too big, so we need to split it - for i in range(0, length, self.max_object_length): - yield SplitPage(page_num=i // self.max_object_length, text=all_text[i : i + self.max_object_length]) - return +import logging +from abc import ABC +from typing import Generator, List + +import tiktoken + +from .page import Page, SplitPage + +logger = logging.getLogger("ingester") + + +class TextSplitter(ABC): + """ + Splits a list of pages into smaller chunks + :param pages: The pages to split + :return: A generator of SplitPage + """ + + def split_pages(self, pages: List[Page]) -> Generator[SplitPage, None, None]: + if False: + yield # pragma: no cover - this is necessary for mypy to type check + + +ENCODING_MODEL = "text-embedding-ada-002" + +STANDARD_WORD_BREAKS = [",", ";", ":", " ", "(", ")", "[", "]", "{", "}", "\t", "\n"] + +# See W3C document https://www.w3.org/TR/jlreq/#cl-01 +CJK_WORD_BREAKS = [ + "、", + ",", + ";", + ":", + "(", + ")", + "【", + "】", + "「", + "」", + "『", + "』", + "〔", + "〕", + "〈", + "〉", + "《", + "》", + "〖", + "〗", + "〘", + "〙", + "〚", + "〛", + "〝", + "〞", + "〟", + "〰", + "–", + "—", + "‘", + "’", + "‚", + "‛", + "“", + "”", + "„", + "‟", + "‹", + "›", +] + +STANDARD_SENTENCE_ENDINGS = [".", "!", "?"] + +# See CL05 and CL06, based on JIS X 4051:2004 +# https://www.w3.org/TR/jlreq/#cl-04 +CJK_SENTENCE_ENDINGS = ["。", "!", "?", "‼", "⁇", "⁈", "⁉"] + +# NB: text-embedding-3-XX is the same BPE as text-embedding-ada-002 +bpe = tiktoken.encoding_for_model(ENCODING_MODEL) + +DEFAULT_OVERLAP_PERCENT = 10 # See semantic search article for 10% overlap performance +DEFAULT_SECTION_LENGTH = 1000 # Roughly 400-500 tokens for English + + +class SentenceTextSplitter(TextSplitter): + """ + Class that splits pages into smaller chunks. This is required because embedding models may not be able to analyze an entire page at once + """ + + def __init__(self, has_image_embeddings: bool, max_tokens_per_section: int = 500): + self.sentence_endings = STANDARD_SENTENCE_ENDINGS + CJK_SENTENCE_ENDINGS + self.word_breaks = STANDARD_WORD_BREAKS + CJK_WORD_BREAKS + self.max_section_length = DEFAULT_SECTION_LENGTH + self.sentence_search_limit = 100 + self.max_tokens_per_section = max_tokens_per_section + self.section_overlap = int(self.max_section_length * DEFAULT_OVERLAP_PERCENT / 100) + self.has_image_embeddings = has_image_embeddings + + def split_page_by_max_tokens(self, page_num: int, text: str) -> Generator[SplitPage, None, None]: + """ + Recursively splits page by maximum number of tokens to better handle languages with higher token/word ratios. + """ + tokens = bpe.encode(text) + if len(tokens) <= self.max_tokens_per_section: + # Section is already within max tokens, return + yield SplitPage(page_num=page_num, text=text) + else: + # Start from the center and try and find the closest sentence ending by spiralling outward. + # IF we get to the outer thirds, then just split in half with a 5% overlap + start = int(len(text) // 2) + pos = 0 + boundary = int(len(text) // 3) + split_position = -1 + while start - pos > boundary: + if text[start - pos] in self.sentence_endings: + split_position = start - pos + break + elif text[start + pos] in self.sentence_endings: + split_position = start + pos + break + else: + pos += 1 + + if split_position > 0: + first_half = text[: split_position + 1] + second_half = text[split_position + 1 :] + else: + # Split page in half and call function again + # Overlap first and second halves by DEFAULT_OVERLAP_PERCENT% + middle = int(len(text) // 2) + overlap = int(len(text) * (DEFAULT_OVERLAP_PERCENT / 100)) + first_half = text[: middle + overlap] + second_half = text[middle - overlap :] + yield from self.split_page_by_max_tokens(page_num, first_half) + yield from self.split_page_by_max_tokens(page_num, second_half) + + def split_pages(self, pages: List[Page]) -> Generator[SplitPage, None, None]: + def find_page(offset): + num_pages = len(pages) + for i in range(num_pages - 1): + if offset >= pages[i].offset and offset < pages[i + 1].offset: + return pages[i].page_num + return pages[num_pages - 1].page_num + + all_text = "".join(page.text for page in pages) + if len(all_text.strip()) == 0: + return + + length = len(all_text) + if length <= self.max_section_length: + yield from self.split_page_by_max_tokens(page_num=find_page(0), text=all_text) + return + + start = 0 + end = length + while start + self.section_overlap < length: + last_word = -1 + end = start + self.max_section_length + + if end > length: + end = length + else: + # Try to find the end of the sentence + while ( + end < length + and (end - start - self.max_section_length) < self.sentence_search_limit + and all_text[end] not in self.sentence_endings + ): + if all_text[end] in self.word_breaks: + last_word = end + end += 1 + if end < length and all_text[end] not in self.sentence_endings and last_word > 0: + end = last_word # Fall back to at least keeping a whole word + if end < length: + end += 1 + + # Try to find the start of the sentence or at least a whole word boundary + last_word = -1 + while ( + start > 0 + and start > end - self.max_section_length - 2 * self.sentence_search_limit + and all_text[start] not in self.sentence_endings + ): + if all_text[start] in self.word_breaks: + last_word = start + start -= 1 + if all_text[start] not in self.sentence_endings and last_word > 0: + start = last_word + if start > 0: + start += 1 + + section_text = all_text[start:end] + yield from self.split_page_by_max_tokens(page_num=find_page(start), text=section_text) + + last_table_start = section_text.rfind(" 2 * self.sentence_search_limit and last_table_start > section_text.rfind(" Generator[SplitPage, None, None]: + all_text = "".join(page.text for page in pages) + if len(all_text.strip()) == 0: + return + + length = len(all_text) + if length <= self.max_object_length: + yield SplitPage(page_num=0, text=all_text) + return + + # its too big, so we need to split it + for i in range(0, length, self.max_object_length): + yield SplitPage(page_num=i // self.max_object_length, text=all_text[i : i + self.max_object_length]) + return diff --git a/app/backend/requirements.in b/app/backend/requirements.in index 93bdd3b609..6d3f812288 100644 --- a/app/backend/requirements.in +++ b/app/backend/requirements.in @@ -1,32 +1,32 @@ -azure-identity -quart -quart-cors -openai>=1.3.7 -numpy>=1 # Used by openai embeddings.create to optimize embeddings (but not required) -tiktoken -tenacity -azure-ai-documentintelligence -azure-cognitiveservices-speech -azure-search-documents==11.6.0b1 -azure-storage-blob -azure-storage-file-datalake -uvicorn -aiohttp -azure-monitor-opentelemetry -opentelemetry-instrumentation-asgi -opentelemetry-instrumentation-httpx -opentelemetry-instrumentation-requests -opentelemetry-instrumentation-aiohttp-client -opentelemetry-instrumentation-openai -msal -cryptography -python-jose[cryptography] -types-python-jose -Pillow -types-Pillow -pypdf -PyMuPDF -beautifulsoup4 -types-beautifulsoup4 -msgraph-sdk==1.1.0 -openai-messages-token-helper +azure-identity +quart +quart-cors +openai>=1.3.7 +numpy>=1 # Used by openai embeddings.create to optimize embeddings (but not required) +tiktoken +tenacity +azure-ai-documentintelligence +azure-cognitiveservices-speech +azure-search-documents==11.6.0b1 +azure-storage-blob +azure-storage-file-datalake +uvicorn +aiohttp +azure-monitor-opentelemetry +opentelemetry-instrumentation-asgi +opentelemetry-instrumentation-httpx +opentelemetry-instrumentation-requests +opentelemetry-instrumentation-aiohttp-client +opentelemetry-instrumentation-openai +msal +cryptography +python-jose[cryptography] +types-python-jose +Pillow +types-Pillow +pypdf +PyMuPDF +beautifulsoup4 +types-beautifulsoup4 +msgraph-sdk==1.1.0 +openai-messages-token-helper diff --git a/app/backend/requirements.txt b/app/backend/requirements.txt index 20d564b4d2..a49bca4f3b 100644 --- a/app/backend/requirements.txt +++ b/app/backend/requirements.txt @@ -1,447 +1,447 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile requirements.in -# -aiofiles==23.2.1 - # via quart -aiohttp==3.9.5 - # via - # -r requirements.in - # microsoft-kiota-authentication-azure -aiosignal==1.3.1 - # via aiohttp -annotated-types==0.7.0 - # via pydantic -anyio==4.4.0 - # via - # httpx - # openai -asgiref==3.8.1 - # via opentelemetry-instrumentation-asgi -attrs==23.2.0 - # via aiohttp -azure-ai-documentintelligence==1.0.0b3 - # via -r requirements.in -azure-cognitiveservices-speech==1.38.0 - # via -r requirements.in -azure-common==1.1.28 - # via azure-search-documents -azure-core==1.30.2 - # via - # azure-ai-documentintelligence - # azure-core-tracing-opentelemetry - # azure-identity - # azure-monitor-opentelemetry - # azure-monitor-opentelemetry-exporter - # azure-search-documents - # azure-storage-blob - # azure-storage-file-datalake - # microsoft-kiota-authentication-azure - # msrest -azure-core-tracing-opentelemetry==1.0.0b11 - # via azure-monitor-opentelemetry -azure-identity==1.17.0 - # via - # -r requirements.in - # msgraph-sdk -azure-monitor-opentelemetry==1.6.0 - # via -r requirements.in -azure-monitor-opentelemetry-exporter==1.0.0b26 - # via azure-monitor-opentelemetry -azure-search-documents==11.6.0b1 - # via -r requirements.in -azure-storage-blob==12.20.0 - # via - # -r requirements.in - # azure-storage-file-datalake -azure-storage-file-datalake==12.15.0 - # via -r requirements.in -beautifulsoup4==4.12.3 - # via -r requirements.in -blinker==1.8.2 - # via - # flask - # quart -certifi==2024.6.2 - # via - # httpcore - # httpx - # msrest - # requests -cffi==1.16.0 - # via cryptography -charset-normalizer==3.3.2 - # via requests -click==8.1.7 - # via - # flask - # quart - # uvicorn -cryptography==42.0.8 - # via - # -r requirements.in - # azure-identity - # azure-storage-blob - # msal - # pyjwt - # python-jose -deprecated==1.2.14 - # via opentelemetry-api -distro==1.9.0 - # via openai -ecdsa==0.19.0 - # via python-jose -fixedint==0.1.6 - # via azure-monitor-opentelemetry-exporter -flask==3.0.3 - # via quart -frozenlist==1.4.1 - # via - # aiohttp - # aiosignal -h11==0.14.0 - # via - # httpcore - # hypercorn - # uvicorn - # wsproto -h2==4.1.0 - # via - # httpx - # hypercorn -hpack==4.0.0 - # via h2 -httpcore==1.0.5 - # via httpx -httpx[http2]==0.27.0 - # via - # microsoft-kiota-http - # msgraph-core - # openai -hypercorn==0.17.3 - # via quart -hyperframe==6.0.1 - # via h2 -idna==3.7 - # via - # anyio - # httpx - # requests - # yarl -importlib-metadata==7.1.0 - # via - # opentelemetry-api - # opentelemetry-instrumentation-flask -isodate==0.6.1 - # via - # azure-ai-documentintelligence - # azure-search-documents - # azure-storage-blob - # azure-storage-file-datalake - # msrest -itsdangerous==2.2.0 - # via - # flask - # quart -jinja2==3.1.4 - # via - # flask - # quart -markupsafe==2.1.5 - # via - # jinja2 - # quart - # werkzeug -microsoft-kiota-abstractions==1.3.3 - # via - # microsoft-kiota-authentication-azure - # microsoft-kiota-http - # microsoft-kiota-serialization-json - # microsoft-kiota-serialization-text - # msgraph-core - # msgraph-sdk -microsoft-kiota-authentication-azure==1.0.0 - # via - # msgraph-core - # msgraph-sdk -microsoft-kiota-http==1.3.1 - # via - # msgraph-core - # msgraph-sdk -microsoft-kiota-serialization-json==1.2.0 - # via msgraph-sdk -microsoft-kiota-serialization-text==1.0.0 - # via msgraph-sdk -msal==1.28.1 - # via - # -r requirements.in - # azure-identity - # msal-extensions -msal-extensions==1.1.0 - # via azure-identity -msgraph-core==1.1.0 - # via msgraph-sdk -msgraph-sdk==1.1.0 - # via -r requirements.in -msrest==0.7.1 - # via azure-monitor-opentelemetry-exporter -multidict==6.0.5 - # via - # aiohttp - # yarl -numpy==2.0.0 - # via -r requirements.in -oauthlib==3.2.2 - # via requests-oauthlib -openai==1.35.1 - # via - # -r requirements.in - # openai-messages-token-helper -openai-messages-token-helper==0.1.5 - # via -r requirements.in -opentelemetry-api==1.25.0 - # via - # azure-core-tracing-opentelemetry - # azure-monitor-opentelemetry-exporter - # microsoft-kiota-abstractions - # microsoft-kiota-authentication-azure - # microsoft-kiota-http - # opentelemetry-instrumentation - # opentelemetry-instrumentation-aiohttp-client - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-dbapi - # opentelemetry-instrumentation-django - # opentelemetry-instrumentation-fastapi - # opentelemetry-instrumentation-flask - # opentelemetry-instrumentation-httpx - # opentelemetry-instrumentation-openai - # opentelemetry-instrumentation-psycopg2 - # opentelemetry-instrumentation-requests - # opentelemetry-instrumentation-urllib - # opentelemetry-instrumentation-urllib3 - # opentelemetry-instrumentation-wsgi - # opentelemetry-sdk - # opentelemetry-semantic-conventions -opentelemetry-instrumentation==0.46b0 - # via - # opentelemetry-instrumentation-aiohttp-client - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-dbapi - # opentelemetry-instrumentation-django - # opentelemetry-instrumentation-fastapi - # opentelemetry-instrumentation-flask - # opentelemetry-instrumentation-httpx - # opentelemetry-instrumentation-openai - # opentelemetry-instrumentation-psycopg2 - # opentelemetry-instrumentation-requests - # opentelemetry-instrumentation-urllib - # opentelemetry-instrumentation-urllib3 - # opentelemetry-instrumentation-wsgi -opentelemetry-instrumentation-aiohttp-client==0.46b0 - # via -r requirements.in -opentelemetry-instrumentation-asgi==0.46b0 - # via - # -r requirements.in - # opentelemetry-instrumentation-fastapi -opentelemetry-instrumentation-dbapi==0.46b0 - # via opentelemetry-instrumentation-psycopg2 -opentelemetry-instrumentation-django==0.46b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-fastapi==0.46b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-flask==0.46b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-httpx==0.46b0 - # via -r requirements.in -opentelemetry-instrumentation-openai==0.23.0 - # via -r requirements.in -opentelemetry-instrumentation-psycopg2==0.46b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-requests==0.46b0 - # via - # -r requirements.in - # azure-monitor-opentelemetry -opentelemetry-instrumentation-urllib==0.46b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-urllib3==0.46b0 - # via azure-monitor-opentelemetry -opentelemetry-instrumentation-wsgi==0.46b0 - # via - # opentelemetry-instrumentation-django - # opentelemetry-instrumentation-flask -opentelemetry-resource-detector-azure==0.1.5 - # via azure-monitor-opentelemetry -opentelemetry-sdk==1.25.0 - # via - # azure-monitor-opentelemetry - # azure-monitor-opentelemetry-exporter - # microsoft-kiota-abstractions - # microsoft-kiota-authentication-azure - # microsoft-kiota-http - # opentelemetry-resource-detector-azure -opentelemetry-semantic-conventions==0.46b0 - # via - # opentelemetry-instrumentation-aiohttp-client - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-dbapi - # opentelemetry-instrumentation-django - # opentelemetry-instrumentation-fastapi - # opentelemetry-instrumentation-flask - # opentelemetry-instrumentation-httpx - # opentelemetry-instrumentation-openai - # opentelemetry-instrumentation-requests - # opentelemetry-instrumentation-urllib - # opentelemetry-instrumentation-urllib3 - # opentelemetry-instrumentation-wsgi - # opentelemetry-sdk -opentelemetry-semantic-conventions-ai==0.3.1 - # via opentelemetry-instrumentation-openai -opentelemetry-util-http==0.46b0 - # via - # opentelemetry-instrumentation-aiohttp-client - # opentelemetry-instrumentation-asgi - # opentelemetry-instrumentation-django - # opentelemetry-instrumentation-fastapi - # opentelemetry-instrumentation-flask - # opentelemetry-instrumentation-httpx - # opentelemetry-instrumentation-requests - # opentelemetry-instrumentation-urllib - # opentelemetry-instrumentation-urllib3 - # opentelemetry-instrumentation-wsgi -packaging==24.1 - # via - # msal-extensions - # opentelemetry-instrumentation-flask -pendulum==3.0.0 - # via microsoft-kiota-serialization-json -pillow==10.3.0 - # via - # -r requirements.in - # openai-messages-token-helper -portalocker==2.8.2 - # via msal-extensions -priority==2.0.0 - # via hypercorn -psutil==5.9.8 - # via azure-monitor-opentelemetry-exporter -pyasn1==0.6.0 - # via - # python-jose - # rsa -pycparser==2.22 - # via cffi -pydantic==2.7.4 - # via openai -pydantic-core==2.18.4 - # via pydantic -pyjwt[crypto]==2.8.0 - # via - # msal - # pyjwt -pymupdf==1.24.5 - # via -r requirements.in -pymupdfb==1.24.3 - # via pymupdf -pypdf==4.2.0 - # via -r requirements.in -python-dateutil==2.9.0.post0 - # via - # microsoft-kiota-serialization-text - # pendulum - # time-machine -python-jose[cryptography]==3.3.0 - # via -r requirements.in -quart==0.19.6 - # via - # -r requirements.in - # quart-cors -quart-cors==0.7.0 - # via -r requirements.in -regex==2024.5.15 - # via tiktoken -requests==2.32.3 - # via - # azure-core - # msal - # msrest - # requests-oauthlib - # tiktoken -requests-oauthlib==2.0.0 - # via msrest -rsa==4.9 - # via python-jose -six==1.16.0 - # via - # azure-core - # ecdsa - # isodate - # python-dateutil -sniffio==1.3.1 - # via - # anyio - # httpx - # openai -soupsieve==2.5 - # via beautifulsoup4 -std-uritemplate==1.0.1 - # via microsoft-kiota-abstractions -tenacity==8.4.1 - # via -r requirements.in -tiktoken==0.7.0 - # via - # -r requirements.in - # openai-messages-token-helper - # opentelemetry-instrumentation-openai -time-machine==2.14.1 - # via pendulum -tqdm==4.66.4 - # via openai -types-beautifulsoup4==4.12.0.20240511 - # via -r requirements.in -types-html5lib==1.1.11.20240228 - # via types-beautifulsoup4 -types-pillow==10.2.0.20240520 - # via -r requirements.in -types-pyasn1==0.6.0.20240402 - # via types-python-jose -types-python-jose==3.3.4.20240106 - # via -r requirements.in -typing-extensions==4.12.2 - # via - # azure-ai-documentintelligence - # azure-core - # azure-identity - # azure-storage-blob - # azure-storage-file-datalake - # openai - # opentelemetry-sdk - # pydantic - # pydantic-core -tzdata==2024.1 - # via pendulum -urllib3==2.2.2 - # via requests -uvicorn==0.30.1 - # via -r requirements.in -werkzeug==3.0.3 - # via - # flask - # quart -wrapt==1.16.0 - # via - # deprecated - # opentelemetry-instrumentation - # opentelemetry-instrumentation-aiohttp-client - # opentelemetry-instrumentation-dbapi - # opentelemetry-instrumentation-urllib3 -wsproto==1.2.0 - # via hypercorn -yarl==1.9.4 - # via aiohttp -zipp==3.19.2 - # via importlib-metadata - -# The following packages are considered to be unsafe in a requirements file: -# setuptools +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile requirements.in +# +aiofiles==23.2.1 + # via quart +aiohttp==3.9.5 + # via + # -r requirements.in + # microsoft-kiota-authentication-azure +aiosignal==1.3.1 + # via aiohttp +annotated-types==0.7.0 + # via pydantic +anyio==4.4.0 + # via + # httpx + # openai +asgiref==3.8.1 + # via opentelemetry-instrumentation-asgi +attrs==23.2.0 + # via aiohttp +azure-ai-documentintelligence==1.0.0b3 + # via -r requirements.in +azure-cognitiveservices-speech==1.38.0 + # via -r requirements.in +azure-common==1.1.28 + # via azure-search-documents +azure-core==1.30.2 + # via + # azure-ai-documentintelligence + # azure-core-tracing-opentelemetry + # azure-identity + # azure-monitor-opentelemetry + # azure-monitor-opentelemetry-exporter + # azure-search-documents + # azure-storage-blob + # azure-storage-file-datalake + # microsoft-kiota-authentication-azure + # msrest +azure-core-tracing-opentelemetry==1.0.0b11 + # via azure-monitor-opentelemetry +azure-identity==1.17.0 + # via + # -r requirements.in + # msgraph-sdk +azure-monitor-opentelemetry==1.6.0 + # via -r requirements.in +azure-monitor-opentelemetry-exporter==1.0.0b26 + # via azure-monitor-opentelemetry +azure-search-documents==11.6.0b1 + # via -r requirements.in +azure-storage-blob==12.20.0 + # via + # -r requirements.in + # azure-storage-file-datalake +azure-storage-file-datalake==12.15.0 + # via -r requirements.in +beautifulsoup4==4.12.3 + # via -r requirements.in +blinker==1.8.2 + # via + # flask + # quart +certifi==2024.6.2 + # via + # httpcore + # httpx + # msrest + # requests +cffi==1.16.0 + # via cryptography +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via + # flask + # quart + # uvicorn +cryptography==42.0.8 + # via + # -r requirements.in + # azure-identity + # azure-storage-blob + # msal + # pyjwt + # python-jose +deprecated==1.2.14 + # via opentelemetry-api +distro==1.9.0 + # via openai +ecdsa==0.19.0 + # via python-jose +fixedint==0.1.6 + # via azure-monitor-opentelemetry-exporter +flask==3.0.3 + # via quart +frozenlist==1.4.1 + # via + # aiohttp + # aiosignal +h11==0.14.0 + # via + # httpcore + # hypercorn + # uvicorn + # wsproto +h2==4.1.0 + # via + # httpx + # hypercorn +hpack==4.0.0 + # via h2 +httpcore==1.0.5 + # via httpx +httpx[http2]==0.27.0 + # via + # microsoft-kiota-http + # msgraph-core + # openai +hypercorn==0.17.3 + # via quart +hyperframe==6.0.1 + # via h2 +idna==3.7 + # via + # anyio + # httpx + # requests + # yarl +importlib-metadata==7.1.0 + # via + # opentelemetry-api + # opentelemetry-instrumentation-flask +isodate==0.6.1 + # via + # azure-ai-documentintelligence + # azure-search-documents + # azure-storage-blob + # azure-storage-file-datalake + # msrest +itsdangerous==2.2.0 + # via + # flask + # quart +jinja2==3.1.4 + # via + # flask + # quart +markupsafe==2.1.5 + # via + # jinja2 + # quart + # werkzeug +microsoft-kiota-abstractions==1.3.3 + # via + # microsoft-kiota-authentication-azure + # microsoft-kiota-http + # microsoft-kiota-serialization-json + # microsoft-kiota-serialization-text + # msgraph-core + # msgraph-sdk +microsoft-kiota-authentication-azure==1.0.0 + # via + # msgraph-core + # msgraph-sdk +microsoft-kiota-http==1.3.1 + # via + # msgraph-core + # msgraph-sdk +microsoft-kiota-serialization-json==1.2.0 + # via msgraph-sdk +microsoft-kiota-serialization-text==1.0.0 + # via msgraph-sdk +msal==1.28.1 + # via + # -r requirements.in + # azure-identity + # msal-extensions +msal-extensions==1.1.0 + # via azure-identity +msgraph-core==1.1.0 + # via msgraph-sdk +msgraph-sdk==1.1.0 + # via -r requirements.in +msrest==0.7.1 + # via azure-monitor-opentelemetry-exporter +multidict==6.0.5 + # via + # aiohttp + # yarl +numpy==2.0.0 + # via -r requirements.in +oauthlib==3.2.2 + # via requests-oauthlib +openai==1.35.1 + # via + # -r requirements.in + # openai-messages-token-helper +openai-messages-token-helper==0.1.5 + # via -r requirements.in +opentelemetry-api==1.25.0 + # via + # azure-core-tracing-opentelemetry + # azure-monitor-opentelemetry-exporter + # microsoft-kiota-abstractions + # microsoft-kiota-authentication-azure + # microsoft-kiota-http + # opentelemetry-instrumentation + # opentelemetry-instrumentation-aiohttp-client + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-dbapi + # opentelemetry-instrumentation-django + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-httpx + # opentelemetry-instrumentation-openai + # opentelemetry-instrumentation-psycopg2 + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-urllib + # opentelemetry-instrumentation-urllib3 + # opentelemetry-instrumentation-wsgi + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-instrumentation==0.46b0 + # via + # opentelemetry-instrumentation-aiohttp-client + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-dbapi + # opentelemetry-instrumentation-django + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-httpx + # opentelemetry-instrumentation-openai + # opentelemetry-instrumentation-psycopg2 + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-urllib + # opentelemetry-instrumentation-urllib3 + # opentelemetry-instrumentation-wsgi +opentelemetry-instrumentation-aiohttp-client==0.46b0 + # via -r requirements.in +opentelemetry-instrumentation-asgi==0.46b0 + # via + # -r requirements.in + # opentelemetry-instrumentation-fastapi +opentelemetry-instrumentation-dbapi==0.46b0 + # via opentelemetry-instrumentation-psycopg2 +opentelemetry-instrumentation-django==0.46b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-fastapi==0.46b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-flask==0.46b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-httpx==0.46b0 + # via -r requirements.in +opentelemetry-instrumentation-openai==0.23.0 + # via -r requirements.in +opentelemetry-instrumentation-psycopg2==0.46b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-requests==0.46b0 + # via + # -r requirements.in + # azure-monitor-opentelemetry +opentelemetry-instrumentation-urllib==0.46b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-urllib3==0.46b0 + # via azure-monitor-opentelemetry +opentelemetry-instrumentation-wsgi==0.46b0 + # via + # opentelemetry-instrumentation-django + # opentelemetry-instrumentation-flask +opentelemetry-resource-detector-azure==0.1.5 + # via azure-monitor-opentelemetry +opentelemetry-sdk==1.25.0 + # via + # azure-monitor-opentelemetry + # azure-monitor-opentelemetry-exporter + # microsoft-kiota-abstractions + # microsoft-kiota-authentication-azure + # microsoft-kiota-http + # opentelemetry-resource-detector-azure +opentelemetry-semantic-conventions==0.46b0 + # via + # opentelemetry-instrumentation-aiohttp-client + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-dbapi + # opentelemetry-instrumentation-django + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-httpx + # opentelemetry-instrumentation-openai + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-urllib + # opentelemetry-instrumentation-urllib3 + # opentelemetry-instrumentation-wsgi + # opentelemetry-sdk +opentelemetry-semantic-conventions-ai==0.3.1 + # via opentelemetry-instrumentation-openai +opentelemetry-util-http==0.46b0 + # via + # opentelemetry-instrumentation-aiohttp-client + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-django + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-httpx + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-urllib + # opentelemetry-instrumentation-urllib3 + # opentelemetry-instrumentation-wsgi +packaging==24.1 + # via + # msal-extensions + # opentelemetry-instrumentation-flask +pendulum==3.0.0 + # via microsoft-kiota-serialization-json +pillow==10.3.0 + # via + # -r requirements.in + # openai-messages-token-helper +portalocker==2.8.2 + # via msal-extensions +priority==2.0.0 + # via hypercorn +psutil==5.9.8 + # via azure-monitor-opentelemetry-exporter +pyasn1==0.6.0 + # via + # python-jose + # rsa +pycparser==2.22 + # via cffi +pydantic==2.7.4 + # via openai +pydantic-core==2.18.4 + # via pydantic +pyjwt[crypto]==2.8.0 + # via + # msal + # pyjwt +pymupdf==1.24.5 + # via -r requirements.in +pymupdfb==1.24.3 + # via pymupdf +pypdf==4.2.0 + # via -r requirements.in +python-dateutil==2.9.0.post0 + # via + # microsoft-kiota-serialization-text + # pendulum + # time-machine +python-jose[cryptography]==3.3.0 + # via -r requirements.in +quart==0.19.6 + # via + # -r requirements.in + # quart-cors +quart-cors==0.7.0 + # via -r requirements.in +regex==2024.5.15 + # via tiktoken +requests==2.32.3 + # via + # azure-core + # msal + # msrest + # requests-oauthlib + # tiktoken +requests-oauthlib==2.0.0 + # via msrest +rsa==4.9 + # via python-jose +six==1.16.0 + # via + # azure-core + # ecdsa + # isodate + # python-dateutil +sniffio==1.3.1 + # via + # anyio + # httpx + # openai +soupsieve==2.5 + # via beautifulsoup4 +std-uritemplate==1.0.1 + # via microsoft-kiota-abstractions +tenacity==8.4.1 + # via -r requirements.in +tiktoken==0.7.0 + # via + # -r requirements.in + # openai-messages-token-helper + # opentelemetry-instrumentation-openai +time-machine==2.14.1 + # via pendulum +tqdm==4.66.4 + # via openai +types-beautifulsoup4==4.12.0.20240511 + # via -r requirements.in +types-html5lib==1.1.11.20240228 + # via types-beautifulsoup4 +types-pillow==10.2.0.20240520 + # via -r requirements.in +types-pyasn1==0.6.0.20240402 + # via types-python-jose +types-python-jose==3.3.4.20240106 + # via -r requirements.in +typing-extensions==4.12.2 + # via + # azure-ai-documentintelligence + # azure-core + # azure-identity + # azure-storage-blob + # azure-storage-file-datalake + # openai + # opentelemetry-sdk + # pydantic + # pydantic-core +tzdata==2024.1 + # via pendulum +urllib3==2.2.2 + # via requests +uvicorn==0.30.1 + # via -r requirements.in +werkzeug==3.0.3 + # via + # flask + # quart +wrapt==1.16.0 + # via + # deprecated + # opentelemetry-instrumentation + # opentelemetry-instrumentation-aiohttp-client + # opentelemetry-instrumentation-dbapi + # opentelemetry-instrumentation-urllib3 +wsproto==1.2.0 + # via hypercorn +yarl==1.9.4 + # via aiohttp +zipp==3.19.2 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/app/backend/text.py b/app/backend/text.py index 98d099d8d1..c98b6ec71e 100644 --- a/app/backend/text.py +++ b/app/backend/text.py @@ -1,2 +1,2 @@ -def nonewlines(s: str) -> str: - return s.replace("\n", " ").replace("\r", " ") +def nonewlines(s: str) -> str: + return s.replace("\n", " ").replace("\r", " ") diff --git a/app/frontend/.npmrc b/app/frontend/.npmrc index 727cdb2649..6be8b6969f 100644 --- a/app/frontend/.npmrc +++ b/app/frontend/.npmrc @@ -1,2 +1,2 @@ -engine-strict=true -fund=false +engine-strict=true +fund=false diff --git a/app/frontend/.nvmrc b/app/frontend/.nvmrc index 946789e619..33734f38f4 100644 --- a/app/frontend/.nvmrc +++ b/app/frontend/.nvmrc @@ -1 +1 @@ -16.0.0 +16.0.0 diff --git a/app/frontend/.prettierignore b/app/frontend/.prettierignore index fc355bcdfb..053c767d8b 100644 --- a/app/frontend/.prettierignore +++ b/app/frontend/.prettierignore @@ -1,2 +1,2 @@ -# Ignore JSON -**/*.json +# Ignore JSON +**/*.json diff --git a/app/frontend/.prettierrc.json b/app/frontend/.prettierrc.json index b7d67747c8..9731f92d1c 100644 --- a/app/frontend/.prettierrc.json +++ b/app/frontend/.prettierrc.json @@ -1,6 +1,6 @@ -{ - "tabWidth": 4, - "printWidth": 160, - "arrowParens": "avoid", - "trailingComma": "none" -} +{ + "tabWidth": 4, + "printWidth": 160, + "arrowParens": "avoid", + "trailingComma": "none" +} diff --git a/app/frontend/index.html b/app/frontend/index.html index a7aa7e9eff..a6bb0aca6d 100644 --- a/app/frontend/index.html +++ b/app/frontend/index.html @@ -4,7 +4,20 @@ - GPT + Enterprise data | Sample + Dubai Health Authority Policy Assistant | Demo + + +
+ + + + + + + + + + Policy Assistant | Demo
diff --git a/app/frontend/package-lock.json b/app/frontend/package-lock.json index d9c93985d4..ef1bdb8ab7 100644 --- a/app/frontend/package-lock.json +++ b/app/frontend/package-lock.json @@ -1,5867 +1,5867 @@ -{ - "name": "frontend", - "version": "0.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.0", - "dependencies": { - "@azure/msal-browser": "^3.17.0", - "@azure/msal-react": "^2.0.6", - "@fluentui/react": "^8.112.5", - "@fluentui/react-components": "^9.37.3", - "@fluentui/react-icons": "^2.0.221", - "@react-spring/web": "^9.7.3", - "dompurify": "^3.0.6", - "marked": "^13.0.0", - "ndjson-readablestream": "^1.2.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.23.1", - "react-syntax-highlighter": "^15.5.0", - "scheduler": "^0.20.2" - }, - "devDependencies": { - "@types/dom-speech-recognition": "^0.0.4", - "@types/dompurify": "^3.0.4", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@types/react-syntax-highlighter": "^15.5.13", - "@vitejs/plugin-react": "^4.1.1", - "prettier": "^3.0.3", - "typescript": "^5.4.5", - "vite": "^4.5.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@azure/msal-browser": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.17.0.tgz", - "integrity": "sha512-csccKXmW2z7EkZ0I3yAoW/offQt+JECdTIV/KrnRoZyM7wCSsQWODpwod8ZhYy7iOyamcHApR9uCh0oD1M+0/A==", - "dependencies": { - "@azure/msal-common": "14.12.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-common": { - "version": "14.12.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", - "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-react": { - "version": "2.0.6", - "license": "MIT", - "dependencies": { - "@rollup/plugin-typescript": "^11.1.0", - "rollup": "^3.20.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@azure/msal-browser": "^3.4.0", - "react": "^16.8.0 || ^17 || ^18" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.22.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.22.20", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.23.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.23.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.20", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.0", - "dev": true, - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.22.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.22.15", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.22.15", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.23.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.23.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.1", - "license": "MIT" - }, - "node_modules/@floating-ui/core": { - "version": "1.5.0", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.1.3" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.5.3", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.4.2", - "@floating-ui/utils": "^0.1.3" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.1.6", - "license": "MIT" - }, - "node_modules/@fluentui/date-time-utilities": { - "version": "8.5.14", - "license": "MIT", - "dependencies": { - "@fluentui/set-version": "^8.2.12", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/dom-utilities": { - "version": "2.2.12", - "license": "MIT", - "dependencies": { - "@fluentui/set-version": "^8.2.12", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/font-icons-mdl2": { - "version": "8.5.26", - "license": "MIT", - "dependencies": { - "@fluentui/set-version": "^8.2.12", - "@fluentui/style-utilities": "^8.9.19", - "@fluentui/utilities": "^8.13.20", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/foundation-legacy": { - "version": "8.2.46", - "license": "MIT", - "dependencies": { - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/set-version": "^8.2.12", - "@fluentui/style-utilities": "^8.9.19", - "@fluentui/utilities": "^8.13.20", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/keyboard-key": { - "version": "0.4.12", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/keyboard-keys": { - "version": "9.0.7", - "license": "MIT", - "dependencies": { - "@swc/helpers": "^0.5.1" - } - }, - "node_modules/@fluentui/merge-styles": { - "version": "8.5.13", - "license": "MIT", - "dependencies": { - "@fluentui/set-version": "^8.2.12", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/priority-overflow": { - "version": "9.1.10", - "license": "MIT", - "dependencies": { - "@swc/helpers": "^0.5.1" - } - }, - "node_modules/@fluentui/react": { - "version": "8.112.5", - "license": "MIT", - "dependencies": { - "@fluentui/date-time-utilities": "^8.5.14", - "@fluentui/font-icons-mdl2": "^8.5.26", - "@fluentui/foundation-legacy": "^8.2.46", - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/react-focus": "^8.8.33", - "@fluentui/react-hooks": "^8.6.32", - "@fluentui/react-portal-compat-context": "^9.0.9", - "@fluentui/react-window-provider": "^2.2.16", - "@fluentui/set-version": "^8.2.12", - "@fluentui/style-utilities": "^8.9.19", - "@fluentui/theme": "^2.6.37", - "@fluentui/utilities": "^8.13.20", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-alert": { - "version": "9.0.0-beta.90", - "license": "MIT", - "dependencies": { - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-alert/node_modules/@fluentui/react-avatar": { - "version": "9.5.48", - "license": "MIT", - "dependencies": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-alert/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-alert/node_modules/@fluentui/react-popover": { - "version": "9.8.23", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-aria": { - "version": "9.4.0", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-badge": { - "version": "9.2.15", - "license": "MIT", - "dependencies": { - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-button": { - "version": "9.3.53", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-card": { - "version": "9.0.52", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-checkbox": { - "version": "9.1.54", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-components": { - "version": "9.37.3", - "license": "MIT", - "dependencies": { - "@fluentui/react-accordion": "^9.3.26", - "@fluentui/react-alert": "9.0.0-beta.90", - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-badge": "^9.2.12", - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-card": "^9.0.52", - "@fluentui/react-checkbox": "^9.1.54", - "@fluentui/react-combobox": "^9.5.28", - "@fluentui/react-dialog": "^9.8.2", - "@fluentui/react-divider": "^9.2.48", - "@fluentui/react-drawer": "9.0.0-beta.40", - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-image": "^9.1.45", - "@fluentui/react-infobutton": "9.0.0-beta.74", - "@fluentui/react-infolabel": "^9.0.2", - "@fluentui/react-input": "^9.4.50", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-link": "^9.1.32", - "@fluentui/react-menu": "^9.12.30", - "@fluentui/react-message-bar": "^9.0.4", - "@fluentui/react-overflow": "^9.0.42", - "@fluentui/react-persona": "^9.2.54", - "@fluentui/react-popover": "^9.8.19", - "@fluentui/react-portal": "^9.3.27", - "@fluentui/react-positioning": "^9.9.23", - "@fluentui/react-progress": "^9.1.50", - "@fluentui/react-provider": "^9.11.1", - "@fluentui/react-radio": "^9.1.54", - "@fluentui/react-select": "^9.1.50", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-skeleton": "^9.0.38", - "@fluentui/react-slider": "^9.1.54", - "@fluentui/react-spinbutton": "^9.2.50", - "@fluentui/react-spinner": "^9.3.28", - "@fluentui/react-switch": "^9.1.54", - "@fluentui/react-table": "^9.10.9", - "@fluentui/react-tabs": "^9.3.55", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-tags": "^9.0.8", - "@fluentui/react-text": "^9.3.45", - "@fluentui/react-textarea": "^9.3.50", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-toast": "^9.3.15", - "@fluentui/react-toolbar": "^9.1.54", - "@fluentui/react-tooltip": "^9.3.20", - "@fluentui/react-tree": "^9.4.11", - "@fluentui/react-utilities": "^9.15.1", - "@fluentui/react-virtualizer": "9.0.0-alpha.55", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-accordion": { - "version": "9.3.30", - "license": "MIT", - "dependencies": { - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-accordion/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-avatar": { - "version": "9.5.48", - "license": "MIT", - "dependencies": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-avatar/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-combobox": { - "version": "9.5.32", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-field": "^9.1.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-combobox/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-menu": { - "version": "9.12.35", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-menu/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-overflow": { - "version": "9.1.1", - "license": "MIT", - "dependencies": { - "@fluentui/priority-overflow": "^9.1.10", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-overflow/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-popover": { - "version": "9.8.23", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-popover/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-radio": { - "version": "9.1.58", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-label": "^9.1.51", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-tabs": { - "version": "9.3.59", - "license": "MIT", - "dependencies": { - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-components/node_modules/@fluentui/react-tabs/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-dialog": { - "version": "9.8.2", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-context-selector": "^9.1.41", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-portal": "^9.3.27", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "react-transition-group": "^4.4.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-dialog/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-divider": { - "version": "9.2.48", - "license": "MIT", - "dependencies": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-drawer": { - "version": "9.0.0-beta.40", - "license": "MIT", - "dependencies": { - "@fluentui/react-dialog": "^9.8.2", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-motion-preview": "^0.5.0", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-field": { - "version": "9.1.43", - "license": "MIT", - "dependencies": { - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-label": "^9.1.51", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-field/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-focus": { - "version": "8.8.33", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-key": "^0.4.12", - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/set-version": "^8.2.12", - "@fluentui/style-utilities": "^8.9.19", - "@fluentui/utilities": "^8.13.20", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-hooks": { - "version": "8.6.32", - "license": "MIT", - "dependencies": { - "@fluentui/react-window-provider": "^2.2.16", - "@fluentui/set-version": "^8.2.12", - "@fluentui/utilities": "^8.13.20", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-icons": { - "version": "2.0.221", - "license": "MIT", - "dependencies": { - "@griffel/react": "^1.0.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-image": { - "version": "9.1.45", - "license": "MIT", - "dependencies": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-infobutton": { - "version": "9.0.0-beta.74", - "license": "MIT", - "dependencies": { - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-popover": "^9.8.19", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-infobutton/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-infobutton/node_modules/@fluentui/react-popover": { - "version": "9.8.23", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-infolabel": { - "version": "9.0.2", - "license": "MIT", - "dependencies": { - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-popover": "^9.8.19", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-infolabel/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-infolabel/node_modules/@fluentui/react-popover": { - "version": "9.8.23", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-input": { - "version": "9.4.50", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-jsx-runtime": { - "version": "9.0.20", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-label": { - "version": "9.1.51", - "license": "MIT", - "dependencies": { - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-link": { - "version": "9.1.32", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-message-bar": { - "version": "9.0.4", - "license": "MIT", - "dependencies": { - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "react-transition-group": "^4.4.1" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-motion-preview": { - "version": "0.5.0", - "license": "MIT", - "dependencies": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-persona": { - "version": "9.2.54", - "license": "MIT", - "dependencies": { - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-badge": "^9.2.12", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-persona/node_modules/@fluentui/react-avatar": { - "version": "9.5.48", - "license": "MIT", - "dependencies": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-persona/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-persona/node_modules/@fluentui/react-popover": { - "version": "9.8.23", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-portal": { - "version": "9.4.3", - "license": "MIT", - "dependencies": { - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "use-disposable": "^1.0.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-portal-compat-context": { - "version": "9.0.9", - "license": "MIT", - "dependencies": { - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-positioning": { - "version": "9.10.2", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.2.0", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "floating-ui-devtools": "0.1.2" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-progress": { - "version": "9.1.50", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-provider": { - "version": "9.11.1", - "license": "MIT", - "dependencies": { - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/core": "^1.14.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-select": { - "version": "9.1.50", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-shared-contexts": { - "version": "9.13.0", - "license": "MIT", - "dependencies": { - "@fluentui/react-theme": "^9.1.16", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-skeleton": { - "version": "9.0.38", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-slider": { - "version": "9.1.54", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-spinbutton": { - "version": "9.2.50", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-spinner": { - "version": "9.3.28", - "license": "MIT", - "dependencies": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-switch": { - "version": "9.1.54", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-table": { - "version": "9.10.9", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-checkbox": "^9.1.54", - "@fluentui/react-context-selector": "^9.1.41", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-radio": "^9.1.54", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-table/node_modules/@fluentui/react-avatar": { - "version": "9.5.48", - "license": "MIT", - "dependencies": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-table/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-table/node_modules/@fluentui/react-popover": { - "version": "9.8.23", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-table/node_modules/@fluentui/react-radio": { - "version": "9.1.58", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-label": "^9.1.51", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-tabster": { - "version": "9.15.0", - "license": "MIT", - "dependencies": { - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "keyborg": "^2.2.0", - "tabster": "^5.0.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-tags": { - "version": "9.0.8", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-tags/node_modules/@fluentui/react-avatar": { - "version": "9.5.48", - "license": "MIT", - "dependencies": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-tags/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-tags/node_modules/@fluentui/react-popover": { - "version": "9.8.23", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-text": { - "version": "9.3.45", - "license": "MIT", - "dependencies": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-textarea": { - "version": "9.3.50", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-theme": { - "version": "9.1.16", - "license": "MIT", - "dependencies": { - "@fluentui/tokens": "1.0.0-alpha.13", - "@swc/helpers": "^0.5.1" - } - }, - "node_modules/@fluentui/react-toast": { - "version": "9.3.15", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-portal": "^9.3.27", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "react-transition-group": "^4.4.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-toolbar": { - "version": "9.1.54", - "license": "MIT", - "dependencies": { - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-context-selector": "^9.1.41", - "@fluentui/react-divider": "^9.2.48", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-radio": "^9.1.54", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-toolbar/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-toolbar/node_modules/@fluentui/react-radio": { - "version": "9.1.58", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-label": "^9.1.51", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-tooltip": { - "version": "9.4.1", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-tree": { - "version": "9.4.11", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-checkbox": "^9.1.54", - "@fluentui/react-context-selector": "^9.1.41", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-radio": "^9.1.54", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-tree/node_modules/@fluentui/react-avatar": { - "version": "9.5.48", - "license": "MIT", - "dependencies": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-tree/node_modules/@fluentui/react-context-selector": { - "version": "9.1.42", - "license": "MIT", - "dependencies": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-tree/node_modules/@fluentui/react-popover": { - "version": "9.8.23", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-tree/node_modules/@fluentui/react-radio": { - "version": "9.1.58", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.1.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-label": "^9.1.51", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0", - "scheduler": "^0.19.0 || ^0.20.0" - } - }, - "node_modules/@fluentui/react-utilities": { - "version": "9.15.2", - "license": "MIT", - "dependencies": { - "@fluentui/keyboard-keys": "^9.0.7", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-virtualizer": { - "version": "9.0.0-alpha.55", - "license": "MIT", - "dependencies": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "@types/react-dom": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0", - "react-dom": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-window-provider": { - "version": "2.2.16", - "license": "MIT", - "dependencies": { - "@fluentui/set-version": "^8.2.12", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/set-version": { - "version": "8.2.12", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/style-utilities": { - "version": "8.9.19", - "license": "MIT", - "dependencies": { - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/set-version": "^8.2.12", - "@fluentui/theme": "^2.6.37", - "@fluentui/utilities": "^8.13.20", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/theme": { - "version": "2.6.37", - "license": "MIT", - "dependencies": { - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/set-version": "^8.2.12", - "@fluentui/utilities": "^8.13.20", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/tokens": { - "version": "1.0.0-alpha.13", - "license": "MIT", - "dependencies": { - "@swc/helpers": "^0.5.1" - } - }, - "node_modules/@fluentui/utilities": { - "version": "8.13.20", - "license": "MIT", - "dependencies": { - "@fluentui/dom-utilities": "^2.2.12", - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/set-version": "^8.2.12", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@griffel/core": { - "version": "1.14.4", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.0", - "@griffel/style-types": "^1.0.2", - "csstype": "^3.1.2", - "rtl-css-js": "^1.16.1", - "stylis": "^4.2.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@griffel/react": { - "version": "1.5.17", - "license": "MIT", - "dependencies": { - "@griffel/core": "^1.14.4", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@griffel/style-types": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "csstype": "^3.1.2" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "dev": true, - "license": "MIT" - }, - "node_modules/@microsoft/load-themed-styles": { - "version": "1.10.295", - "license": "MIT" - }, - "node_modules/@react-spring/animated": { - "version": "9.7.3", - "license": "MIT", - "dependencies": { - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/core": { - "version": "9.7.3", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/shared": { - "version": "9.7.3", - "license": "MIT", - "dependencies": { - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.3", - "license": "MIT" - }, - "node_modules/@react-spring/web": { - "version": "9.7.3", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@remix-run/router": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", - "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rollup/plugin-typescript": { - "version": "11.1.3", - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.14.0||^3.0.0", - "tslib": "*", - "typescript": ">=3.7.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - }, - "tslib": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.0.4", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.3", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/dom-speech-recognition": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.4.tgz", - "integrity": "sha512-zf2GwV/G6TdaLwpLDcGTIkHnXf8JEf/viMux+khqKQKDa8/8BAUtXXZS563GnvJ4Fg0PBLGAaFf2GekEVSZ6GQ==", - "dev": true - }, - "node_modules/@types/dompurify": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/trusted-types": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "2.3.7", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.5", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/react-syntax-highlighter": { - "version": "15.5.13", - "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", - "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/unist": { - "version": "2.0.7", - "license": "MIT" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.23.2", - "@babel/plugin-transform-react-jsx-self": "^7.22.5", - "@babel/plugin-transform-react-jsx-source": "^7.22.5", - "@types/babel__core": "^7.20.3", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/browserslist": { - "version": "4.22.1", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001547", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/character-entities": { - "version": "1.2.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "1.1.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "1.1.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/comma-separated-tokens": { - "version": "1.0.8", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.1.2", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/dompurify": { - "version": "3.0.6", - "license": "(MPL-2.0 OR Apache-2.0)" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.549", - "dev": true, - "license": "ISC" - }, - "node_modules/esbuild": { - "version": "0.18.11", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.11", - "@esbuild/android-arm64": "0.18.11", - "@esbuild/android-x64": "0.18.11", - "@esbuild/darwin-arm64": "0.18.11", - "@esbuild/darwin-x64": "0.18.11", - "@esbuild/freebsd-arm64": "0.18.11", - "@esbuild/freebsd-x64": "0.18.11", - "@esbuild/linux-arm": "0.18.11", - "@esbuild/linux-arm64": "0.18.11", - "@esbuild/linux-ia32": "0.18.11", - "@esbuild/linux-loong64": "0.18.11", - "@esbuild/linux-mips64el": "0.18.11", - "@esbuild/linux-ppc64": "0.18.11", - "@esbuild/linux-riscv64": "0.18.11", - "@esbuild/linux-s390x": "0.18.11", - "@esbuild/linux-x64": "0.18.11", - "@esbuild/netbsd-x64": "0.18.11", - "@esbuild/openbsd-x64": "0.18.11", - "@esbuild/sunos-x64": "0.18.11", - "@esbuild/win32-arm64": "0.18.11", - "@esbuild/win32-ia32": "0.18.11", - "@esbuild/win32-x64": "0.18.11" - } - }, - "node_modules/escalade": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "license": "MIT" - }, - "node_modules/fault": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "format": "^0.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/floating-ui-devtools": { - "version": "0.1.2", - "peerDependencies": { - "@floating-ui/dom": ">=1.0.0 <2.0.0" - } - }, - "node_modules/format": { - "version": "0.2.2", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "license": "MIT" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "2.2.5", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/is-alphabetical": { - "version": "1.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-core-module": { - "version": "2.13.0", - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "1.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-hexadecimal": { - "version": "1.0.4", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyborg": { - "version": "2.2.0", - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lowlight": { - "version": "1.20.0", - "license": "MIT", - "dependencies": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/marked": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.0.tgz", - "integrity": "sha512-VTeDCd9txf4KLLljUZ0nljE/Incb9SrWuueE44QVuU0pkOdh4sfCeW1Z6lPcxyDRSVY6rm8db/0OPaN75RNUmw==", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.6", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/ndjson-readablestream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ndjson-readablestream/-/ndjson-readablestream-1.2.0.tgz", - "integrity": "sha512-QbWX2IIfKMVL+ZFHm9vFEzPh1NzZfzJql59T+9XoXzUp8n0wu2t9qgDV9nT0A77YYa6KbAjsHNWzJfpZTfp4xQ==" - }, - "node_modules/node-releases": { - "version": "2.0.13", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parse-entities": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prettier": { - "version": "3.0.3", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prismjs": { - "version": "1.29.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "license": "MIT" - }, - "node_modules/property-information": { - "version": "5.6.0", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-dom/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.14.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", - "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", - "dependencies": { - "@remix-run/router": "1.16.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", - "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", - "dependencies": { - "@remix-run/router": "1.16.1", - "react-router": "6.23.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-syntax-highlighter": { - "version": "15.5.0", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.3.1", - "highlight.js": "^10.4.1", - "lowlight": "^1.17.0", - "prismjs": "^1.27.0", - "refractor": "^3.6.0" - }, - "peerDependencies": { - "react": ">= 0.14.0" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/refractor": { - "version": "3.6.0", - "license": "MIT", - "dependencies": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/prismjs": { - "version": "1.27.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.4", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rollup": { - "version": "3.29.4", - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rtl-css-js": { - "version": "1.16.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.1.2" - } - }, - "node_modules/scheduler": { - "version": "0.20.2", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "1.1.5", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/stylis": { - "version": "4.3.0", - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tabster": { - "version": "5.1.0", - "license": "MIT", - "dependencies": { - "keyborg": "^2.2.0", - "tslib": "^2.3.1" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/tslib": { - "version": "2.5.0", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-disposable": { - "version": "1.0.2", - "license": "MIT", - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" - } - }, - "node_modules/vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", - "dev": true, - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "dev": true, - "license": "ISC" - } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.2.1", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@azure/msal-browser": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.17.0.tgz", - "integrity": "sha512-csccKXmW2z7EkZ0I3yAoW/offQt+JECdTIV/KrnRoZyM7wCSsQWODpwod8ZhYy7iOyamcHApR9uCh0oD1M+0/A==", - "requires": { - "@azure/msal-common": "14.12.0" - } - }, - "@azure/msal-common": { - "version": "14.12.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", - "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==" - }, - "@azure/msal-react": { - "version": "2.0.6", - "requires": { - "@rollup/plugin-typescript": "^11.1.0", - "rollup": "^3.20.2" - } - }, - "@babel/code-frame": { - "version": "7.22.13", - "dev": true, - "requires": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" - } - }, - "@babel/compat-data": { - "version": "7.22.20", - "dev": true - }, - "@babel/core": { - "version": "7.23.2", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.0", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - } - }, - "@babel/generator": { - "version": "7.23.0", - "dev": true, - "requires": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.22.15", - "dev": true, - "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.22.20", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.23.0", - "dev": true, - "requires": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.22.15", - "dev": true, - "requires": { - "@babel/types": "^7.22.15" - } - }, - "@babel/helper-module-transforms": { - "version": "7.23.0", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.22.5", - "dev": true - }, - "@babel/helper-simple-access": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-string-parser": { - "version": "7.22.5", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.22.15", - "dev": true - }, - "@babel/helpers": { - "version": "7.23.2", - "dev": true, - "requires": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0" - } - }, - "@babel/highlight": { - "version": "7.22.20", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.23.0", - "dev": true - }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.22.5", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/runtime": { - "version": "7.22.15", - "requires": { - "regenerator-runtime": "^0.14.0" - } - }, - "@babel/template": { - "version": "7.22.15", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" - } - }, - "@babel/traverse": { - "version": "7.23.2", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.23.0", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - }, - "@emotion/hash": { - "version": "0.9.1" - }, - "@floating-ui/core": { - "version": "1.5.0", - "requires": { - "@floating-ui/utils": "^0.1.3" - } - }, - "@floating-ui/dom": { - "version": "1.5.3", - "requires": { - "@floating-ui/core": "^1.4.2", - "@floating-ui/utils": "^0.1.3" - } - }, - "@floating-ui/utils": { - "version": "0.1.6" - }, - "@fluentui/date-time-utilities": { - "version": "8.5.14", - "requires": { - "@fluentui/set-version": "^8.2.12", - "tslib": "^2.1.0" - } - }, - "@fluentui/dom-utilities": { - "version": "2.2.12", - "requires": { - "@fluentui/set-version": "^8.2.12", - "tslib": "^2.1.0" - } - }, - "@fluentui/font-icons-mdl2": { - "version": "8.5.26", - "requires": { - "@fluentui/set-version": "^8.2.12", - "@fluentui/style-utilities": "^8.9.19", - "@fluentui/utilities": "^8.13.20", - "tslib": "^2.1.0" - } - }, - "@fluentui/foundation-legacy": { - "version": "8.2.46", - "requires": { - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/set-version": "^8.2.12", - "@fluentui/style-utilities": "^8.9.19", - "@fluentui/utilities": "^8.13.20", - "tslib": "^2.1.0" - } - }, - "@fluentui/keyboard-key": { - "version": "0.4.12", - "requires": { - "tslib": "^2.1.0" - } - }, - "@fluentui/keyboard-keys": { - "version": "9.0.7", - "requires": { - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/merge-styles": { - "version": "8.5.13", - "requires": { - "@fluentui/set-version": "^8.2.12", - "tslib": "^2.1.0" - } - }, - "@fluentui/priority-overflow": { - "version": "9.1.10", - "requires": { - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react": { - "version": "8.112.5", - "requires": { - "@fluentui/date-time-utilities": "^8.5.14", - "@fluentui/font-icons-mdl2": "^8.5.26", - "@fluentui/foundation-legacy": "^8.2.46", - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/react-focus": "^8.8.33", - "@fluentui/react-hooks": "^8.6.32", - "@fluentui/react-portal-compat-context": "^9.0.9", - "@fluentui/react-window-provider": "^2.2.16", - "@fluentui/set-version": "^8.2.12", - "@fluentui/style-utilities": "^8.9.19", - "@fluentui/theme": "^2.6.37", - "@fluentui/utilities": "^8.13.20", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - } - }, - "@fluentui/react-alert": { - "version": "9.0.0-beta.90", - "requires": { - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-avatar": { - "version": "9.5.48", - "requires": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-popover": { - "version": "9.8.23", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-aria": { - "version": "9.4.0", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-badge": { - "version": "9.2.15", - "requires": { - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-button": { - "version": "9.3.53", - "requires": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-card": { - "version": "9.0.52", - "requires": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-checkbox": { - "version": "9.1.54", - "requires": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-components": { - "version": "9.37.3", - "requires": { - "@fluentui/react-accordion": "^9.3.26", - "@fluentui/react-alert": "9.0.0-beta.90", - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-badge": "^9.2.12", - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-card": "^9.0.52", - "@fluentui/react-checkbox": "^9.1.54", - "@fluentui/react-combobox": "^9.5.28", - "@fluentui/react-dialog": "^9.8.2", - "@fluentui/react-divider": "^9.2.48", - "@fluentui/react-drawer": "9.0.0-beta.40", - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-image": "^9.1.45", - "@fluentui/react-infobutton": "9.0.0-beta.74", - "@fluentui/react-infolabel": "^9.0.2", - "@fluentui/react-input": "^9.4.50", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-link": "^9.1.32", - "@fluentui/react-menu": "^9.12.30", - "@fluentui/react-message-bar": "^9.0.4", - "@fluentui/react-overflow": "^9.0.42", - "@fluentui/react-persona": "^9.2.54", - "@fluentui/react-popover": "^9.8.19", - "@fluentui/react-portal": "^9.3.27", - "@fluentui/react-positioning": "^9.9.23", - "@fluentui/react-progress": "^9.1.50", - "@fluentui/react-provider": "^9.11.1", - "@fluentui/react-radio": "^9.1.54", - "@fluentui/react-select": "^9.1.50", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-skeleton": "^9.0.38", - "@fluentui/react-slider": "^9.1.54", - "@fluentui/react-spinbutton": "^9.2.50", - "@fluentui/react-spinner": "^9.3.28", - "@fluentui/react-switch": "^9.1.54", - "@fluentui/react-table": "^9.10.9", - "@fluentui/react-tabs": "^9.3.55", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-tags": "^9.0.8", - "@fluentui/react-text": "^9.3.45", - "@fluentui/react-textarea": "^9.3.50", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-toast": "^9.3.15", - "@fluentui/react-toolbar": "^9.1.54", - "@fluentui/react-tooltip": "^9.3.20", - "@fluentui/react-tree": "^9.4.11", - "@fluentui/react-utilities": "^9.15.1", - "@fluentui/react-virtualizer": "9.0.0-alpha.55", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-accordion": { - "version": "9.3.30", - "requires": { - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-avatar": { - "version": "9.5.48", - "requires": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-combobox": { - "version": "9.5.32", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-field": "^9.1.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-menu": { - "version": "9.12.35", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-overflow": { - "version": "9.1.1", - "requires": { - "@fluentui/priority-overflow": "^9.1.10", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-popover": { - "version": "9.8.23", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-radio": { - "version": "9.1.58", - "requires": { - "@fluentui/react-field": "^9.1.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-label": "^9.1.51", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-tabs": { - "version": "9.3.59", - "requires": { - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - } - } - } - } - }, - "@fluentui/react-dialog": { - "version": "9.8.2", - "requires": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-context-selector": "^9.1.41", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-portal": "^9.3.27", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "react-transition-group": "^4.4.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-divider": { - "version": "9.2.48", - "requires": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-drawer": { - "version": "9.0.0-beta.40", - "requires": { - "@fluentui/react-dialog": "^9.8.2", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-motion-preview": "^0.5.0", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-field": { - "version": "9.1.43", - "requires": { - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-label": "^9.1.51", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-focus": { - "version": "8.8.33", - "requires": { - "@fluentui/keyboard-key": "^0.4.12", - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/set-version": "^8.2.12", - "@fluentui/style-utilities": "^8.9.19", - "@fluentui/utilities": "^8.13.20", - "tslib": "^2.1.0" - } - }, - "@fluentui/react-hooks": { - "version": "8.6.32", - "requires": { - "@fluentui/react-window-provider": "^2.2.16", - "@fluentui/set-version": "^8.2.12", - "@fluentui/utilities": "^8.13.20", - "tslib": "^2.1.0" - } - }, - "@fluentui/react-icons": { - "version": "2.0.221", - "requires": { - "@griffel/react": "^1.0.0", - "tslib": "^2.1.0" - } - }, - "@fluentui/react-image": { - "version": "9.1.45", - "requires": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-infobutton": { - "version": "9.0.0-beta.74", - "requires": { - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-popover": "^9.8.19", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-popover": { - "version": "9.8.23", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-infolabel": { - "version": "9.0.2", - "requires": { - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-popover": "^9.8.19", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-popover": { - "version": "9.8.23", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-input": { - "version": "9.4.50", - "requires": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-jsx-runtime": { - "version": "9.0.20", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1", - "react-is": "^17.0.2" - } - }, - "@fluentui/react-label": { - "version": "9.1.51", - "requires": { - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-link": { - "version": "9.1.32", - "requires": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-message-bar": { - "version": "9.0.4", - "requires": { - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "react-transition-group": "^4.4.1" - } - }, - "@fluentui/react-motion-preview": { - "version": "0.5.0", - "requires": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-persona": { - "version": "9.2.54", - "requires": { - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-badge": "^9.2.12", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-avatar": { - "version": "9.5.48", - "requires": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-popover": { - "version": "9.8.23", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-portal": { - "version": "9.4.3", - "requires": { - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "use-disposable": "^1.0.1" - } - }, - "@fluentui/react-portal-compat-context": { - "version": "9.0.9", - "requires": { - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-positioning": { - "version": "9.10.2", - "requires": { - "@floating-ui/dom": "^1.2.0", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "floating-ui-devtools": "0.1.2" - } - }, - "@fluentui/react-progress": { - "version": "9.1.50", - "requires": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-provider": { - "version": "9.11.1", - "requires": { - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/core": "^1.14.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-select": { - "version": "9.1.50", - "requires": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-shared-contexts": { - "version": "9.13.0", - "requires": { - "@fluentui/react-theme": "^9.1.16", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-skeleton": { - "version": "9.0.38", - "requires": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-slider": { - "version": "9.1.54", - "requires": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-spinbutton": { - "version": "9.2.50", - "requires": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-spinner": { - "version": "9.3.28", - "requires": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-switch": { - "version": "9.1.54", - "requires": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-label": "^9.1.48", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-table": { - "version": "9.10.9", - "requires": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-checkbox": "^9.1.54", - "@fluentui/react-context-selector": "^9.1.41", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-radio": "^9.1.54", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-avatar": { - "version": "9.5.48", - "requires": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-popover": { - "version": "9.8.23", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-radio": { - "version": "9.1.58", - "requires": { - "@fluentui/react-field": "^9.1.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-label": "^9.1.51", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-tabster": { - "version": "9.15.0", - "requires": { - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "keyborg": "^2.2.0", - "tabster": "^5.0.1" - } - }, - "@fluentui/react-tags": { - "version": "9.0.8", - "requires": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-avatar": { - "version": "9.5.48", - "requires": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-popover": { - "version": "9.8.23", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-text": { - "version": "9.3.45", - "requires": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-textarea": { - "version": "9.3.50", - "requires": { - "@fluentui/react-field": "^9.1.40", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-theme": { - "version": "9.1.16", - "requires": { - "@fluentui/tokens": "1.0.0-alpha.13", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-toast": { - "version": "9.3.15", - "requires": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-portal": "^9.3.27", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1", - "react-transition-group": "^4.4.1" - } - }, - "@fluentui/react-toolbar": { - "version": "9.1.54", - "requires": { - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-context-selector": "^9.1.41", - "@fluentui/react-divider": "^9.2.48", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-radio": "^9.1.54", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-radio": { - "version": "9.1.58", - "requires": { - "@fluentui/react-field": "^9.1.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-label": "^9.1.51", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-tooltip": { - "version": "9.4.1", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-tree": { - "version": "9.4.11", - "requires": { - "@fluentui/keyboard-keys": "^9.0.6", - "@fluentui/react-aria": "^9.3.43", - "@fluentui/react-avatar": "^9.5.44", - "@fluentui/react-button": "^9.3.53", - "@fluentui/react-checkbox": "^9.1.54", - "@fluentui/react-context-selector": "^9.1.41", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-radio": "^9.1.54", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-tabster": "^9.14.3", - "@fluentui/react-theme": "^9.1.15", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - }, - "dependencies": { - "@fluentui/react-avatar": { - "version": "9.5.48", - "requires": { - "@fluentui/react-badge": "^9.2.15", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-popover": "^9.8.23", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-tooltip": "^9.4.1", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-context-selector": { - "version": "9.1.42", - "requires": { - "@fluentui/react-utilities": "^9.15.2", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-popover": { - "version": "9.8.23", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@fluentui/react-aria": "^9.4.0", - "@fluentui/react-context-selector": "^9.1.42", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-portal": "^9.4.3", - "@fluentui/react-positioning": "^9.10.2", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-radio": { - "version": "9.1.58", - "requires": { - "@fluentui/react-field": "^9.1.43", - "@fluentui/react-icons": "^2.0.217", - "@fluentui/react-jsx-runtime": "^9.0.20", - "@fluentui/react-label": "^9.1.51", - "@fluentui/react-shared-contexts": "^9.13.0", - "@fluentui/react-tabster": "^9.15.0", - "@fluentui/react-theme": "^9.1.16", - "@fluentui/react-utilities": "^9.15.2", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - } - } - }, - "@fluentui/react-utilities": { - "version": "9.15.2", - "requires": { - "@fluentui/keyboard-keys": "^9.0.7", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-virtualizer": { - "version": "9.0.0-alpha.55", - "requires": { - "@fluentui/react-jsx-runtime": "^9.0.18", - "@fluentui/react-shared-contexts": "^9.11.1", - "@fluentui/react-utilities": "^9.15.1", - "@griffel/react": "^1.5.14", - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/react-window-provider": { - "version": "2.2.16", - "requires": { - "@fluentui/set-version": "^8.2.12", - "tslib": "^2.1.0" - } - }, - "@fluentui/set-version": { - "version": "8.2.12", - "requires": { - "tslib": "^2.1.0" - } - }, - "@fluentui/style-utilities": { - "version": "8.9.19", - "requires": { - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/set-version": "^8.2.12", - "@fluentui/theme": "^2.6.37", - "@fluentui/utilities": "^8.13.20", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - } - }, - "@fluentui/theme": { - "version": "2.6.37", - "requires": { - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/set-version": "^8.2.12", - "@fluentui/utilities": "^8.13.20", - "tslib": "^2.1.0" - } - }, - "@fluentui/tokens": { - "version": "1.0.0-alpha.13", - "requires": { - "@swc/helpers": "^0.5.1" - } - }, - "@fluentui/utilities": { - "version": "8.13.20", - "requires": { - "@fluentui/dom-utilities": "^2.2.12", - "@fluentui/merge-styles": "^8.5.13", - "@fluentui/set-version": "^8.2.12", - "tslib": "^2.1.0" - } - }, - "@griffel/core": { - "version": "1.14.4", - "requires": { - "@emotion/hash": "^0.9.0", - "@griffel/style-types": "^1.0.2", - "csstype": "^3.1.2", - "rtl-css-js": "^1.16.1", - "stylis": "^4.2.0", - "tslib": "^2.1.0" - } - }, - "@griffel/react": { - "version": "1.5.17", - "requires": { - "@griffel/core": "^1.14.4", - "tslib": "^2.1.0" - } - }, - "@griffel/style-types": { - "version": "1.0.2", - "requires": { - "csstype": "^3.1.2" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.18", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - }, - "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "dev": true - } - } - }, - "@microsoft/load-themed-styles": { - "version": "1.10.295" - }, - "@react-spring/animated": { - "version": "9.7.3", - "requires": { - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - } - }, - "@react-spring/core": { - "version": "9.7.3", - "requires": { - "@react-spring/animated": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - } - }, - "@react-spring/shared": { - "version": "9.7.3", - "requires": { - "@react-spring/types": "~9.7.3" - } - }, - "@react-spring/types": { - "version": "9.7.3" - }, - "@react-spring/web": { - "version": "9.7.3", - "requires": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - } - }, - "@remix-run/router": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", - "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==" - }, - "@rollup/plugin-typescript": { - "version": "11.1.3", - "requires": { - "@rollup/pluginutils": "^5.0.1", - "resolve": "^1.22.1" - } - }, - "@rollup/pluginutils": { - "version": "5.0.4", - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - } - }, - "@swc/helpers": { - "version": "0.5.3", - "requires": { - "tslib": "^2.4.0" - } - }, - "@types/babel__core": { - "version": "7.20.3", - "dev": true, - "requires": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.6.6", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.3", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.20.3", - "dev": true, - "requires": { - "@babel/types": "^7.20.7" - } - }, - "@types/dom-speech-recognition": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.4.tgz", - "integrity": "sha512-zf2GwV/G6TdaLwpLDcGTIkHnXf8JEf/viMux+khqKQKDa8/8BAUtXXZS563GnvJ4Fg0PBLGAaFf2GekEVSZ6GQ==", - "dev": true - }, - "@types/dompurify": { - "version": "3.0.4", - "dev": true, - "requires": { - "@types/trusted-types": "*" - } - }, - "@types/estree": { - "version": "1.0.1" - }, - "@types/hast": { - "version": "2.3.7", - "requires": { - "@types/unist": "^2" - } - }, - "@types/prop-types": { - "version": "15.7.5" - }, - "@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "requires": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "requires": { - "@types/react": "*" - } - }, - "@types/react-syntax-highlighter": { - "version": "15.5.13", - "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", - "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, - "@types/trusted-types": { - "version": "2.0.3", - "dev": true - }, - "@types/unist": { - "version": "2.0.7" - }, - "@vitejs/plugin-react": { - "version": "4.1.1", - "dev": true, - "requires": { - "@babel/core": "^7.23.2", - "@babel/plugin-transform-react-jsx-self": "^7.22.5", - "@babel/plugin-transform-react-jsx-source": "^7.22.5", - "@types/babel__core": "^7.20.3", - "react-refresh": "^0.14.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "browserslist": { - "version": "4.22.1", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" - } - }, - "caniuse-lite": { - "version": "1.0.30001547", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "character-entities": { - "version": "1.2.4" - }, - "character-entities-legacy": { - "version": "1.1.4" - }, - "character-reference-invalid": { - "version": "1.1.4" - }, - "color-convert": { - "version": "1.9.3", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "dev": true - }, - "comma-separated-tokens": { - "version": "1.0.8" - }, - "convert-source-map": { - "version": "2.0.0", - "dev": true - }, - "csstype": { - "version": "3.1.2" - }, - "debug": { - "version": "4.3.4", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "dom-helpers": { - "version": "5.2.1", - "requires": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "dompurify": { - "version": "3.0.6" - }, - "electron-to-chromium": { - "version": "1.4.549", - "dev": true - }, - "esbuild": { - "version": "0.18.11", - "dev": true, - "requires": { - "@esbuild/android-arm": "0.18.11", - "@esbuild/android-arm64": "0.18.11", - "@esbuild/android-x64": "0.18.11", - "@esbuild/darwin-arm64": "0.18.11", - "@esbuild/darwin-x64": "0.18.11", - "@esbuild/freebsd-arm64": "0.18.11", - "@esbuild/freebsd-x64": "0.18.11", - "@esbuild/linux-arm": "0.18.11", - "@esbuild/linux-arm64": "0.18.11", - "@esbuild/linux-ia32": "0.18.11", - "@esbuild/linux-loong64": "0.18.11", - "@esbuild/linux-mips64el": "0.18.11", - "@esbuild/linux-ppc64": "0.18.11", - "@esbuild/linux-riscv64": "0.18.11", - "@esbuild/linux-s390x": "0.18.11", - "@esbuild/linux-x64": "0.18.11", - "@esbuild/netbsd-x64": "0.18.11", - "@esbuild/openbsd-x64": "0.18.11", - "@esbuild/sunos-x64": "0.18.11", - "@esbuild/win32-arm64": "0.18.11", - "@esbuild/win32-ia32": "0.18.11", - "@esbuild/win32-x64": "0.18.11" - } - }, - "escalade": { - "version": "3.1.1", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "dev": true - }, - "estree-walker": { - "version": "2.0.2" - }, - "fault": { - "version": "1.0.4", - "requires": { - "format": "^0.2.0" - } - }, - "floating-ui-devtools": { - "version": "0.1.2", - "requires": {} - }, - "format": { - "version": "0.2.2" - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "optional": true - }, - "function-bind": { - "version": "1.1.1" - }, - "gensync": { - "version": "1.0.0-beta.2", - "dev": true - }, - "globals": { - "version": "11.12.0", - "dev": true - }, - "has": { - "version": "1.0.3", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "dev": true - }, - "hast-util-parse-selector": { - "version": "2.2.5" - }, - "hastscript": { - "version": "6.0.0", - "requires": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" - } - }, - "highlight.js": { - "version": "10.7.3" - }, - "is-alphabetical": { - "version": "1.0.4" - }, - "is-alphanumerical": { - "version": "1.0.4", - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - } - }, - "is-core-module": { - "version": "2.13.0", - "requires": { - "has": "^1.0.3" - } - }, - "is-decimal": { - "version": "1.0.4" - }, - "is-hexadecimal": { - "version": "1.0.4" - }, - "js-tokens": { - "version": "4.0.0" - }, - "jsesc": { - "version": "2.5.2", - "dev": true - }, - "json5": { - "version": "2.2.3", - "dev": true - }, - "keyborg": { - "version": "2.2.0" - }, - "loose-envify": { - "version": "1.4.0", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lowlight": { - "version": "1.20.0", - "requires": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "marked": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.0.tgz", - "integrity": "sha512-VTeDCd9txf4KLLljUZ0nljE/Incb9SrWuueE44QVuU0pkOdh4sfCeW1Z6lPcxyDRSVY6rm8db/0OPaN75RNUmw==" - }, - "ms": { - "version": "2.1.2", - "dev": true - }, - "nanoid": { - "version": "3.3.6", - "dev": true - }, - "ndjson-readablestream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ndjson-readablestream/-/ndjson-readablestream-1.2.0.tgz", - "integrity": "sha512-QbWX2IIfKMVL+ZFHm9vFEzPh1NzZfzJql59T+9XoXzUp8n0wu2t9qgDV9nT0A77YYa6KbAjsHNWzJfpZTfp4xQ==" - }, - "node-releases": { - "version": "2.0.13", - "dev": true - }, - "object-assign": { - "version": "4.1.1" - }, - "parse-entities": { - "version": "2.0.0", - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "path-parse": { - "version": "1.0.7" - }, - "picocolors": { - "version": "1.0.0", - "dev": true - }, - "picomatch": { - "version": "2.3.1" - }, - "postcss": { - "version": "8.4.31", - "dev": true, - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "prettier": { - "version": "3.0.3", - "dev": true - }, - "prismjs": { - "version": "1.29.0" - }, - "prop-types": { - "version": "15.8.1", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - }, - "dependencies": { - "react-is": { - "version": "16.13.1" - } - } - }, - "property-information": { - "version": "5.6.0", - "requires": { - "xtend": "^4.0.0" - } - }, - "react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "requires": { - "loose-envify": "^1.1.0" - } - }, - "react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "dependencies": { - "scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "requires": { - "loose-envify": "^1.1.0" - } - } - } - }, - "react-is": { - "version": "17.0.2" - }, - "react-refresh": { - "version": "0.14.0", - "dev": true - }, - "react-router": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", - "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", - "requires": { - "@remix-run/router": "1.16.1" - } - }, - "react-router-dom": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", - "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", - "requires": { - "@remix-run/router": "1.16.1", - "react-router": "6.23.1" - } - }, - "react-syntax-highlighter": { - "version": "15.5.0", - "requires": { - "@babel/runtime": "^7.3.1", - "highlight.js": "^10.4.1", - "lowlight": "^1.17.0", - "prismjs": "^1.27.0", - "refractor": "^3.6.0" - } - }, - "react-transition-group": { - "version": "4.4.5", - "requires": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - } - }, - "refractor": { - "version": "3.6.0", - "requires": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" - }, - "dependencies": { - "prismjs": { - "version": "1.27.0" - } - } - }, - "regenerator-runtime": { - "version": "0.14.0" - }, - "resolve": { - "version": "1.22.4", - "requires": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "rollup": { - "version": "3.29.4", - "requires": { - "fsevents": "~2.3.2" - } - }, - "rtl-css-js": { - "version": "1.16.1", - "requires": { - "@babel/runtime": "^7.1.2" - } - }, - "scheduler": { - "version": "0.20.2", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "semver": { - "version": "6.3.1", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "dev": true - }, - "space-separated-tokens": { - "version": "1.1.5" - }, - "stylis": { - "version": "4.3.0" - }, - "supports-color": { - "version": "5.5.0", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0" - }, - "tabster": { - "version": "5.1.0", - "requires": { - "keyborg": "^2.2.0", - "tslib": "^2.3.1" - } - }, - "to-fast-properties": { - "version": "2.0.0", - "dev": true - }, - "tslib": { - "version": "2.5.0" - }, - "typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==" - }, - "update-browserslist-db": { - "version": "1.0.13", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "use-disposable": { - "version": "1.0.2", - "requires": {} - }, - "vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", - "dev": true, - "requires": { - "esbuild": "^0.18.10", - "fsevents": "~2.3.2", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - } - }, - "xtend": { - "version": "4.0.2" - }, - "yallist": { - "version": "3.1.1", - "dev": true - } - } -} +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@azure/msal-browser": "^3.17.0", + "@azure/msal-react": "^2.0.6", + "@fluentui/react": "^8.112.5", + "@fluentui/react-components": "^9.37.3", + "@fluentui/react-icons": "^2.0.221", + "@react-spring/web": "^9.7.3", + "dompurify": "^3.0.6", + "marked": "^13.0.0", + "ndjson-readablestream": "^1.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.23.1", + "react-syntax-highlighter": "^15.5.0", + "scheduler": "^0.20.2" + }, + "devDependencies": { + "@types/dom-speech-recognition": "^0.0.4", + "@types/dompurify": "^3.0.4", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", + "@vitejs/plugin-react": "^4.1.1", + "prettier": "^3.0.3", + "typescript": "^5.4.5", + "vite": "^4.5.3" + }, + "engines": { + "node": ">=20.14.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.17.0.tgz", + "integrity": "sha512-csccKXmW2z7EkZ0I3yAoW/offQt+JECdTIV/KrnRoZyM7wCSsQWODpwod8ZhYy7iOyamcHApR9uCh0oD1M+0/A==", + "dependencies": { + "@azure/msal-common": "14.12.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-react": { + "version": "2.0.6", + "license": "MIT", + "dependencies": { + "@rollup/plugin-typescript": "^11.1.0", + "rollup": "^3.20.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@azure/msal-browser": "^3.4.0", + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.20", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.15", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.0", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.22.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.22.15", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "license": "MIT" + }, + "node_modules/@floating-ui/core": { + "version": "1.5.0", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "license": "MIT" + }, + "node_modules/@fluentui/date-time-utilities": { + "version": "8.5.14", + "license": "MIT", + "dependencies": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/dom-utilities": { + "version": "2.2.12", + "license": "MIT", + "dependencies": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/font-icons-mdl2": { + "version": "8.5.26", + "license": "MIT", + "dependencies": { + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/foundation-legacy": { + "version": "8.2.46", + "license": "MIT", + "dependencies": { + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/keyboard-key": { + "version": "0.4.12", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/keyboard-keys": { + "version": "9.0.7", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/merge-styles": { + "version": "8.5.13", + "license": "MIT", + "dependencies": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/priority-overflow": { + "version": "9.1.10", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/react": { + "version": "8.112.5", + "license": "MIT", + "dependencies": { + "@fluentui/date-time-utilities": "^8.5.14", + "@fluentui/font-icons-mdl2": "^8.5.26", + "@fluentui/foundation-legacy": "^8.2.46", + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/react-focus": "^8.8.33", + "@fluentui/react-hooks": "^8.6.32", + "@fluentui/react-portal-compat-context": "^9.0.9", + "@fluentui/react-window-provider": "^2.2.16", + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/theme": "^2.6.37", + "@fluentui/utilities": "^8.13.20", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-alert": { + "version": "9.0.0-beta.90", + "license": "MIT", + "dependencies": { + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-alert/node_modules/@fluentui/react-avatar": { + "version": "9.5.48", + "license": "MIT", + "dependencies": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-alert/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-alert/node_modules/@fluentui/react-popover": { + "version": "9.8.23", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-aria": { + "version": "9.4.0", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-badge": { + "version": "9.2.15", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-button": { + "version": "9.3.53", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-card": { + "version": "9.0.52", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-checkbox": { + "version": "9.1.54", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-components": { + "version": "9.37.3", + "license": "MIT", + "dependencies": { + "@fluentui/react-accordion": "^9.3.26", + "@fluentui/react-alert": "9.0.0-beta.90", + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-badge": "^9.2.12", + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-card": "^9.0.52", + "@fluentui/react-checkbox": "^9.1.54", + "@fluentui/react-combobox": "^9.5.28", + "@fluentui/react-dialog": "^9.8.2", + "@fluentui/react-divider": "^9.2.48", + "@fluentui/react-drawer": "9.0.0-beta.40", + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-image": "^9.1.45", + "@fluentui/react-infobutton": "9.0.0-beta.74", + "@fluentui/react-infolabel": "^9.0.2", + "@fluentui/react-input": "^9.4.50", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-link": "^9.1.32", + "@fluentui/react-menu": "^9.12.30", + "@fluentui/react-message-bar": "^9.0.4", + "@fluentui/react-overflow": "^9.0.42", + "@fluentui/react-persona": "^9.2.54", + "@fluentui/react-popover": "^9.8.19", + "@fluentui/react-portal": "^9.3.27", + "@fluentui/react-positioning": "^9.9.23", + "@fluentui/react-progress": "^9.1.50", + "@fluentui/react-provider": "^9.11.1", + "@fluentui/react-radio": "^9.1.54", + "@fluentui/react-select": "^9.1.50", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-skeleton": "^9.0.38", + "@fluentui/react-slider": "^9.1.54", + "@fluentui/react-spinbutton": "^9.2.50", + "@fluentui/react-spinner": "^9.3.28", + "@fluentui/react-switch": "^9.1.54", + "@fluentui/react-table": "^9.10.9", + "@fluentui/react-tabs": "^9.3.55", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-tags": "^9.0.8", + "@fluentui/react-text": "^9.3.45", + "@fluentui/react-textarea": "^9.3.50", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-toast": "^9.3.15", + "@fluentui/react-toolbar": "^9.1.54", + "@fluentui/react-tooltip": "^9.3.20", + "@fluentui/react-tree": "^9.4.11", + "@fluentui/react-utilities": "^9.15.1", + "@fluentui/react-virtualizer": "9.0.0-alpha.55", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-accordion": { + "version": "9.3.30", + "license": "MIT", + "dependencies": { + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-accordion/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-avatar": { + "version": "9.5.48", + "license": "MIT", + "dependencies": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-avatar/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-combobox": { + "version": "9.5.32", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-field": "^9.1.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-combobox/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-menu": { + "version": "9.12.35", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-menu/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-overflow": { + "version": "9.1.1", + "license": "MIT", + "dependencies": { + "@fluentui/priority-overflow": "^9.1.10", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-overflow/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-popover": { + "version": "9.8.23", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-popover/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-radio": { + "version": "9.1.58", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-label": "^9.1.51", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-tabs": { + "version": "9.3.59", + "license": "MIT", + "dependencies": { + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@fluentui/react-tabs/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-dialog": { + "version": "9.8.2", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-context-selector": "^9.1.41", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-portal": "^9.3.27", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-dialog/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-divider": { + "version": "9.2.48", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-drawer": { + "version": "9.0.0-beta.40", + "license": "MIT", + "dependencies": { + "@fluentui/react-dialog": "^9.8.2", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-motion-preview": "^0.5.0", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-field": { + "version": "9.1.43", + "license": "MIT", + "dependencies": { + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-label": "^9.1.51", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-field/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-focus": { + "version": "8.8.33", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-key": "^0.4.12", + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-hooks": { + "version": "8.6.32", + "license": "MIT", + "dependencies": { + "@fluentui/react-window-provider": "^2.2.16", + "@fluentui/set-version": "^8.2.12", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-icons": { + "version": "2.0.221", + "license": "MIT", + "dependencies": { + "@griffel/react": "^1.0.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-image": { + "version": "9.1.45", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-infobutton": { + "version": "9.0.0-beta.74", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-popover": "^9.8.19", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-infobutton/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-infobutton/node_modules/@fluentui/react-popover": { + "version": "9.8.23", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-infolabel": { + "version": "9.0.2", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-popover": "^9.8.19", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-infolabel/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-infolabel/node_modules/@fluentui/react-popover": { + "version": "9.8.23", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-input": { + "version": "9.4.50", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-jsx-runtime": { + "version": "9.0.20", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-label": { + "version": "9.1.51", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-link": { + "version": "9.1.32", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-message-bar": { + "version": "9.0.4", + "license": "MIT", + "dependencies": { + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-motion-preview": { + "version": "0.5.0", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-persona": { + "version": "9.2.54", + "license": "MIT", + "dependencies": { + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-badge": "^9.2.12", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-persona/node_modules/@fluentui/react-avatar": { + "version": "9.5.48", + "license": "MIT", + "dependencies": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-persona/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-persona/node_modules/@fluentui/react-popover": { + "version": "9.8.23", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-portal": { + "version": "9.4.3", + "license": "MIT", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "use-disposable": "^1.0.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-portal-compat-context": { + "version": "9.0.9", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-positioning": { + "version": "9.10.2", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.2.0", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "floating-ui-devtools": "0.1.2" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-progress": { + "version": "9.1.50", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-provider": { + "version": "9.11.1", + "license": "MIT", + "dependencies": { + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/core": "^1.14.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-select": { + "version": "9.1.50", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-shared-contexts": { + "version": "9.13.0", + "license": "MIT", + "dependencies": { + "@fluentui/react-theme": "^9.1.16", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-skeleton": { + "version": "9.0.38", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-slider": { + "version": "9.1.54", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-spinbutton": { + "version": "9.2.50", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-spinner": { + "version": "9.3.28", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-switch": { + "version": "9.1.54", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-table": { + "version": "9.10.9", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-checkbox": "^9.1.54", + "@fluentui/react-context-selector": "^9.1.41", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-radio": "^9.1.54", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-table/node_modules/@fluentui/react-avatar": { + "version": "9.5.48", + "license": "MIT", + "dependencies": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-table/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-table/node_modules/@fluentui/react-popover": { + "version": "9.8.23", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-table/node_modules/@fluentui/react-radio": { + "version": "9.1.58", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-label": "^9.1.51", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-tabster": { + "version": "9.15.0", + "license": "MIT", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "keyborg": "^2.2.0", + "tabster": "^5.0.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tags": { + "version": "9.0.8", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tags/node_modules/@fluentui/react-avatar": { + "version": "9.5.48", + "license": "MIT", + "dependencies": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-tags/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-tags/node_modules/@fluentui/react-popover": { + "version": "9.8.23", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-text": { + "version": "9.3.45", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-textarea": { + "version": "9.3.50", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-theme": { + "version": "9.1.16", + "license": "MIT", + "dependencies": { + "@fluentui/tokens": "1.0.0-alpha.13", + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/react-toast": { + "version": "9.3.15", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-portal": "^9.3.27", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-toolbar": { + "version": "9.1.54", + "license": "MIT", + "dependencies": { + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-context-selector": "^9.1.41", + "@fluentui/react-divider": "^9.2.48", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-radio": "^9.1.54", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-toolbar/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-toolbar/node_modules/@fluentui/react-radio": { + "version": "9.1.58", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-label": "^9.1.51", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-tooltip": { + "version": "9.4.1", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tree": { + "version": "9.4.11", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-checkbox": "^9.1.54", + "@fluentui/react-context-selector": "^9.1.41", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-radio": "^9.1.54", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tree/node_modules/@fluentui/react-avatar": { + "version": "9.5.48", + "license": "MIT", + "dependencies": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-tree/node_modules/@fluentui/react-context-selector": { + "version": "9.1.42", + "license": "MIT", + "dependencies": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-tree/node_modules/@fluentui/react-popover": { + "version": "9.8.23", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-tree/node_modules/@fluentui/react-radio": { + "version": "9.1.58", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.1.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-label": "^9.1.51", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-utilities": { + "version": "9.15.2", + "license": "MIT", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-virtualizer": { + "version": "9.0.0-alpha.55", + "license": "MIT", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-window-provider": { + "version": "2.2.16", + "license": "MIT", + "dependencies": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/set-version": { + "version": "8.2.12", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/style-utilities": { + "version": "8.9.19", + "license": "MIT", + "dependencies": { + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/theme": "^2.6.37", + "@fluentui/utilities": "^8.13.20", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/theme": { + "version": "2.6.37", + "license": "MIT", + "dependencies": { + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/tokens": { + "version": "1.0.0-alpha.13", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/utilities": { + "version": "8.13.20", + "license": "MIT", + "dependencies": { + "@fluentui/dom-utilities": "^2.2.12", + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/core": { + "version": "1.14.4", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@griffel/style-types": "^1.0.2", + "csstype": "^3.1.2", + "rtl-css-js": "^1.16.1", + "stylis": "^4.2.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@griffel/react": { + "version": "1.5.17", + "license": "MIT", + "dependencies": { + "@griffel/core": "^1.14.4", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/style-types": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.2" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "dev": true, + "license": "MIT" + }, + "node_modules/@microsoft/load-themed-styles": { + "version": "1.10.295", + "license": "MIT" + }, + "node_modules/@react-spring/animated": { + "version": "9.7.3", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.3", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/shared": { + "version": "9.7.3", + "license": "MIT", + "dependencies": { + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.3", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "9.7.3", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "11.1.3", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.0.4", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.3", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/dom-speech-recognition": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.4.tgz", + "integrity": "sha512-zf2GwV/G6TdaLwpLDcGTIkHnXf8JEf/viMux+khqKQKDa8/8BAUtXXZS563GnvJ4Fg0PBLGAaFf2GekEVSZ6GQ==", + "dev": true + }, + "node_modules/@types/dompurify": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "2.3.7", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "2.0.7", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.2", + "@babel/plugin-transform-react-jsx-self": "^7.22.5", + "@babel/plugin-transform-react-jsx-source": "^7.22.5", + "@types/babel__core": "^7.20.3", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/browserslist": { + "version": "4.22.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001547", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/character-entities": { + "version": "1.2.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.2", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "3.0.6", + "license": "(MPL-2.0 OR Apache-2.0)" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.549", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.18.11", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.11", + "@esbuild/android-arm64": "0.18.11", + "@esbuild/android-x64": "0.18.11", + "@esbuild/darwin-arm64": "0.18.11", + "@esbuild/darwin-x64": "0.18.11", + "@esbuild/freebsd-arm64": "0.18.11", + "@esbuild/freebsd-x64": "0.18.11", + "@esbuild/linux-arm": "0.18.11", + "@esbuild/linux-arm64": "0.18.11", + "@esbuild/linux-ia32": "0.18.11", + "@esbuild/linux-loong64": "0.18.11", + "@esbuild/linux-mips64el": "0.18.11", + "@esbuild/linux-ppc64": "0.18.11", + "@esbuild/linux-riscv64": "0.18.11", + "@esbuild/linux-s390x": "0.18.11", + "@esbuild/linux-x64": "0.18.11", + "@esbuild/netbsd-x64": "0.18.11", + "@esbuild/openbsd-x64": "0.18.11", + "@esbuild/sunos-x64": "0.18.11", + "@esbuild/win32-arm64": "0.18.11", + "@esbuild/win32-ia32": "0.18.11", + "@esbuild/win32-x64": "0.18.11" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "license": "MIT" + }, + "node_modules/fault": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/floating-ui-devtools": { + "version": "0.1.2", + "peerDependencies": { + "@floating-ui/dom": ">=1.0.0 <2.0.0" + } + }, + "node_modules/format": { + "version": "0.2.2", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-core-module": { + "version": "2.13.0", + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyborg": { + "version": "2.2.0", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "1.20.0", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/marked": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.0.tgz", + "integrity": "sha512-VTeDCd9txf4KLLljUZ0nljE/Incb9SrWuueE44QVuU0pkOdh4sfCeW1Z6lPcxyDRSVY6rm8db/0OPaN75RNUmw==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.6", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/ndjson-readablestream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ndjson-readablestream/-/ndjson-readablestream-1.2.0.tgz", + "integrity": "sha512-QbWX2IIfKMVL+ZFHm9vFEzPh1NzZfzJql59T+9XoXzUp8n0wu2t9qgDV9nT0A77YYa6KbAjsHNWzJfpZTfp4xQ==" + }, + "node_modules/node-releases": { + "version": "2.0.13", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "5.6.0", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dom/node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-syntax-highlighter": { + "version": "15.5.0", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/refractor": { + "version": "3.6.0", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.4", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/scheduler": { + "version": "0.20.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stylis": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tabster": { + "version": "5.1.0", + "license": "MIT", + "dependencies": { + "keyborg": "^2.2.0", + "tslib": "^2.3.1" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.5.0", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-disposable": { + "version": "1.0.2", + "license": "MIT", + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@azure/msal-browser": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.17.0.tgz", + "integrity": "sha512-csccKXmW2z7EkZ0I3yAoW/offQt+JECdTIV/KrnRoZyM7wCSsQWODpwod8ZhYy7iOyamcHApR9uCh0oD1M+0/A==", + "requires": { + "@azure/msal-common": "14.12.0" + } + }, + "@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==" + }, + "@azure/msal-react": { + "version": "2.0.6", + "requires": { + "@rollup/plugin-typescript": "^11.1.0", + "rollup": "^3.20.2" + } + }, + "@babel/code-frame": { + "version": "7.22.13", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + } + }, + "@babel/compat-data": { + "version": "7.22.20", + "dev": true + }, + "@babel/core": { + "version": "7.23.2", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.0", + "@babel/helpers": "^7.23.2", + "@babel/parser": "^7.23.0", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + } + }, + "@babel/generator": { + "version": "7.23.0", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.22.15", + "dev": true, + "requires": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-module-imports": { + "version": "7.22.15", + "dev": true, + "requires": { + "@babel/types": "^7.22.15" + } + }, + "@babel/helper-module-transforms": { + "version": "7.23.0", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.22.5", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.22.5", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.22.15", + "dev": true + }, + "@babel/helpers": { + "version": "7.23.2", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.2", + "@babel/types": "^7.23.0" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.23.0", + "dev": true + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.22.5", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.22.5", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.22.5" + } + }, + "@babel/runtime": { + "version": "7.22.15", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.22.15", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.2", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.23.0", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@emotion/hash": { + "version": "0.9.1" + }, + "@floating-ui/core": { + "version": "1.5.0", + "requires": { + "@floating-ui/utils": "^0.1.3" + } + }, + "@floating-ui/dom": { + "version": "1.5.3", + "requires": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "@floating-ui/utils": { + "version": "0.1.6" + }, + "@fluentui/date-time-utilities": { + "version": "8.5.14", + "requires": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "@fluentui/dom-utilities": { + "version": "2.2.12", + "requires": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "@fluentui/font-icons-mdl2": { + "version": "8.5.26", + "requires": { + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + } + }, + "@fluentui/foundation-legacy": { + "version": "8.2.46", + "requires": { + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + } + }, + "@fluentui/keyboard-key": { + "version": "0.4.12", + "requires": { + "tslib": "^2.1.0" + } + }, + "@fluentui/keyboard-keys": { + "version": "9.0.7", + "requires": { + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/merge-styles": { + "version": "8.5.13", + "requires": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "@fluentui/priority-overflow": { + "version": "9.1.10", + "requires": { + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react": { + "version": "8.112.5", + "requires": { + "@fluentui/date-time-utilities": "^8.5.14", + "@fluentui/font-icons-mdl2": "^8.5.26", + "@fluentui/foundation-legacy": "^8.2.46", + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/react-focus": "^8.8.33", + "@fluentui/react-hooks": "^8.6.32", + "@fluentui/react-portal-compat-context": "^9.0.9", + "@fluentui/react-window-provider": "^2.2.16", + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/theme": "^2.6.37", + "@fluentui/utilities": "^8.13.20", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + } + }, + "@fluentui/react-alert": { + "version": "9.0.0-beta.90", + "requires": { + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-avatar": { + "version": "9.5.48", + "requires": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-popover": { + "version": "9.8.23", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-aria": { + "version": "9.4.0", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-badge": { + "version": "9.2.15", + "requires": { + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-button": { + "version": "9.3.53", + "requires": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-card": { + "version": "9.0.52", + "requires": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-checkbox": { + "version": "9.1.54", + "requires": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-components": { + "version": "9.37.3", + "requires": { + "@fluentui/react-accordion": "^9.3.26", + "@fluentui/react-alert": "9.0.0-beta.90", + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-badge": "^9.2.12", + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-card": "^9.0.52", + "@fluentui/react-checkbox": "^9.1.54", + "@fluentui/react-combobox": "^9.5.28", + "@fluentui/react-dialog": "^9.8.2", + "@fluentui/react-divider": "^9.2.48", + "@fluentui/react-drawer": "9.0.0-beta.40", + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-image": "^9.1.45", + "@fluentui/react-infobutton": "9.0.0-beta.74", + "@fluentui/react-infolabel": "^9.0.2", + "@fluentui/react-input": "^9.4.50", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-link": "^9.1.32", + "@fluentui/react-menu": "^9.12.30", + "@fluentui/react-message-bar": "^9.0.4", + "@fluentui/react-overflow": "^9.0.42", + "@fluentui/react-persona": "^9.2.54", + "@fluentui/react-popover": "^9.8.19", + "@fluentui/react-portal": "^9.3.27", + "@fluentui/react-positioning": "^9.9.23", + "@fluentui/react-progress": "^9.1.50", + "@fluentui/react-provider": "^9.11.1", + "@fluentui/react-radio": "^9.1.54", + "@fluentui/react-select": "^9.1.50", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-skeleton": "^9.0.38", + "@fluentui/react-slider": "^9.1.54", + "@fluentui/react-spinbutton": "^9.2.50", + "@fluentui/react-spinner": "^9.3.28", + "@fluentui/react-switch": "^9.1.54", + "@fluentui/react-table": "^9.10.9", + "@fluentui/react-tabs": "^9.3.55", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-tags": "^9.0.8", + "@fluentui/react-text": "^9.3.45", + "@fluentui/react-textarea": "^9.3.50", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-toast": "^9.3.15", + "@fluentui/react-toolbar": "^9.1.54", + "@fluentui/react-tooltip": "^9.3.20", + "@fluentui/react-tree": "^9.4.11", + "@fluentui/react-utilities": "^9.15.1", + "@fluentui/react-virtualizer": "9.0.0-alpha.55", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-accordion": { + "version": "9.3.30", + "requires": { + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-avatar": { + "version": "9.5.48", + "requires": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-combobox": { + "version": "9.5.32", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-field": "^9.1.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-menu": { + "version": "9.12.35", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-overflow": { + "version": "9.1.1", + "requires": { + "@fluentui/priority-overflow": "^9.1.10", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-popover": { + "version": "9.8.23", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-radio": { + "version": "9.1.58", + "requires": { + "@fluentui/react-field": "^9.1.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-label": "^9.1.51", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-tabs": { + "version": "9.3.59", + "requires": { + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + } + } + } + } + }, + "@fluentui/react-dialog": { + "version": "9.8.2", + "requires": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-context-selector": "^9.1.41", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-portal": "^9.3.27", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-divider": { + "version": "9.2.48", + "requires": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-drawer": { + "version": "9.0.0-beta.40", + "requires": { + "@fluentui/react-dialog": "^9.8.2", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-motion-preview": "^0.5.0", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-field": { + "version": "9.1.43", + "requires": { + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-label": "^9.1.51", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-focus": { + "version": "8.8.33", + "requires": { + "@fluentui/keyboard-key": "^0.4.12", + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/style-utilities": "^8.9.19", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + } + }, + "@fluentui/react-hooks": { + "version": "8.6.32", + "requires": { + "@fluentui/react-window-provider": "^2.2.16", + "@fluentui/set-version": "^8.2.12", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + } + }, + "@fluentui/react-icons": { + "version": "2.0.221", + "requires": { + "@griffel/react": "^1.0.0", + "tslib": "^2.1.0" + } + }, + "@fluentui/react-image": { + "version": "9.1.45", + "requires": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-infobutton": { + "version": "9.0.0-beta.74", + "requires": { + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-popover": "^9.8.19", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-popover": { + "version": "9.8.23", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-infolabel": { + "version": "9.0.2", + "requires": { + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-popover": "^9.8.19", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-popover": { + "version": "9.8.23", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-input": { + "version": "9.4.50", + "requires": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-jsx-runtime": { + "version": "9.0.20", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1", + "react-is": "^17.0.2" + } + }, + "@fluentui/react-label": { + "version": "9.1.51", + "requires": { + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-link": { + "version": "9.1.32", + "requires": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-message-bar": { + "version": "9.0.4", + "requires": { + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" + } + }, + "@fluentui/react-motion-preview": { + "version": "0.5.0", + "requires": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-persona": { + "version": "9.2.54", + "requires": { + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-badge": "^9.2.12", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-avatar": { + "version": "9.5.48", + "requires": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-popover": { + "version": "9.8.23", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-portal": { + "version": "9.4.3", + "requires": { + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "use-disposable": "^1.0.1" + } + }, + "@fluentui/react-portal-compat-context": { + "version": "9.0.9", + "requires": { + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-positioning": { + "version": "9.10.2", + "requires": { + "@floating-ui/dom": "^1.2.0", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "floating-ui-devtools": "0.1.2" + } + }, + "@fluentui/react-progress": { + "version": "9.1.50", + "requires": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-provider": { + "version": "9.11.1", + "requires": { + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/core": "^1.14.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-select": { + "version": "9.1.50", + "requires": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-shared-contexts": { + "version": "9.13.0", + "requires": { + "@fluentui/react-theme": "^9.1.16", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-skeleton": { + "version": "9.0.38", + "requires": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-slider": { + "version": "9.1.54", + "requires": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-spinbutton": { + "version": "9.2.50", + "requires": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-spinner": { + "version": "9.3.28", + "requires": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-switch": { + "version": "9.1.54", + "requires": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-label": "^9.1.48", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-table": { + "version": "9.10.9", + "requires": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-checkbox": "^9.1.54", + "@fluentui/react-context-selector": "^9.1.41", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-radio": "^9.1.54", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-avatar": { + "version": "9.5.48", + "requires": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-popover": { + "version": "9.8.23", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-radio": { + "version": "9.1.58", + "requires": { + "@fluentui/react-field": "^9.1.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-label": "^9.1.51", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-tabster": { + "version": "9.15.0", + "requires": { + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "keyborg": "^2.2.0", + "tabster": "^5.0.1" + } + }, + "@fluentui/react-tags": { + "version": "9.0.8", + "requires": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-avatar": { + "version": "9.5.48", + "requires": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-popover": { + "version": "9.8.23", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-text": { + "version": "9.3.45", + "requires": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-textarea": { + "version": "9.3.50", + "requires": { + "@fluentui/react-field": "^9.1.40", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-theme": { + "version": "9.1.16", + "requires": { + "@fluentui/tokens": "1.0.0-alpha.13", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-toast": { + "version": "9.3.15", + "requires": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-portal": "^9.3.27", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" + } + }, + "@fluentui/react-toolbar": { + "version": "9.1.54", + "requires": { + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-context-selector": "^9.1.41", + "@fluentui/react-divider": "^9.2.48", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-radio": "^9.1.54", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-radio": { + "version": "9.1.58", + "requires": { + "@fluentui/react-field": "^9.1.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-label": "^9.1.51", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-tooltip": { + "version": "9.4.1", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-tree": { + "version": "9.4.11", + "requires": { + "@fluentui/keyboard-keys": "^9.0.6", + "@fluentui/react-aria": "^9.3.43", + "@fluentui/react-avatar": "^9.5.44", + "@fluentui/react-button": "^9.3.53", + "@fluentui/react-checkbox": "^9.1.54", + "@fluentui/react-context-selector": "^9.1.41", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-radio": "^9.1.54", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-tabster": "^9.14.3", + "@fluentui/react-theme": "^9.1.15", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "dependencies": { + "@fluentui/react-avatar": { + "version": "9.5.48", + "requires": { + "@fluentui/react-badge": "^9.2.15", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-popover": "^9.8.23", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.1", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-context-selector": { + "version": "9.1.42", + "requires": { + "@fluentui/react-utilities": "^9.15.2", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-popover": { + "version": "9.8.23", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.4.0", + "@fluentui/react-context-selector": "^9.1.42", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-portal": "^9.4.3", + "@fluentui/react-positioning": "^9.10.2", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-radio": { + "version": "9.1.58", + "requires": { + "@fluentui/react-field": "^9.1.43", + "@fluentui/react-icons": "^2.0.217", + "@fluentui/react-jsx-runtime": "^9.0.20", + "@fluentui/react-label": "^9.1.51", + "@fluentui/react-shared-contexts": "^9.13.0", + "@fluentui/react-tabster": "^9.15.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.2", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + } + } + }, + "@fluentui/react-utilities": { + "version": "9.15.2", + "requires": { + "@fluentui/keyboard-keys": "^9.0.7", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-virtualizer": { + "version": "9.0.0-alpha.55", + "requires": { + "@fluentui/react-jsx-runtime": "^9.0.18", + "@fluentui/react-shared-contexts": "^9.11.1", + "@fluentui/react-utilities": "^9.15.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/react-window-provider": { + "version": "2.2.16", + "requires": { + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "@fluentui/set-version": { + "version": "8.2.12", + "requires": { + "tslib": "^2.1.0" + } + }, + "@fluentui/style-utilities": { + "version": "8.9.19", + "requires": { + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/theme": "^2.6.37", + "@fluentui/utilities": "^8.13.20", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + } + }, + "@fluentui/theme": { + "version": "2.6.37", + "requires": { + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "@fluentui/utilities": "^8.13.20", + "tslib": "^2.1.0" + } + }, + "@fluentui/tokens": { + "version": "1.0.0-alpha.13", + "requires": { + "@swc/helpers": "^0.5.1" + } + }, + "@fluentui/utilities": { + "version": "8.13.20", + "requires": { + "@fluentui/dom-utilities": "^2.2.12", + "@fluentui/merge-styles": "^8.5.13", + "@fluentui/set-version": "^8.2.12", + "tslib": "^2.1.0" + } + }, + "@griffel/core": { + "version": "1.14.4", + "requires": { + "@emotion/hash": "^0.9.0", + "@griffel/style-types": "^1.0.2", + "csstype": "^3.1.2", + "rtl-css-js": "^1.16.1", + "stylis": "^4.2.0", + "tslib": "^2.1.0" + } + }, + "@griffel/react": { + "version": "1.5.17", + "requires": { + "@griffel/core": "^1.14.4", + "tslib": "^2.1.0" + } + }, + "@griffel/style-types": { + "version": "1.0.2", + "requires": { + "csstype": "^3.1.2" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "dev": true + } + } + }, + "@microsoft/load-themed-styles": { + "version": "1.10.295" + }, + "@react-spring/animated": { + "version": "9.7.3", + "requires": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + } + }, + "@react-spring/core": { + "version": "9.7.3", + "requires": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + } + }, + "@react-spring/shared": { + "version": "9.7.3", + "requires": { + "@react-spring/types": "~9.7.3" + } + }, + "@react-spring/types": { + "version": "9.7.3" + }, + "@react-spring/web": { + "version": "9.7.3", + "requires": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + } + }, + "@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==" + }, + "@rollup/plugin-typescript": { + "version": "11.1.3", + "requires": { + "@rollup/pluginutils": "^5.0.1", + "resolve": "^1.22.1" + } + }, + "@rollup/pluginutils": { + "version": "5.0.4", + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + } + }, + "@swc/helpers": { + "version": "0.5.3", + "requires": { + "tslib": "^2.4.0" + } + }, + "@types/babel__core": { + "version": "7.20.3", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.6", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.3", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.3", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" + } + }, + "@types/dom-speech-recognition": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.4.tgz", + "integrity": "sha512-zf2GwV/G6TdaLwpLDcGTIkHnXf8JEf/viMux+khqKQKDa8/8BAUtXXZS563GnvJ4Fg0PBLGAaFf2GekEVSZ6GQ==", + "dev": true + }, + "@types/dompurify": { + "version": "3.0.4", + "dev": true, + "requires": { + "@types/trusted-types": "*" + } + }, + "@types/estree": { + "version": "1.0.1" + }, + "@types/hast": { + "version": "2.3.7", + "requires": { + "@types/unist": "^2" + } + }, + "@types/prop-types": { + "version": "15.7.5" + }, + "@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/trusted-types": { + "version": "2.0.3", + "dev": true + }, + "@types/unist": { + "version": "2.0.7" + }, + "@vitejs/plugin-react": { + "version": "4.1.1", + "dev": true, + "requires": { + "@babel/core": "^7.23.2", + "@babel/plugin-transform-react-jsx-self": "^7.22.5", + "@babel/plugin-transform-react-jsx-source": "^7.22.5", + "@types/babel__core": "^7.20.3", + "react-refresh": "^0.14.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "browserslist": { + "version": "4.22.1", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + } + }, + "caniuse-lite": { + "version": "1.0.30001547", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "character-entities": { + "version": "1.2.4" + }, + "character-entities-legacy": { + "version": "1.1.4" + }, + "character-reference-invalid": { + "version": "1.1.4" + }, + "color-convert": { + "version": "1.9.3", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "dev": true + }, + "comma-separated-tokens": { + "version": "1.0.8" + }, + "convert-source-map": { + "version": "2.0.0", + "dev": true + }, + "csstype": { + "version": "3.1.2" + }, + "debug": { + "version": "4.3.4", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "dom-helpers": { + "version": "5.2.1", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "dompurify": { + "version": "3.0.6" + }, + "electron-to-chromium": { + "version": "1.4.549", + "dev": true + }, + "esbuild": { + "version": "0.18.11", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.18.11", + "@esbuild/android-arm64": "0.18.11", + "@esbuild/android-x64": "0.18.11", + "@esbuild/darwin-arm64": "0.18.11", + "@esbuild/darwin-x64": "0.18.11", + "@esbuild/freebsd-arm64": "0.18.11", + "@esbuild/freebsd-x64": "0.18.11", + "@esbuild/linux-arm": "0.18.11", + "@esbuild/linux-arm64": "0.18.11", + "@esbuild/linux-ia32": "0.18.11", + "@esbuild/linux-loong64": "0.18.11", + "@esbuild/linux-mips64el": "0.18.11", + "@esbuild/linux-ppc64": "0.18.11", + "@esbuild/linux-riscv64": "0.18.11", + "@esbuild/linux-s390x": "0.18.11", + "@esbuild/linux-x64": "0.18.11", + "@esbuild/netbsd-x64": "0.18.11", + "@esbuild/openbsd-x64": "0.18.11", + "@esbuild/sunos-x64": "0.18.11", + "@esbuild/win32-arm64": "0.18.11", + "@esbuild/win32-ia32": "0.18.11", + "@esbuild/win32-x64": "0.18.11" + } + }, + "escalade": { + "version": "3.1.1", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "dev": true + }, + "estree-walker": { + "version": "2.0.2" + }, + "fault": { + "version": "1.0.4", + "requires": { + "format": "^0.2.0" + } + }, + "floating-ui-devtools": { + "version": "0.1.2", + "requires": {} + }, + "format": { + "version": "0.2.2" + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, + "function-bind": { + "version": "1.1.1" + }, + "gensync": { + "version": "1.0.0-beta.2", + "dev": true + }, + "globals": { + "version": "11.12.0", + "dev": true + }, + "has": { + "version": "1.0.3", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "dev": true + }, + "hast-util-parse-selector": { + "version": "2.2.5" + }, + "hastscript": { + "version": "6.0.0", + "requires": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + } + }, + "highlight.js": { + "version": "10.7.3" + }, + "is-alphabetical": { + "version": "1.0.4" + }, + "is-alphanumerical": { + "version": "1.0.4", + "requires": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + } + }, + "is-core-module": { + "version": "2.13.0", + "requires": { + "has": "^1.0.3" + } + }, + "is-decimal": { + "version": "1.0.4" + }, + "is-hexadecimal": { + "version": "1.0.4" + }, + "js-tokens": { + "version": "4.0.0" + }, + "jsesc": { + "version": "2.5.2", + "dev": true + }, + "json5": { + "version": "2.2.3", + "dev": true + }, + "keyborg": { + "version": "2.2.0" + }, + "loose-envify": { + "version": "1.4.0", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lowlight": { + "version": "1.20.0", + "requires": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "marked": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.0.tgz", + "integrity": "sha512-VTeDCd9txf4KLLljUZ0nljE/Incb9SrWuueE44QVuU0pkOdh4sfCeW1Z6lPcxyDRSVY6rm8db/0OPaN75RNUmw==" + }, + "ms": { + "version": "2.1.2", + "dev": true + }, + "nanoid": { + "version": "3.3.6", + "dev": true + }, + "ndjson-readablestream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ndjson-readablestream/-/ndjson-readablestream-1.2.0.tgz", + "integrity": "sha512-QbWX2IIfKMVL+ZFHm9vFEzPh1NzZfzJql59T+9XoXzUp8n0wu2t9qgDV9nT0A77YYa6KbAjsHNWzJfpZTfp4xQ==" + }, + "node-releases": { + "version": "2.0.13", + "dev": true + }, + "object-assign": { + "version": "4.1.1" + }, + "parse-entities": { + "version": "2.0.0", + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "path-parse": { + "version": "1.0.7" + }, + "picocolors": { + "version": "1.0.0", + "dev": true + }, + "picomatch": { + "version": "2.3.1" + }, + "postcss": { + "version": "8.4.31", + "dev": true, + "requires": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "prettier": { + "version": "3.0.3", + "dev": true + }, + "prismjs": { + "version": "1.29.0" + }, + "prop-types": { + "version": "15.8.1", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1" + } + } + }, + "property-information": { + "version": "5.6.0", + "requires": { + "xtend": "^4.0.0" + } + }, + "react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "dependencies": { + "scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "requires": { + "loose-envify": "^1.1.0" + } + } + } + }, + "react-is": { + "version": "17.0.2" + }, + "react-refresh": { + "version": "0.14.0", + "dev": true + }, + "react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "requires": { + "@remix-run/router": "1.16.1" + } + }, + "react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "requires": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + } + }, + "react-syntax-highlighter": { + "version": "15.5.0", + "requires": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + } + }, + "react-transition-group": { + "version": "4.4.5", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "refractor": { + "version": "3.6.0", + "requires": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "dependencies": { + "prismjs": { + "version": "1.27.0" + } + } + }, + "regenerator-runtime": { + "version": "0.14.0" + }, + "resolve": { + "version": "1.22.4", + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rollup": { + "version": "3.29.4", + "requires": { + "fsevents": "~2.3.2" + } + }, + "rtl-css-js": { + "version": "1.16.1", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, + "scheduler": { + "version": "0.20.2", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "semver": { + "version": "6.3.1", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "dev": true + }, + "space-separated-tokens": { + "version": "1.1.5" + }, + "stylis": { + "version": "4.3.0" + }, + "supports-color": { + "version": "5.5.0", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0" + }, + "tabster": { + "version": "5.1.0", + "requires": { + "keyborg": "^2.2.0", + "tslib": "^2.3.1" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "dev": true + }, + "tslib": { + "version": "2.5.0" + }, + "typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==" + }, + "update-browserslist-db": { + "version": "1.0.13", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "use-disposable": { + "version": "1.0.2", + "requires": {} + }, + "vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "dev": true, + "requires": { + "esbuild": "^0.18.10", + "fsevents": "~2.3.2", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + } + }, + "xtend": { + "version": "4.0.2" + }, + "yallist": { + "version": "3.1.1", + "dev": true + } + } +} diff --git a/app/frontend/package.json b/app/frontend/package.json index 489ad18a06..aa3bbe4e11 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -1,41 +1,41 @@ -{ - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "engines": { - "node": ">=14.0.0" - }, - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview" - }, - "dependencies": { - "@azure/msal-react": "^2.0.6", - "@azure/msal-browser": "^3.17.0", - "@fluentui/react": "^8.112.5", - "@fluentui/react-components": "^9.37.3", - "@fluentui/react-icons": "^2.0.221", - "@react-spring/web": "^9.7.3", - "marked": "^13.0.0", - "dompurify": "^3.0.6", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.23.1", - "ndjson-readablestream": "^1.2.0", - "react-syntax-highlighter": "^15.5.0", - "scheduler": "^0.20.2" - }, - "devDependencies": { - "@types/dompurify": "^3.0.4", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.1.1", - "prettier": "^3.0.3", - "typescript": "^5.4.5", - "@types/react-syntax-highlighter": "^15.5.13", - "@types/dom-speech-recognition": "^0.0.4", - "vite": "^4.5.3" - } -} +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=20.14.0" + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@azure/msal-react": "^2.0.6", + "@azure/msal-browser": "^3.17.0", + "@fluentui/react": "^8.112.5", + "@fluentui/react-components": "^9.37.3", + "@fluentui/react-icons": "^2.0.221", + "@react-spring/web": "^9.7.3", + "marked": "^13.0.0", + "dompurify": "^3.0.6", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.23.1", + "ndjson-readablestream": "^1.2.0", + "react-syntax-highlighter": "^15.5.0", + "scheduler": "^0.20.2" + }, + "devDependencies": { + "@types/dompurify": "^3.0.4", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.1.1", + "prettier": "^3.0.3", + "typescript": "^5.4.5", + "@types/react-syntax-highlighter": "^15.5.13", + "@types/dom-speech-recognition": "^0.0.4", + "vite": "^4.5.3" + } +} diff --git a/app/frontend/src/api/api.ts b/app/frontend/src/api/api.ts index 021418cf0f..58868952c6 100644 --- a/app/frontend/src/api/api.ts +++ b/app/frontend/src/api/api.ts @@ -1,122 +1,122 @@ -const BACKEND_URI = ""; - -import { ChatAppResponse, ChatAppResponseOrError, ChatAppRequest, Config, SimpleAPIResponse } from "./models"; -import { useLogin, appServicesToken } from "../authConfig"; - -export function getHeaders(idToken: string | undefined): Record { - // If using login and not using app services, add the id token of the logged in account as the authorization - if (useLogin && appServicesToken == null) { - if (idToken) { - return { Authorization: `Bearer ${idToken}` }; - } - } - - return {}; -} - -export async function configApi(): Promise { - const response = await fetch(`${BACKEND_URI}/config`, { - method: "GET" - }); - - return (await response.json()) as Config; -} - -export async function askApi(request: ChatAppRequest, idToken: string | undefined): Promise { - const response = await fetch(`${BACKEND_URI}/ask`, { - method: "POST", - headers: { ...getHeaders(idToken), "Content-Type": "application/json" }, - body: JSON.stringify(request) - }); - - const parsedResponse: ChatAppResponseOrError = await response.json(); - if (response.status > 299 || !response.ok) { - throw Error(parsedResponse.error || "Unknown error"); - } - - return parsedResponse as ChatAppResponse; -} - -export async function chatApi(request: ChatAppRequest, shouldStream: boolean, idToken: string | undefined): Promise { - let url = `${BACKEND_URI}/chat`; - if (shouldStream) { - url += "/stream"; - } - return await fetch(url, { - method: "POST", - headers: { ...getHeaders(idToken), "Content-Type": "application/json" }, - body: JSON.stringify(request) - }); -} - -export async function getSpeechApi(text: string): Promise { - return await fetch("/speech", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - text: text - }) - }) - .then(response => { - if (response.status == 200) { - return response.blob(); - } else if (response.status == 400) { - console.log("Speech synthesis is not enabled."); - return null; - } else { - console.error("Unable to get speech synthesis."); - return null; - } - }) - .then(blob => (blob ? URL.createObjectURL(blob) : null)); -} - -export function getCitationFilePath(citation: string): string { - return `${BACKEND_URI}/content/${citation}`; -} - -export async function uploadFileApi(request: FormData, idToken: string): Promise { - const response = await fetch("/upload", { - method: "POST", - headers: getHeaders(idToken), - body: request - }); - - if (!response.ok) { - throw new Error(`Uploading files failed: ${response.statusText}`); - } - - const dataResponse: SimpleAPIResponse = await response.json(); - return dataResponse; -} - -export async function deleteUploadedFileApi(filename: string, idToken: string): Promise { - const response = await fetch("/delete_uploaded", { - method: "POST", - headers: { ...getHeaders(idToken), "Content-Type": "application/json" }, - body: JSON.stringify({ filename }) - }); - - if (!response.ok) { - throw new Error(`Deleting file failed: ${response.statusText}`); - } - - const dataResponse: SimpleAPIResponse = await response.json(); - return dataResponse; -} - -export async function listUploadedFilesApi(idToken: string): Promise { - const response = await fetch(`/list_uploaded`, { - method: "GET", - headers: getHeaders(idToken) - }); - - if (!response.ok) { - throw new Error(`Listing files failed: ${response.statusText}`); - } - - const dataResponse: string[] = await response.json(); - return dataResponse; -} +const BACKEND_URI = ""; + +import { ChatAppResponse, ChatAppResponseOrError, ChatAppRequest, Config, SimpleAPIResponse } from "./models"; +import { useLogin, appServicesToken } from "../authConfig"; + +export function getHeaders(idToken: string | undefined): Record { + // If using login and not using app services, add the id token of the logged in account as the authorization + if (useLogin && appServicesToken == null) { + if (idToken) { + return { Authorization: `Bearer ${idToken}` }; + } + } + + return {}; +} + +export async function configApi(): Promise { + const response = await fetch(`${BACKEND_URI}/config`, { + method: "GET" + }); + + return (await response.json()) as Config; +} + +export async function askApi(request: ChatAppRequest, idToken: string | undefined): Promise { + const response = await fetch(`${BACKEND_URI}/ask`, { + method: "POST", + headers: { ...getHeaders(idToken), "Content-Type": "application/json" }, + body: JSON.stringify(request) + }); + + const parsedResponse: ChatAppResponseOrError = await response.json(); + if (response.status > 299 || !response.ok) { + throw Error(parsedResponse.error || "Unknown error"); + } + + return parsedResponse as ChatAppResponse; +} + +export async function chatApi(request: ChatAppRequest, shouldStream: boolean, idToken: string | undefined): Promise { + let url = `${BACKEND_URI}/chat`; + if (shouldStream) { + url += "/stream"; + } + return await fetch(url, { + method: "POST", + headers: { ...getHeaders(idToken), "Content-Type": "application/json" }, + body: JSON.stringify(request) + }); +} + +export async function getSpeechApi(text: string): Promise { + return await fetch("/speech", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + text: text + }) + }) + .then(response => { + if (response.status == 200) { + return response.blob(); + } else if (response.status == 400) { + console.log("Speech synthesis is not enabled."); + return null; + } else { + console.error("Unable to get speech synthesis."); + return null; + } + }) + .then(blob => (blob ? URL.createObjectURL(blob) : null)); +} + +export function getCitationFilePath(citation: string): string { + return `${BACKEND_URI}/content/${citation}`; +} + +export async function uploadFileApi(request: FormData, idToken: string): Promise { + const response = await fetch("/upload", { + method: "POST", + headers: getHeaders(idToken), + body: request + }); + + if (!response.ok) { + throw new Error(`Uploading files failed: ${response.statusText}`); + } + + const dataResponse: SimpleAPIResponse = await response.json(); + return dataResponse; +} + +export async function deleteUploadedFileApi(filename: string, idToken: string): Promise { + const response = await fetch("/delete_uploaded", { + method: "POST", + headers: { ...getHeaders(idToken), "Content-Type": "application/json" }, + body: JSON.stringify({ filename }) + }); + + if (!response.ok) { + throw new Error(`Deleting file failed: ${response.statusText}`); + } + + const dataResponse: SimpleAPIResponse = await response.json(); + return dataResponse; +} + +export async function listUploadedFilesApi(idToken: string): Promise { + const response = await fetch(`/list_uploaded`, { + method: "GET", + headers: getHeaders(idToken) + }); + + if (!response.ok) { + throw new Error(`Listing files failed: ${response.statusText}`); + } + + const dataResponse: string[] = await response.json(); + return dataResponse; +} diff --git a/app/frontend/src/api/index.ts b/app/frontend/src/api/index.ts index 0475d357ad..1829aefa3e 100644 --- a/app/frontend/src/api/index.ts +++ b/app/frontend/src/api/index.ts @@ -1,2 +1,2 @@ -export * from "./api"; -export * from "./models"; +export * from "./api"; +export * from "./models"; diff --git a/app/frontend/src/api/models.ts b/app/frontend/src/api/models.ts index e66b070437..78e49f0d04 100644 --- a/app/frontend/src/api/models.ts +++ b/app/frontend/src/api/models.ts @@ -1,93 +1,93 @@ -export const enum RetrievalMode { - Hybrid = "hybrid", - Vectors = "vectors", - Text = "text" -} - -export const enum GPT4VInput { - TextAndImages = "textAndImages", - Images = "images", - Texts = "texts" -} - -export const enum VectorFieldOptions { - Embedding = "embedding", - ImageEmbedding = "imageEmbedding", - Both = "both" -} - -export type ChatAppRequestOverrides = { - retrieval_mode?: RetrievalMode; - semantic_ranker?: boolean; - semantic_captions?: boolean; - exclude_category?: string; - top?: number; - temperature?: number; - minimum_search_score?: number; - minimum_reranker_score?: number; - prompt_template?: string; - prompt_template_prefix?: string; - prompt_template_suffix?: string; - suggest_followup_questions?: boolean; - use_oid_security_filter?: boolean; - use_groups_security_filter?: boolean; - use_gpt4v?: boolean; - gpt4v_input?: GPT4VInput; - vector_fields: VectorFieldOptions[]; -}; - -export type ResponseMessage = { - content: string; - role: string; -}; - -export type Thoughts = { - title: string; - description: any; // It can be any output from the api - props?: { [key: string]: string }; -}; - -export type ResponseContext = { - data_points: string[]; - followup_questions: string[] | null; - thoughts: Thoughts[]; -}; - -export type ChatAppResponseOrError = { - message: ResponseMessage; - delta: ResponseMessage; - context: ResponseContext; - session_state: any; - error?: string; -}; - -export type ChatAppResponse = { - message: ResponseMessage; - delta: ResponseMessage; - context: ResponseContext; - session_state: any; -}; - -export type ChatAppRequestContext = { - overrides?: ChatAppRequestOverrides; -}; - -export type ChatAppRequest = { - messages: ResponseMessage[]; - context?: ChatAppRequestContext; - session_state: any; -}; - -export type Config = { - showGPT4VOptions: boolean; - showSemanticRankerOption: boolean; - showVectorOption: boolean; - showUserUpload: boolean; - showSpeechInput: boolean; - showSpeechOutputBrowser: boolean; - showSpeechOutputAzure: boolean; -}; - -export type SimpleAPIResponse = { - message?: string; -}; +export const enum RetrievalMode { + Hybrid = "hybrid", + Vectors = "vectors", + Text = "text" +} + +export const enum GPT4VInput { + TextAndImages = "textAndImages", + Images = "images", + Texts = "texts" +} + +export const enum VectorFieldOptions { + Embedding = "embedding", + ImageEmbedding = "imageEmbedding", + Both = "both" +} + +export type ChatAppRequestOverrides = { + retrieval_mode?: RetrievalMode; + semantic_ranker?: boolean; + semantic_captions?: boolean; + exclude_category?: string; + top?: number; + temperature?: number; + minimum_search_score?: number; + minimum_reranker_score?: number; + prompt_template?: string; + prompt_template_prefix?: string; + prompt_template_suffix?: string; + suggest_followup_questions?: boolean; + use_oid_security_filter?: boolean; + use_groups_security_filter?: boolean; + use_gpt4v?: boolean; + gpt4v_input?: GPT4VInput; + vector_fields: VectorFieldOptions[]; +}; + +export type ResponseMessage = { + content: string; + role: string; +}; + +export type Thoughts = { + title: string; + description: any; // It can be any output from the api + props?: { [key: string]: string }; +}; + +export type ResponseContext = { + data_points: string[]; + followup_questions: string[] | null; + thoughts: Thoughts[]; +}; + +export type ChatAppResponseOrError = { + message: ResponseMessage; + delta: ResponseMessage; + context: ResponseContext; + session_state: any; + error?: string; +}; + +export type ChatAppResponse = { + message: ResponseMessage; + delta: ResponseMessage; + context: ResponseContext; + session_state: any; +}; + +export type ChatAppRequestContext = { + overrides?: ChatAppRequestOverrides; +}; + +export type ChatAppRequest = { + messages: ResponseMessage[]; + context?: ChatAppRequestContext; + session_state: any; +}; + +export type Config = { + showGPT4VOptions: boolean; + showSemanticRankerOption: boolean; + showVectorOption: boolean; + showUserUpload: boolean; + showSpeechInput: boolean; + showSpeechOutputBrowser: boolean; + showSpeechOutputAzure: boolean; +}; + +export type SimpleAPIResponse = { + message?: string; +}; diff --git a/app/frontend/src/authConfig.ts b/app/frontend/src/authConfig.ts index 1ea1b6c4e1..95286ae10a 100644 --- a/app/frontend/src/authConfig.ts +++ b/app/frontend/src/authConfig.ts @@ -1,158 +1,158 @@ -// Refactored from https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/blob/main/1-Authentication/1-sign-in/SPA/src/authConfig.js - -import { IPublicClientApplication } from "@azure/msal-browser"; - -const appServicesAuthTokenUrl = ".auth/me"; -const appServicesAuthTokenRefreshUrl = ".auth/refresh"; -const appServicesAuthLogoutUrl = ".auth/logout?post_logout_redirect_uri=/"; - -interface AppServicesToken { - id_token: string; - access_token: string; - user_claims: Record; -} - -interface AuthSetup { - // Set to true if login elements should be shown in the UI - useLogin: boolean; - // Set to true if access control is enforced by the application - requireAccessControl: boolean; - // Set to true if the application allows unauthenticated access (only applies for documents without access control) - enableUnauthenticatedAccess: boolean; - /** - * Configuration object to be passed to MSAL instance on creation. - * For a full list of MSAL.js configuration parameters, visit: - * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md - */ - msalConfig: { - auth: { - clientId: string; // Client app id used for login - authority: string; // Directory to use for login https://learn.microsoft.com/entra/identity-platform/msal-client-application-configuration#authority - redirectUri: string; // Points to window.location.origin. You must register this URI on Azure Portal/App Registration. - postLogoutRedirectUri: string; // Indicates the page to navigate after logout. - navigateToLoginRequestUrl: boolean; // If "true", will navigate back to the original request location before processing the auth code response. - }; - cache: { - cacheLocation: string; // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs. - storeAuthStateInCookie: boolean; // Set this to "true" if you are having issues on IE11 or Edge - }; - }; - loginRequest: { - /** - * Scopes you add here will be prompted for user consent during sign-in. - * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. - * For more information about OIDC scopes, visit: - * https://learn.microsoft.com/entra/identity-platform/permissions-consent-overview#openid-connect-scopes - */ - scopes: Array; - }; - tokenRequest: { - scopes: Array; - }; -} - -// Fetch the auth setup JSON data from the API if not already cached -async function fetchAuthSetup(): Promise { - const response = await fetch("/auth_setup"); - if (!response.ok) { - throw new Error(`auth setup response was not ok: ${response.status}`); - } - return await response.json(); -} - -const authSetup = await fetchAuthSetup(); - -export const useLogin = authSetup.useLogin; - -export const requireAccessControl = authSetup.requireAccessControl; - -export const enableUnauthenticatedAccess = authSetup.enableUnauthenticatedAccess; - -export const requireLogin = requireAccessControl && !enableUnauthenticatedAccess; - -/** - * Configuration object to be passed to MSAL instance on creation. - * For a full list of MSAL.js configuration parameters, visit: - * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md - */ -export const msalConfig = authSetup.msalConfig; - -/** - * Scopes you add here will be prompted for user consent during sign-in. - * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. - * For more information about OIDC scopes, visit: - * https://learn.microsoft.com/entra/identity-platform/permissions-consent-overview#openid-connect-scopes - */ -export const loginRequest = authSetup.loginRequest; - -const tokenRequest = authSetup.tokenRequest; - -// Build an absolute redirect URI using the current window's location and the relative redirect URI from auth setup -export const getRedirectUri = () => { - return window.location.origin + authSetup.msalConfig.auth.redirectUri; -}; - -// Get an access token if a user logged in using app services authentication -// Returns null if the app doesn't support app services authentication -const getAppServicesToken = (): Promise => { - return fetch(appServicesAuthTokenRefreshUrl).then(r => { - if (r.ok) { - return fetch(appServicesAuthTokenUrl).then(r => { - if (r.ok) { - return r.json().then(json => { - if (json.length > 0) { - return { - id_token: json[0]["id_token"] as string, - access_token: json[0]["access_token"] as string, - user_claims: json[0]["user_claims"].reduce((acc: Record, item: Record) => { - acc[item.typ] = item.val; - return acc; - }, {}) as Record - }; - } - - return null; - }); - } - - return null; - }); - } - - return null; - }); -}; - -export const appServicesToken = await getAppServicesToken(); - -// Sign out of app services -// Learn more at https://learn.microsoft.com/azure/app-service/configure-authentication-customize-sign-in-out#sign-out-of-a-session -export const appServicesLogout = () => { - window.location.href = appServicesAuthLogoutUrl; -}; - -// Determine if the user is logged in -// The user may have logged in either using the app services login or the on-page login -export const isLoggedIn = (client: IPublicClientApplication | undefined): boolean => { - return client?.getActiveAccount() != null || appServicesToken != null; -}; - -// Get an access token for use with the API server. -// ID token received when logging in may not be used for this purpose because it has the incorrect audience -// Use the access token from app services login if available -export const getToken = (client: IPublicClientApplication): Promise => { - if (appServicesToken) { - return Promise.resolve(appServicesToken.access_token); - } - - return client - .acquireTokenSilent({ - ...tokenRequest, - redirectUri: getRedirectUri() - }) - .then(r => r.accessToken) - .catch(error => { - console.log(error); - return undefined; - }); -}; +// Refactored from https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/blob/main/1-Authentication/1-sign-in/SPA/src/authConfig.js + +import { IPublicClientApplication } from "@azure/msal-browser"; + +const appServicesAuthTokenUrl = ".auth/me"; +const appServicesAuthTokenRefreshUrl = ".auth/refresh"; +const appServicesAuthLogoutUrl = ".auth/logout?post_logout_redirect_uri=/"; + +interface AppServicesToken { + id_token: string; + access_token: string; + user_claims: Record; +} + +interface AuthSetup { + // Set to true if login elements should be shown in the UI + useLogin: boolean; + // Set to true if access control is enforced by the application + requireAccessControl: boolean; + // Set to true if the application allows unauthenticated access (only applies for documents without access control) + enableUnauthenticatedAccess: boolean; + /** + * Configuration object to be passed to MSAL instance on creation. + * For a full list of MSAL.js configuration parameters, visit: + * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md + */ + msalConfig: { + auth: { + clientId: string; // Client app id used for login + authority: string; // Directory to use for login https://learn.microsoft.com/entra/identity-platform/msal-client-application-configuration#authority + redirectUri: string; // Points to window.location.origin. You must register this URI on Azure Portal/App Registration. + postLogoutRedirectUri: string; // Indicates the page to navigate after logout. + navigateToLoginRequestUrl: boolean; // If "true", will navigate back to the original request location before processing the auth code response. + }; + cache: { + cacheLocation: string; // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs. + storeAuthStateInCookie: boolean; // Set this to "true" if you are having issues on IE11 or Edge + }; + }; + loginRequest: { + /** + * Scopes you add here will be prompted for user consent during sign-in. + * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. + * For more information about OIDC scopes, visit: + * https://learn.microsoft.com/entra/identity-platform/permissions-consent-overview#openid-connect-scopes + */ + scopes: Array; + }; + tokenRequest: { + scopes: Array; + }; +} + +// Fetch the auth setup JSON data from the API if not already cached +async function fetchAuthSetup(): Promise { + const response = await fetch("/auth_setup"); + if (!response.ok) { + throw new Error(`auth setup response was not ok: ${response.status}`); + } + return await response.json(); +} + +const authSetup = await fetchAuthSetup(); + +export const useLogin = authSetup.useLogin; + +export const requireAccessControl = authSetup.requireAccessControl; + +export const enableUnauthenticatedAccess = authSetup.enableUnauthenticatedAccess; + +export const requireLogin = requireAccessControl && !enableUnauthenticatedAccess; + +/** + * Configuration object to be passed to MSAL instance on creation. + * For a full list of MSAL.js configuration parameters, visit: + * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md + */ +export const msalConfig = authSetup.msalConfig; + +/** + * Scopes you add here will be prompted for user consent during sign-in. + * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request. + * For more information about OIDC scopes, visit: + * https://learn.microsoft.com/entra/identity-platform/permissions-consent-overview#openid-connect-scopes + */ +export const loginRequest = authSetup.loginRequest; + +const tokenRequest = authSetup.tokenRequest; + +// Build an absolute redirect URI using the current window's location and the relative redirect URI from auth setup +export const getRedirectUri = () => { + return window.location.origin + authSetup.msalConfig.auth.redirectUri; +}; + +// Get an access token if a user logged in using app services authentication +// Returns null if the app doesn't support app services authentication +const getAppServicesToken = (): Promise => { + return fetch(appServicesAuthTokenRefreshUrl).then(r => { + if (r.ok) { + return fetch(appServicesAuthTokenUrl).then(r => { + if (r.ok) { + return r.json().then(json => { + if (json.length > 0) { + return { + id_token: json[0]["id_token"] as string, + access_token: json[0]["access_token"] as string, + user_claims: json[0]["user_claims"].reduce((acc: Record, item: Record) => { + acc[item.typ] = item.val; + return acc; + }, {}) as Record + }; + } + + return null; + }); + } + + return null; + }); + } + + return null; + }); +}; + +export const appServicesToken = await getAppServicesToken(); + +// Sign out of app services +// Learn more at https://learn.microsoft.com/azure/app-service/configure-authentication-customize-sign-in-out#sign-out-of-a-session +export const appServicesLogout = () => { + window.location.href = appServicesAuthLogoutUrl; +}; + +// Determine if the user is logged in +// The user may have logged in either using the app services login or the on-page login +export const isLoggedIn = (client: IPublicClientApplication | undefined): boolean => { + return client?.getActiveAccount() != null || appServicesToken != null; +}; + +// Get an access token for use with the API server. +// ID token received when logging in may not be used for this purpose because it has the incorrect audience +// Use the access token from app services login if available +export const getToken = (client: IPublicClientApplication): Promise => { + if (appServicesToken) { + return Promise.resolve(appServicesToken.access_token); + } + + return client + .acquireTokenSilent({ + ...tokenRequest, + redirectUri: getRedirectUri() + }) + .then(r => r.accessToken) + .catch(error => { + console.log(error); + return undefined; + }); +}; diff --git a/app/frontend/src/components/AnalysisPanel/AnalysisPanel.module.css b/app/frontend/src/components/AnalysisPanel/AnalysisPanel.module.css index 5bb03c848e..1cf9ca0081 100644 --- a/app/frontend/src/components/AnalysisPanel/AnalysisPanel.module.css +++ b/app/frontend/src/components/AnalysisPanel/AnalysisPanel.module.css @@ -1,64 +1,64 @@ -.thoughtProcess { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; - word-wrap: break-word; - padding-top: 12px; - padding-bottom: 12px; -} - -.tList { - padding: 20px 20px 0 20px; - display: inline-block; - background: #e9e9e9; -} - -.tListItem { - list-style: none; - margin: auto; - margin-left: 20px; - min-height: 50px; - border-left: 1px dashed #123bb6; - padding: 0 0 30px 30px; - position: relative; -} - -.tListItem:last-child { - border-left: 0; -} - -.tListItem::before { - position: absolute; - left: -18px; - top: -5px; - content: " "; - border: 8px solid #d1dbfa; - border-radius: 500%; - background: #123bb6; - height: 20px; - width: 20px; -} - -.tStep { - color: #123bb6; - position: relative; - font-size: 14px; - margin-bottom: 8px; -} - -.tCodeBlock { - max-height: 300px; -} - -.tProp { - background-color: #d7d7d7; - color: #333232; - font-size: 12px; - padding: 3px 10px; - border-radius: 10px; - margin-bottom: 8px; -} - -.citationImg { - height: 450px; - max-width: 100%; - object-fit: contain; -} +.thoughtProcess { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; + word-wrap: break-word; + padding-top: 12px; + padding-bottom: 12px; +} + +.tList { + padding: 20px 20px 0 20px; + display: inline-block; + background: #e9e9e9; +} + +.tListItem { + list-style: none; + margin: auto; + margin-left: 20px; + min-height: 50px; + border-left: 1px dashed #123bb6; + padding: 0 0 30px 30px; + position: relative; +} + +.tListItem:last-child { + border-left: 0; +} + +.tListItem::before { + position: absolute; + left: -18px; + top: -5px; + content: " "; + border: 8px solid #d1dbfa; + border-radius: 500%; + background: #123bb6; + height: 20px; + width: 20px; +} + +.tStep { + color: #123bb6; + position: relative; + font-size: 14px; + margin-bottom: 8px; +} + +.tCodeBlock { + max-height: 300px; +} + +.tProp { + background-color: #d7d7d7; + color: #333232; + font-size: 12px; + padding: 3px 10px; + border-radius: 10px; + margin-bottom: 8px; +} + +.citationImg { + height: 450px; + max-width: 100%; + object-fit: contain; +} diff --git a/app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx b/app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx index 3f4853797a..1014bbac18 100644 --- a/app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx +++ b/app/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx @@ -1,102 +1,102 @@ -import { Stack, Pivot, PivotItem } from "@fluentui/react"; - -import styles from "./AnalysisPanel.module.css"; - -import { SupportingContent } from "../SupportingContent"; -import { ChatAppResponse } from "../../api"; -import { AnalysisPanelTabs } from "./AnalysisPanelTabs"; -import { ThoughtProcess } from "./ThoughtProcess"; -import { MarkdownViewer } from "../MarkdownViewer"; -import { useMsal } from "@azure/msal-react"; -import { getHeaders } from "../../api"; -import { useLogin, getToken } from "../../authConfig"; -import { useState, useEffect } from "react"; - -interface Props { - className: string; - activeTab: AnalysisPanelTabs; - onActiveTabChanged: (tab: AnalysisPanelTabs) => void; - activeCitation: string | undefined; - citationHeight: string; - answer: ChatAppResponse; -} - -const pivotItemDisabledStyle = { disabled: true, style: { color: "grey" } }; - -export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeight, className, onActiveTabChanged }: Props) => { - const isDisabledThoughtProcessTab: boolean = !answer.context.thoughts; - const isDisabledSupportingContentTab: boolean = !answer.context.data_points; - const isDisabledCitationTab: boolean = !activeCitation; - const [citation, setCitation] = useState(""); - - const client = useLogin ? useMsal().instance : undefined; - - const fetchCitation = async () => { - const token = client ? await getToken(client) : undefined; - if (activeCitation) { - // Get hash from the URL as it may contain #page=N - // which helps browser PDF renderer jump to correct page N - const originalHash = activeCitation.indexOf("#") ? activeCitation.split("#")[1] : ""; - const response = await fetch(activeCitation, { - method: "GET", - headers: getHeaders(token) - }); - const citationContent = await response.blob(); - let citationObjectUrl = URL.createObjectURL(citationContent); - // Add hash back to the new blob URL - if (originalHash) { - citationObjectUrl += "#" + originalHash; - } - setCitation(citationObjectUrl); - } - }; - useEffect(() => { - fetchCitation(); - }, []); - - const renderFileViewer = () => { - if (!activeCitation) { - return null; - } - - const fileExtension = activeCitation.split(".").pop()?.toLowerCase(); - switch (fileExtension) { - case "png": - return Citation Image; - case "md": - return ; - default: - return