mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-04-02 05:22:43 +00:00
Compare commits
9 Commits
feat/resol
...
multi-mode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caa8811d61 | ||
|
|
7208d7ba8d | ||
|
|
23058c416d | ||
|
|
0874e0a5e6 | ||
|
|
165237faf4 | ||
|
|
19c3122fec | ||
|
|
00b228b357 | ||
|
|
59ae32f764 | ||
|
|
76000330ad |
285
.github/workflows/deployment.yml
vendored
285
.github/workflows/deployment.yml
vendored
@@ -1509,105 +1509,232 @@ jobs:
|
||||
$(printf '%s\n' "${META_TAGS}" | xargs -I {} echo -t {}) \
|
||||
$IMAGES
|
||||
|
||||
trivy-scan:
|
||||
trivy-scan-web:
|
||||
needs:
|
||||
- determine-builds
|
||||
- merge-web
|
||||
- merge-web-cloud
|
||||
- merge-backend
|
||||
- merge-model-server
|
||||
if: >-
|
||||
always() && !cancelled() &&
|
||||
(needs.merge-web.result == 'success' ||
|
||||
needs.merge-web-cloud.result == 'success' ||
|
||||
needs.merge-backend.result == 'success' ||
|
||||
needs.merge-model-server.result == 'success')
|
||||
if: needs.merge-web.result == 'success'
|
||||
runs-on:
|
||||
- runs-on
|
||||
- runner=2cpu-linux-arm64
|
||||
- run-id=${{ github.run_id }}-trivy-scan-${{ matrix.component }}
|
||||
- run-id=${{ github.run_id }}-trivy-scan-web
|
||||
- extras=ecr-cache
|
||||
permissions:
|
||||
security-events: write # needed for SARIF uploads
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- component: web
|
||||
registry-image: onyxdotapp/onyx-web-server
|
||||
- component: web-cloud
|
||||
registry-image: onyxdotapp/onyx-web-server-cloud
|
||||
- component: backend
|
||||
registry-image: ${{ contains(github.ref_name, 'cloud') && 'onyxdotapp/onyx-backend-cloud' || 'onyxdotapp/onyx-backend' }}
|
||||
trivyignore: backend/.trivyignore
|
||||
- component: model-server
|
||||
registry-image: ${{ contains(github.ref_name, 'cloud') && 'onyxdotapp/onyx-model-server-cloud' || 'onyxdotapp/onyx-model-server' }}
|
||||
timeout-minutes: 90
|
||||
environment: release
|
||||
env:
|
||||
REGISTRY_IMAGE: onyxdotapp/onyx-web-server
|
||||
steps:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
|
||||
- name: Get AWS Secrets
|
||||
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
|
||||
with:
|
||||
secret-ids: |
|
||||
DOCKER_USERNAME, deploy/docker-username
|
||||
DOCKER_TOKEN, deploy/docker-token
|
||||
parse-json-secrets: true
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # ratchet:nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 10
|
||||
command: |
|
||||
if [ "${{ needs.determine-builds.outputs.is-test-run }}" == "true" ]; then
|
||||
SCAN_IMAGE="${{ env.RUNS_ON_ECR_CACHE }}:web-${{ needs.determine-builds.outputs.sanitized-tag }}"
|
||||
else
|
||||
SCAN_IMAGE="docker.io/${{ env.REGISTRY_IMAGE }}:${{ github.ref_name }}"
|
||||
fi
|
||||
docker run --rm -v $HOME/.cache/trivy:/root/.cache/trivy \
|
||||
-e TRIVY_DB_REPOSITORY="public.ecr.aws/aquasecurity/trivy-db:2" \
|
||||
-e TRIVY_JAVA_DB_REPOSITORY="public.ecr.aws/aquasecurity/trivy-java-db:1" \
|
||||
-e TRIVY_USERNAME="${{ env.DOCKER_USERNAME }}" \
|
||||
-e TRIVY_PASSWORD="${{ env.DOCKER_TOKEN }}" \
|
||||
aquasec/trivy@sha256:a22415a38938a56c379387a8163fcb0ce38b10ace73e593475d3658d578b2436 \
|
||||
image \
|
||||
--skip-version-check \
|
||||
--timeout 20m \
|
||||
--severity CRITICAL,HIGH \
|
||||
${SCAN_IMAGE}
|
||||
|
||||
trivy-scan-web-cloud:
|
||||
needs:
|
||||
- determine-builds
|
||||
- merge-web-cloud
|
||||
if: needs.merge-web-cloud.result == 'success'
|
||||
runs-on:
|
||||
- runs-on
|
||||
- runner=2cpu-linux-arm64
|
||||
- run-id=${{ github.run_id }}-trivy-scan-web-cloud
|
||||
- extras=ecr-cache
|
||||
timeout-minutes: 90
|
||||
environment: release
|
||||
env:
|
||||
REGISTRY_IMAGE: onyxdotapp/onyx-web-server-cloud
|
||||
steps:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
|
||||
- name: Get AWS Secrets
|
||||
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
|
||||
with:
|
||||
secret-ids: |
|
||||
DOCKER_USERNAME, deploy/docker-username
|
||||
DOCKER_TOKEN, deploy/docker-token
|
||||
parse-json-secrets: true
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # ratchet:nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 10
|
||||
command: |
|
||||
if [ "${{ needs.determine-builds.outputs.is-test-run }}" == "true" ]; then
|
||||
SCAN_IMAGE="${{ env.RUNS_ON_ECR_CACHE }}:web-cloud-${{ needs.determine-builds.outputs.sanitized-tag }}"
|
||||
else
|
||||
SCAN_IMAGE="docker.io/${{ env.REGISTRY_IMAGE }}:${{ github.ref_name }}"
|
||||
fi
|
||||
docker run --rm -v $HOME/.cache/trivy:/root/.cache/trivy \
|
||||
-e TRIVY_DB_REPOSITORY="public.ecr.aws/aquasecurity/trivy-db:2" \
|
||||
-e TRIVY_JAVA_DB_REPOSITORY="public.ecr.aws/aquasecurity/trivy-java-db:1" \
|
||||
-e TRIVY_USERNAME="${{ env.DOCKER_USERNAME }}" \
|
||||
-e TRIVY_PASSWORD="${{ env.DOCKER_TOKEN }}" \
|
||||
aquasec/trivy@sha256:a22415a38938a56c379387a8163fcb0ce38b10ace73e593475d3658d578b2436 \
|
||||
image \
|
||||
--skip-version-check \
|
||||
--timeout 20m \
|
||||
--severity CRITICAL,HIGH \
|
||||
${SCAN_IMAGE}
|
||||
|
||||
trivy-scan-backend:
|
||||
needs:
|
||||
- determine-builds
|
||||
- merge-backend
|
||||
if: needs.merge-backend.result == 'success'
|
||||
runs-on:
|
||||
- runs-on
|
||||
- runner=2cpu-linux-arm64
|
||||
- run-id=${{ github.run_id }}-trivy-scan-backend
|
||||
- extras=ecr-cache
|
||||
timeout-minutes: 90
|
||||
environment: release
|
||||
env:
|
||||
REGISTRY_IMAGE: ${{ contains(github.ref_name, 'cloud') && 'onyxdotapp/onyx-backend-cloud' || 'onyxdotapp/onyx-backend' }}
|
||||
steps:
|
||||
- name: Check if this scan should run
|
||||
id: should-run
|
||||
run: |
|
||||
case "$COMPONENT" in
|
||||
web) RESULT="$MERGE_WEB" ;;
|
||||
web-cloud) RESULT="$MERGE_WEB_CLOUD" ;;
|
||||
backend) RESULT="$MERGE_BACKEND" ;;
|
||||
model-server) RESULT="$MERGE_MODEL_SERVER" ;;
|
||||
esac
|
||||
if [ "$RESULT" == "success" ]; then
|
||||
echo "run=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "run=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
env:
|
||||
COMPONENT: ${{ matrix.component }}
|
||||
MERGE_WEB: ${{ needs.merge-web.result }}
|
||||
MERGE_WEB_CLOUD: ${{ needs.merge-web-cloud.result }}
|
||||
MERGE_BACKEND: ${{ needs.merge-backend.result }}
|
||||
MERGE_MODEL_SERVER: ${{ needs.merge-model-server.result }}
|
||||
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
if: steps.should-run.outputs.run == 'true'
|
||||
|
||||
- name: Checkout
|
||||
if: steps.should-run.outputs.run == 'true' && matrix.trivyignore != ''
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Determine scan image
|
||||
if: steps.should-run.outputs.run == 'true'
|
||||
id: scan-image
|
||||
run: |
|
||||
if [ "$IS_TEST_RUN" == "true" ]; then
|
||||
echo "image=${RUNS_ON_ECR_CACHE}:${TAG_PREFIX}-${SANITIZED_TAG}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "image=docker.io/${REGISTRY_IMAGE}:${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
env:
|
||||
IS_TEST_RUN: ${{ needs.determine-builds.outputs.is-test-run }}
|
||||
TAG_PREFIX: ${{ matrix.component }}
|
||||
SANITIZED_TAG: ${{ needs.determine-builds.outputs.sanitized-tag }}
|
||||
REGISTRY_IMAGE: ${{ matrix.registry-image }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
|
||||
- name: Get AWS Secrets
|
||||
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
|
||||
with:
|
||||
secret-ids: |
|
||||
DOCKER_USERNAME, deploy/docker-username
|
||||
DOCKER_TOKEN, deploy/docker-token
|
||||
parse-json-secrets: true
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
if: steps.should-run.outputs.run == 'true'
|
||||
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # ratchet:aquasecurity/trivy-action@v0.35.0
|
||||
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # ratchet:nick-fields/retry@v3
|
||||
with:
|
||||
image-ref: ${{ steps.scan-image.outputs.image }}
|
||||
severity: CRITICAL,HIGH
|
||||
format: "sarif"
|
||||
output: "trivy-results.sarif"
|
||||
trivyignores: ${{ matrix.trivyignore }}
|
||||
env:
|
||||
TRIVY_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
TRIVY_PASSWORD: ${{ secrets.DOCKER_TOKEN }}
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 10
|
||||
command: |
|
||||
if [ "${{ needs.determine-builds.outputs.is-test-run }}" == "true" ]; then
|
||||
SCAN_IMAGE="${{ env.RUNS_ON_ECR_CACHE }}:backend-${{ needs.determine-builds.outputs.sanitized-tag }}"
|
||||
else
|
||||
SCAN_IMAGE="docker.io/${{ env.REGISTRY_IMAGE }}:${{ github.ref_name }}"
|
||||
fi
|
||||
docker run --rm -v $HOME/.cache/trivy:/root/.cache/trivy \
|
||||
-v ${{ github.workspace }}/backend/.trivyignore:/tmp/.trivyignore:ro \
|
||||
-e TRIVY_DB_REPOSITORY="public.ecr.aws/aquasecurity/trivy-db:2" \
|
||||
-e TRIVY_JAVA_DB_REPOSITORY="public.ecr.aws/aquasecurity/trivy-java-db:1" \
|
||||
-e TRIVY_USERNAME="${{ env.DOCKER_USERNAME }}" \
|
||||
-e TRIVY_PASSWORD="${{ env.DOCKER_TOKEN }}" \
|
||||
aquasec/trivy@sha256:a22415a38938a56c379387a8163fcb0ce38b10ace73e593475d3658d578b2436 \
|
||||
image \
|
||||
--skip-version-check \
|
||||
--timeout 20m \
|
||||
--severity CRITICAL,HIGH \
|
||||
--ignorefile /tmp/.trivyignore \
|
||||
${SCAN_IMAGE}
|
||||
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
if: steps.should-run.outputs.run == 'true'
|
||||
uses: github/codeql-action/upload-sarif@ba454b8ab46733eb6145342877cd148270bb77ab
|
||||
trivy-scan-model-server:
|
||||
needs:
|
||||
- determine-builds
|
||||
- merge-model-server
|
||||
if: needs.merge-model-server.result == 'success'
|
||||
runs-on:
|
||||
- runs-on
|
||||
- runner=2cpu-linux-arm64
|
||||
- run-id=${{ github.run_id }}-trivy-scan-model-server
|
||||
- extras=ecr-cache
|
||||
timeout-minutes: 90
|
||||
environment: release
|
||||
env:
|
||||
REGISTRY_IMAGE: ${{ contains(github.ref_name, 'cloud') && 'onyxdotapp/onyx-model-server-cloud' || 'onyxdotapp/onyx-model-server' }}
|
||||
steps:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7
|
||||
with:
|
||||
sarif_file: "trivy-results.sarif"
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
|
||||
- name: Get AWS Secrets
|
||||
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
|
||||
with:
|
||||
secret-ids: |
|
||||
DOCKER_USERNAME, deploy/docker-username
|
||||
DOCKER_TOKEN, deploy/docker-token
|
||||
parse-json-secrets: true
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # ratchet:nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
retry_wait_seconds: 10
|
||||
command: |
|
||||
if [ "${{ needs.determine-builds.outputs.is-test-run }}" == "true" ]; then
|
||||
SCAN_IMAGE="${{ env.RUNS_ON_ECR_CACHE }}:model-server-${{ needs.determine-builds.outputs.sanitized-tag }}"
|
||||
else
|
||||
SCAN_IMAGE="docker.io/${{ env.REGISTRY_IMAGE }}:${{ github.ref_name }}"
|
||||
fi
|
||||
docker run --rm -v $HOME/.cache/trivy:/root/.cache/trivy \
|
||||
-e TRIVY_DB_REPOSITORY="public.ecr.aws/aquasecurity/trivy-db:2" \
|
||||
-e TRIVY_JAVA_DB_REPOSITORY="public.ecr.aws/aquasecurity/trivy-java-db:1" \
|
||||
-e TRIVY_USERNAME="${{ env.DOCKER_USERNAME }}" \
|
||||
-e TRIVY_PASSWORD="${{ env.DOCKER_TOKEN }}" \
|
||||
aquasec/trivy@sha256:a22415a38938a56c379387a8163fcb0ce38b10ace73e593475d3658d578b2436 \
|
||||
image \
|
||||
--skip-version-check \
|
||||
--timeout 20m \
|
||||
--severity CRITICAL,HIGH \
|
||||
${SCAN_IMAGE}
|
||||
|
||||
notify-slack-on-failure:
|
||||
needs:
|
||||
|
||||
174
.github/workflows/pr-python-connector-tests.yml
vendored
174
.github/workflows/pr-python-connector-tests.yml
vendored
@@ -22,40 +22,132 @@ on:
|
||||
- cron: "0 16 * * *"
|
||||
|
||||
permissions:
|
||||
id-token: write # Required for OIDC-based AWS credential exchange
|
||||
contents: read
|
||||
|
||||
env:
|
||||
PYTHONPATH: ./backend
|
||||
DISABLE_TELEMETRY: "true"
|
||||
# AWS
|
||||
AWS_ACCESS_KEY_ID_DAILY_CONNECTOR_TESTS: ${{ secrets.AWS_ACCESS_KEY_ID_DAILY_CONNECTOR_TESTS }}
|
||||
AWS_SECRET_ACCESS_KEY_DAILY_CONNECTOR_TESTS: ${{ secrets.AWS_SECRET_ACCESS_KEY_DAILY_CONNECTOR_TESTS }}
|
||||
|
||||
# Cloudflare R2
|
||||
R2_ACCOUNT_ID_DAILY_CONNECTOR_TESTS: ${{ vars.R2_ACCOUNT_ID_DAILY_CONNECTOR_TESTS }}
|
||||
R2_ACCESS_KEY_ID_DAILY_CONNECTOR_TESTS: ${{ secrets.R2_ACCESS_KEY_ID_DAILY_CONNECTOR_TESTS }}
|
||||
R2_SECRET_ACCESS_KEY_DAILY_CONNECTOR_TESTS: ${{ secrets.R2_SECRET_ACCESS_KEY_DAILY_CONNECTOR_TESTS }}
|
||||
|
||||
# Google Cloud Storage
|
||||
GCS_ACCESS_KEY_ID_DAILY_CONNECTOR_TESTS: ${{ secrets.GCS_ACCESS_KEY_ID_DAILY_CONNECTOR_TESTS }}
|
||||
GCS_SECRET_ACCESS_KEY_DAILY_CONNECTOR_TESTS: ${{ secrets.GCS_SECRET_ACCESS_KEY_DAILY_CONNECTOR_TESTS }}
|
||||
|
||||
# Confluence
|
||||
CONFLUENCE_TEST_SPACE_URL: ${{ vars.CONFLUENCE_TEST_SPACE_URL }}
|
||||
CONFLUENCE_TEST_SPACE: ${{ vars.CONFLUENCE_TEST_SPACE }}
|
||||
CONFLUENCE_TEST_PAGE_ID: ${{ secrets.CONFLUENCE_TEST_PAGE_ID }}
|
||||
CONFLUENCE_USER_NAME: ${{ vars.CONFLUENCE_USER_NAME }}
|
||||
CONFLUENCE_ACCESS_TOKEN: ${{ secrets.CONFLUENCE_ACCESS_TOKEN }}
|
||||
CONFLUENCE_ACCESS_TOKEN_SCOPED: ${{ secrets.CONFLUENCE_ACCESS_TOKEN_SCOPED }}
|
||||
|
||||
# Jira
|
||||
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
|
||||
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
|
||||
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
||||
JIRA_API_TOKEN_SCOPED: ${{ secrets.JIRA_API_TOKEN_SCOPED }}
|
||||
|
||||
# Gong
|
||||
GONG_ACCESS_KEY: ${{ secrets.GONG_ACCESS_KEY }}
|
||||
GONG_ACCESS_KEY_SECRET: ${{ secrets.GONG_ACCESS_KEY_SECRET }}
|
||||
|
||||
# Google
|
||||
GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON_STR: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON_STR }}
|
||||
GOOGLE_DRIVE_OAUTH_CREDENTIALS_JSON_STR_TEST_USER_1: ${{ secrets.GOOGLE_DRIVE_OAUTH_CREDENTIALS_JSON_STR_TEST_USER_1 }}
|
||||
GOOGLE_DRIVE_OAUTH_CREDENTIALS_JSON_STR: ${{ secrets.GOOGLE_DRIVE_OAUTH_CREDENTIALS_JSON_STR }}
|
||||
GOOGLE_GMAIL_SERVICE_ACCOUNT_JSON_STR: ${{ secrets.GOOGLE_GMAIL_SERVICE_ACCOUNT_JSON_STR }}
|
||||
GOOGLE_GMAIL_OAUTH_CREDENTIALS_JSON_STR: ${{ secrets.GOOGLE_GMAIL_OAUTH_CREDENTIALS_JSON_STR }}
|
||||
|
||||
# Slab
|
||||
SLAB_BOT_TOKEN: ${{ secrets.SLAB_BOT_TOKEN }}
|
||||
|
||||
# Zendesk
|
||||
ZENDESK_SUBDOMAIN: ${{ secrets.ZENDESK_SUBDOMAIN }}
|
||||
ZENDESK_EMAIL: ${{ secrets.ZENDESK_EMAIL }}
|
||||
ZENDESK_TOKEN: ${{ secrets.ZENDESK_TOKEN }}
|
||||
|
||||
# Salesforce
|
||||
SF_USERNAME: ${{ vars.SF_USERNAME }}
|
||||
SF_PASSWORD: ${{ secrets.SF_PASSWORD }}
|
||||
SF_SECURITY_TOKEN: ${{ secrets.SF_SECURITY_TOKEN }}
|
||||
|
||||
# Hubspot
|
||||
HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_ACCESS_TOKEN }}
|
||||
|
||||
# IMAP
|
||||
IMAP_HOST: ${{ vars.IMAP_HOST }}
|
||||
IMAP_USERNAME: ${{ vars.IMAP_USERNAME }}
|
||||
IMAP_PASSWORD: ${{ secrets.IMAP_PASSWORD }}
|
||||
IMAP_MAILBOXES: ${{ vars.IMAP_MAILBOXES }}
|
||||
|
||||
# Airtable
|
||||
AIRTABLE_TEST_BASE_ID: ${{ vars.AIRTABLE_TEST_BASE_ID }}
|
||||
AIRTABLE_TEST_TABLE_ID: ${{ vars.AIRTABLE_TEST_TABLE_ID }}
|
||||
AIRTABLE_TEST_TABLE_NAME: ${{ vars.AIRTABLE_TEST_TABLE_NAME }}
|
||||
AIRTABLE_ACCESS_TOKEN: ${{ secrets.AIRTABLE_ACCESS_TOKEN }}
|
||||
|
||||
# Sharepoint
|
||||
SHAREPOINT_CLIENT_ID: ${{ vars.SHAREPOINT_CLIENT_ID }}
|
||||
SHAREPOINT_CLIENT_SECRET: ${{ secrets.SHAREPOINT_CLIENT_SECRET }}
|
||||
SHAREPOINT_CLIENT_DIRECTORY_ID: ${{ vars.SHAREPOINT_CLIENT_DIRECTORY_ID }}
|
||||
SHAREPOINT_SITE: ${{ vars.SHAREPOINT_SITE }}
|
||||
PERM_SYNC_SHAREPOINT_CLIENT_ID: ${{ secrets.PERM_SYNC_SHAREPOINT_CLIENT_ID }}
|
||||
PERM_SYNC_SHAREPOINT_PRIVATE_KEY: ${{ secrets.PERM_SYNC_SHAREPOINT_PRIVATE_KEY }}
|
||||
PERM_SYNC_SHAREPOINT_CERTIFICATE_PASSWORD: ${{ secrets.PERM_SYNC_SHAREPOINT_CERTIFICATE_PASSWORD }}
|
||||
PERM_SYNC_SHAREPOINT_DIRECTORY_ID: ${{ secrets.PERM_SYNC_SHAREPOINT_DIRECTORY_ID }}
|
||||
|
||||
# Github
|
||||
ACCESS_TOKEN_GITHUB: ${{ secrets.ACCESS_TOKEN_GITHUB }}
|
||||
|
||||
# Gitlab
|
||||
GITLAB_ACCESS_TOKEN: ${{ secrets.GITLAB_ACCESS_TOKEN }}
|
||||
|
||||
# Gitbook
|
||||
GITBOOK_SPACE_ID: ${{ secrets.GITBOOK_SPACE_ID }}
|
||||
GITBOOK_API_KEY: ${{ secrets.GITBOOK_API_KEY }}
|
||||
|
||||
# Notion
|
||||
NOTION_INTEGRATION_TOKEN: ${{ secrets.NOTION_INTEGRATION_TOKEN }}
|
||||
|
||||
# Highspot
|
||||
HIGHSPOT_KEY: ${{ secrets.HIGHSPOT_KEY }}
|
||||
HIGHSPOT_SECRET: ${{ secrets.HIGHSPOT_SECRET }}
|
||||
|
||||
# Slack
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
|
||||
# Discord
|
||||
DISCORD_CONNECTOR_BOT_TOKEN: ${{ secrets.DISCORD_CONNECTOR_BOT_TOKEN }}
|
||||
|
||||
# Teams
|
||||
TEAMS_APPLICATION_ID: ${{ secrets.TEAMS_APPLICATION_ID }}
|
||||
TEAMS_DIRECTORY_ID: ${{ secrets.TEAMS_DIRECTORY_ID }}
|
||||
TEAMS_SECRET: ${{ secrets.TEAMS_SECRET }}
|
||||
|
||||
# Bitbucket
|
||||
BITBUCKET_WORKSPACE: ${{ secrets.BITBUCKET_WORKSPACE }}
|
||||
BITBUCKET_REPOSITORIES: ${{ secrets.BITBUCKET_REPOSITORIES }}
|
||||
BITBUCKET_PROJECTS: ${{ secrets.BITBUCKET_PROJECTS }}
|
||||
BITBUCKET_EMAIL: ${{ vars.BITBUCKET_EMAIL }}
|
||||
BITBUCKET_API_TOKEN: ${{ secrets.BITBUCKET_API_TOKEN }}
|
||||
|
||||
# Fireflies
|
||||
FIREFLIES_API_KEY: ${{ secrets.FIREFLIES_API_KEY }}
|
||||
|
||||
jobs:
|
||||
connectors-check:
|
||||
# See https://runs-on.com/runners/linux/
|
||||
runs-on:
|
||||
[
|
||||
runs-on,
|
||||
runner=8cpu-linux-x64,
|
||||
"run-id=${{ github.run_id }}-connectors-check",
|
||||
"extras=s3-cache",
|
||||
]
|
||||
runs-on: [runs-on, runner=8cpu-linux-x64, "run-id=${{ github.run_id }}-connectors-check", "extras=s3-cache"]
|
||||
timeout-minutes: 45
|
||||
environment: ci-protected
|
||||
|
||||
env:
|
||||
PYTHONPATH: ./backend
|
||||
DISABLE_TELEMETRY: "true"
|
||||
|
||||
steps:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
@@ -96,66 +188,6 @@ jobs:
|
||||
- 'backend/onyx/file_processing/**'
|
||||
- 'uv.lock'
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # ratchet:aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
|
||||
aws-region: us-east-2
|
||||
|
||||
- name: Get connector test secrets from AWS Secrets Manager
|
||||
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802 # ratchet:aws-actions/aws-secretsmanager-get-secrets@v2
|
||||
with:
|
||||
parse-json-secrets: false
|
||||
secret-ids: |
|
||||
AWS_ACCESS_KEY_ID_DAILY_CONNECTOR_TESTS, test/aws-access-key-id
|
||||
AWS_SECRET_ACCESS_KEY_DAILY_CONNECTOR_TESTS, test/aws-secret-access-key
|
||||
R2_ACCESS_KEY_ID_DAILY_CONNECTOR_TESTS, test/r2-access-key-id
|
||||
R2_SECRET_ACCESS_KEY_DAILY_CONNECTOR_TESTS, test/r2-secret-access-key
|
||||
GCS_ACCESS_KEY_ID_DAILY_CONNECTOR_TESTS, test/gcs-access-key-id
|
||||
GCS_SECRET_ACCESS_KEY_DAILY_CONNECTOR_TESTS, test/gcs-secret-access-key
|
||||
CONFLUENCE_ACCESS_TOKEN, test/confluence-access-token
|
||||
CONFLUENCE_ACCESS_TOKEN_SCOPED, test/confluence-access-token-scoped
|
||||
JIRA_BASE_URL, test/jira-base-url
|
||||
JIRA_USER_EMAIL, test/jira-user-email
|
||||
JIRA_API_TOKEN, test/jira-api-token
|
||||
JIRA_API_TOKEN_SCOPED, test/jira-api-token-scoped
|
||||
GONG_ACCESS_KEY, test/gong-access-key
|
||||
GONG_ACCESS_KEY_SECRET, test/gong-access-key-secret
|
||||
GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON_STR, test/google-drive-service-account-json
|
||||
GOOGLE_DRIVE_OAUTH_CREDENTIALS_JSON_STR_TEST_USER_1, test/google-drive-oauth-creds-test-user-1
|
||||
GOOGLE_DRIVE_OAUTH_CREDENTIALS_JSON_STR, test/google-drive-oauth-creds
|
||||
GOOGLE_GMAIL_SERVICE_ACCOUNT_JSON_STR, test/google-gmail-service-account-json
|
||||
GOOGLE_GMAIL_OAUTH_CREDENTIALS_JSON_STR, test/google-gmail-oauth-creds
|
||||
SLAB_BOT_TOKEN, test/slab-bot-token
|
||||
ZENDESK_SUBDOMAIN, test/zendesk-subdomain
|
||||
ZENDESK_EMAIL, test/zendesk-email
|
||||
ZENDESK_TOKEN, test/zendesk-token
|
||||
SF_PASSWORD, test/sf-password
|
||||
SF_SECURITY_TOKEN, test/sf-security-token
|
||||
HUBSPOT_ACCESS_TOKEN, test/hubspot-access-token
|
||||
IMAP_PASSWORD, test/imap-password
|
||||
AIRTABLE_ACCESS_TOKEN, test/airtable-access-token
|
||||
SHAREPOINT_CLIENT_SECRET, test/sharepoint-client-secret
|
||||
PERM_SYNC_SHAREPOINT_CLIENT_ID, test/perm-sync-sharepoint-client-id
|
||||
PERM_SYNC_SHAREPOINT_PRIVATE_KEY, test/perm-sync-sharepoint-private-key
|
||||
PERM_SYNC_SHAREPOINT_CERTIFICATE_PASSWORD, test/perm-sync-sharepoint-cert-password
|
||||
PERM_SYNC_SHAREPOINT_DIRECTORY_ID, test/perm-sync-sharepoint-directory-id
|
||||
ACCESS_TOKEN_GITHUB, test/github-access-token
|
||||
GITLAB_ACCESS_TOKEN, test/gitlab-access-token
|
||||
GITBOOK_SPACE_ID, test/gitbook-space-id
|
||||
GITBOOK_API_KEY, test/gitbook-api-key
|
||||
NOTION_INTEGRATION_TOKEN, test/notion-integration-token
|
||||
HIGHSPOT_KEY, test/highspot-key
|
||||
HIGHSPOT_SECRET, test/highspot-secret
|
||||
SLACK_BOT_TOKEN, test/slack-bot-token
|
||||
DISCORD_CONNECTOR_BOT_TOKEN, test/discord-bot-token
|
||||
TEAMS_APPLICATION_ID, test/teams-application-id
|
||||
TEAMS_DIRECTORY_ID, test/teams-directory-id
|
||||
TEAMS_SECRET, test/teams-secret
|
||||
BITBUCKET_WORKSPACE, test/bitbucket-workspace
|
||||
BITBUCKET_API_TOKEN, test/bitbucket-api-token
|
||||
FIREFLIES_API_KEY, test/fireflies-api-key
|
||||
|
||||
- name: Run Tests (excluding HubSpot, Salesforce, GitHub, and Coda)
|
||||
shell: script -q -e -c "bash --noprofile --norc -eo pipefail {0}"
|
||||
run: |
|
||||
|
||||
1
.github/workflows/preview.yml
vendored
1
.github/workflows/preview.yml
vendored
@@ -15,6 +15,7 @@ permissions:
|
||||
jobs:
|
||||
Deploy-Preview:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ci-protected
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
|
||||
@@ -6,7 +6,7 @@ Use explicit type annotations for variables to enhance code clarity, especially
|
||||
|
||||
## Best Practices
|
||||
|
||||
Use the "Engineering Best Practices" section of `CONTRIBUTING.md` as core review context. Prefer consistency with existing patterns, fix issues in code you touch, avoid tacking new features onto muddy interfaces, fail loudly instead of silently swallowing errors, keep code strictly typed, preserve clear state boundaries, remove duplicate or dead logic, break up overly long functions, avoid hidden import-time side effects, respect module boundaries, and favor correctness-by-construction over relying on callers to use an API correctly.
|
||||
Use `contributing_guides/best_practices.md` as core review context. Prefer consistency with existing patterns, fix issues in code you touch, avoid tacking new features onto muddy interfaces, fail loudly instead of silently swallowing errors, keep code strictly typed, preserve clear state boundaries, remove duplicate or dead logic, break up overly long functions, avoid hidden import-time side effects, respect module boundaries, and favor correctness-by-construction over relying on callers to use an API correctly.
|
||||
|
||||
## TODOs
|
||||
|
||||
@@ -27,7 +27,6 @@ Code changes must consider both multi-tenant and single-tenant deployments. In m
|
||||
## Nginx Routing — New Backend Routes
|
||||
|
||||
Whenever a new backend route is added that does NOT start with `/api`, it must also be explicitly added to ALL nginx configs:
|
||||
|
||||
- `deployment/helm/charts/onyx/templates/nginx-conf.yaml` (Helm/k8s)
|
||||
- `deployment/data/nginx/app.conf.template` (docker-compose dev)
|
||||
- `deployment/data/nginx/app.conf.template.prod` (docker-compose prod)
|
||||
@@ -38,7 +37,3 @@ Routes not starting with `/api` are not caught by the existing `^/(api|openapi\.
|
||||
## Full vs Lite Deployments
|
||||
|
||||
Code changes must consider both regular Onyx deployments and Onyx lite deployments. Lite deployments disable the vector DB, Redis, model servers, and background workers by default, use PostgreSQL-backed cache/auth/file storage, and rely on the API server to handle background work. Do not assume those services are available unless the code path is explicitly limited to full deployments.
|
||||
|
||||
## SWR Cache Keys — Always Use SWR_KEYS Registry
|
||||
|
||||
All `useSWR()` calls and `mutate()` calls in the frontend must reference the centralized `SWR_KEYS` registry in `web/src/lib/swr-keys.ts` instead of inline endpoint strings or local string constants. Never write `useSWR("/api/some/endpoint", ...)` or `mutate("/api/some/endpoint")` — always use the corresponding `SWR_KEYS.someEndpoint` constant. If the endpoint does not yet exist in the registry, add it there first. This applies to all variants of an endpoint (e.g. query-string variants like `?get_editable=true` must also be registered as their own key).
|
||||
|
||||
@@ -357,5 +357,5 @@ raise OnyxError(OnyxErrorCode.BAD_GATEWAY, detail, status_code_override=e.respon
|
||||
## Best Practices
|
||||
|
||||
In addition to the other content in this file, best practices for contributing
|
||||
to the codebase can be found in the "Engineering Best Practices" section of
|
||||
`CONTRIBUTING.md`. Understand its contents and follow them.
|
||||
to the codebase can be found at `contributing_guides/best_practices.md`.
|
||||
Understand its contents and follow them.
|
||||
|
||||
481
CONTRIBUTING.md
481
CONTRIBUTING.md
@@ -1,487 +1,32 @@
|
||||
# Contributing to Onyx
|
||||
|
||||
Hey there! We are so excited that you're interested in Onyx.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Contribution Opportunities](#contribution-opportunities)
|
||||
- [Contribution Process](#contribution-process)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Backend: Python Requirements](#backend-python-requirements)
|
||||
- [Frontend: Node Dependencies](#frontend-node-dependencies)
|
||||
- [Formatting and Linting](#formatting-and-linting)
|
||||
- [Running the Application](#running-the-application)
|
||||
- [VSCode Debugger (Recommended)](#vscode-debugger-recommended)
|
||||
- [Manually Running for Development](#manually-running-for-development)
|
||||
- [Running in Docker](#running-in-docker)
|
||||
- [macOS-Specific Notes](#macos-specific-notes)
|
||||
- [Engineering Best Practices](#engineering-best-practices)
|
||||
- [Principles and Collaboration](#principles-and-collaboration)
|
||||
- [Style and Maintainability](#style-and-maintainability)
|
||||
- [Performance and Correctness](#performance-and-correctness)
|
||||
- [Repository Conventions](#repository-conventions)
|
||||
- [Release Process](#release-process)
|
||||
- [Getting Help](#getting-help)
|
||||
- [Enterprise Edition Contributions](#enterprise-edition-contributions)
|
||||
|
||||
---
|
||||
|
||||
## Contribution Opportunities
|
||||
|
||||
The [GitHub Issues](https://github.com/onyx-dot-app/onyx/issues) page is a great place to look for and share contribution ideas.
|
||||
|
||||
If you have your own feature that you would like to build, please create an issue and community members can provide feedback and upvote if they feel a common need.
|
||||
If you have your own feature that you would like to build please create an issue and community members can provide feedback and
|
||||
thumb it up if they feel a common need.
|
||||
|
||||
---
|
||||
|
||||
## Contribution Process
|
||||
## Contributing Code
|
||||
Please reference the documents in contributing_guides folder to ensure that the code base is kept to a high standard.
|
||||
1. dev_setup.md (start here): gives you a guide to setting up a local development environment.
|
||||
2. contribution_process.md: how to ensure you are building valuable features that will get reviewed and merged.
|
||||
3. best_practices.md: before asking for reviews, ensure your changes meet the repo code quality standards.
|
||||
|
||||
To contribute, please follow the
|
||||
["fork and pull request"](https://docs.github.com/en/get-started/quickstart/contributing-to-projects) workflow.
|
||||
|
||||
### 1. Get the feature or enhancement approved
|
||||
|
||||
Create a GitHub issue and see if there are upvotes. If you feel the feature is sufficiently value-additive and you would like approval to contribute it to the repo, tag [Yuhong](https://github.com/yuhongsun96) to review.
|
||||
|
||||
If you do not get a response within a week, feel free to email yuhong@onyx.app and include the issue in the message.
|
||||
|
||||
Not all small features and enhancements will be accepted as there is a balance between feature richness and bloat. We strive to provide the best user experience possible so we have to be intentional about what we include in the app.
|
||||
|
||||
### 2. Get the design approved
|
||||
|
||||
The Onyx team will either provide a design doc and PRD for the feature or request one from you, the contributor. The scope and detail of the design will depend on the individual feature.
|
||||
|
||||
### 3. IP attribution for EE contributions
|
||||
|
||||
If you are contributing features to Onyx Enterprise Edition, you are required to sign the [IP Assignment Agreement](contributor_ip_assignment/EE_Contributor_IP_Assignment_Agreement.md).
|
||||
|
||||
### 4. Review and testing
|
||||
|
||||
Your features must pass all tests and all comments must be addressed prior to merging.
|
||||
|
||||
### Implicit agreements
|
||||
|
||||
If we approve an issue, we are promising you the following:
|
||||
- Your work will receive timely attention and we will put aside other important items to ensure you are not blocked.
|
||||
- You will receive necessary coaching on eng quality, system design, etc. to ensure the feature is completed well.
|
||||
- The Onyx team will pull resources and bandwidth from design, PM, and engineering to ensure that you have all the resources to build the feature to the quality required for merging.
|
||||
|
||||
Because this is a large investment from our team, we ask that you:
|
||||
- Thoroughly read all the requirements of the design docs, engineering best practices, and try to minimize overhead for the Onyx team.
|
||||
- Complete the feature in a timely manner to reduce context switching and an ongoing resource pull from the Onyx team.
|
||||
|
||||
---
|
||||
|
||||
## Development Setup
|
||||
|
||||
Onyx being a fully functional app, relies on some external software, specifically:
|
||||
|
||||
- [Postgres](https://www.postgresql.org/) (Relational DB)
|
||||
- [OpenSearch](https://opensearch.org/) (Vector DB/Search Engine)
|
||||
- [Redis](https://redis.io/) (Cache)
|
||||
- [MinIO](https://min.io/) (File Store)
|
||||
- [Nginx](https://nginx.org/) (Not needed for development flows generally)
|
||||
|
||||
> **Note:**
|
||||
> This guide provides instructions to build and run Onyx locally from source with Docker containers providing the above external software.
|
||||
> We believe this combination is easier for development purposes. If you prefer to use pre-built container images, see [Running in Docker](#running-in-docker) below.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Python 3.11** — If using a lower version, modifications will have to be made to the code. Higher versions may have library compatibility issues.
|
||||
- **Docker** — Required for running external services (Postgres, OpenSearch, Redis, MinIO).
|
||||
- **Node.js v22** — We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage Node installations.
|
||||
|
||||
### Backend: Python Requirements
|
||||
|
||||
We use [uv](https://docs.astral.sh/uv/) and recommend creating a [virtual environment](https://docs.astral.sh/uv/pip/environments/#using-a-virtual-environment).
|
||||
|
||||
```bash
|
||||
uv venv .venv --python 3.11
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
_For Windows, activate the virtual environment using Command Prompt:_
|
||||
|
||||
```bash
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
If using PowerShell, the command slightly differs:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
Install the required Python dependencies:
|
||||
|
||||
```bash
|
||||
uv sync --all-extras
|
||||
```
|
||||
|
||||
Install Playwright for Python (headless browser required by the Web Connector):
|
||||
|
||||
```bash
|
||||
uv run playwright install
|
||||
```
|
||||
|
||||
### Frontend: Node Dependencies
|
||||
|
||||
```bash
|
||||
nvm install 22 && nvm use 22
|
||||
node -v # verify your active version
|
||||
```
|
||||
|
||||
Navigate to `onyx/web` and run:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
```
|
||||
|
||||
### Formatting and Linting
|
||||
|
||||
#### Backend
|
||||
|
||||
Set up pre-commit hooks (black / reorder-python-imports):
|
||||
|
||||
```bash
|
||||
uv run pre-commit install
|
||||
```
|
||||
|
||||
We also use `mypy` for static type checking. Onyx is fully type-annotated, and we want to keep it that way! To run the mypy checks manually:
|
||||
|
||||
```bash
|
||||
uv run mypy . # from onyx/backend
|
||||
```
|
||||
|
||||
#### Frontend
|
||||
|
||||
We use `prettier` for formatting. The desired version will be installed via `npm i` from the `onyx/web` directory. To run the formatter:
|
||||
|
||||
```bash
|
||||
npx prettier --write . # from onyx/web
|
||||
```
|
||||
|
||||
Pre-commit will also run prettier automatically on files you've recently touched. If re-formatted, your commit will fail. Re-stage your changes and commit again.
|
||||
|
||||
---
|
||||
|
||||
## Running the Application
|
||||
|
||||
### VSCode Debugger (Recommended)
|
||||
|
||||
We highly recommend using VSCode's debugger for development.
|
||||
|
||||
#### Initial Setup
|
||||
|
||||
1. Copy `.vscode/env_template.txt` to `.vscode/.env`
|
||||
2. Fill in the necessary environment variables in `.vscode/.env`
|
||||
|
||||
#### Using the Debugger
|
||||
|
||||
Before starting, make sure the Docker Daemon is running.
|
||||
|
||||
1. Open the Debug view in VSCode (Cmd+Shift+D on macOS)
|
||||
2. From the dropdown at the top, select "Clear and Restart External Volumes and Containers" and press the green play button
|
||||
3. From the dropdown at the top, select "Run All Onyx Services" and press the green play button
|
||||
4. Navigate to http://localhost:3000 in your browser to start using the app
|
||||
5. Set breakpoints by clicking to the left of line numbers to help debug while the app is running
|
||||
6. Use the debug toolbar to step through code, inspect variables, etc.
|
||||
|
||||
> **Note:** "Clear and Restart External Volumes and Containers" will reset your Postgres and OpenSearch (relational-db and index). Only run this if you are okay with wiping your data.
|
||||
|
||||
**Features:**
|
||||
- Hot reload is enabled for the web server and API servers
|
||||
- Python debugging is configured with debugpy
|
||||
- Environment variables are loaded from `.vscode/.env`
|
||||
- Console output is organized in the integrated terminal with labeled tabs
|
||||
|
||||
### Manually Running for Development
|
||||
|
||||
#### Docker containers for external software
|
||||
|
||||
You will need Docker installed to run these containers.
|
||||
|
||||
Navigate to `onyx/deployment/docker_compose`, then start up Postgres/OpenSearch/Redis/MinIO with:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d index relational_db cache minio
|
||||
```
|
||||
|
||||
(index refers to OpenSearch, relational_db refers to Postgres, and cache refers to Redis)
|
||||
|
||||
#### Running Onyx locally
|
||||
|
||||
To start the frontend, navigate to `onyx/web` and run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Next, start the model server which runs the local NLP models. Navigate to `onyx/backend` and run:
|
||||
|
||||
```bash
|
||||
uvicorn model_server.main:app --reload --port 9000
|
||||
```
|
||||
|
||||
_For Windows (for compatibility with both PowerShell and Command Prompt):_
|
||||
|
||||
```bash
|
||||
powershell -Command "uvicorn model_server.main:app --reload --port 9000"
|
||||
```
|
||||
|
||||
The first time running Onyx, you will need to run the DB migrations for Postgres. After the first time, this is no longer required unless the DB models change.
|
||||
|
||||
Navigate to `onyx/backend` and with the venv active, run:
|
||||
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
Next, start the task queue which orchestrates the background jobs. Still in `onyx/backend`, run:
|
||||
|
||||
```bash
|
||||
python ./scripts/dev_run_background_jobs.py
|
||||
```
|
||||
|
||||
To run the backend API server, navigate back to `onyx/backend` and run:
|
||||
|
||||
```bash
|
||||
AUTH_TYPE=basic uvicorn onyx.main:app --reload --port 8080
|
||||
```
|
||||
|
||||
_For Windows (for compatibility with both PowerShell and Command Prompt):_
|
||||
|
||||
```bash
|
||||
powershell -Command "
|
||||
$env:AUTH_TYPE='basic'
|
||||
uvicorn onyx.main:app --reload --port 8080
|
||||
"
|
||||
```
|
||||
|
||||
> **Note:** If you need finer logging, add the additional environment variable `LOG_LEVEL=DEBUG` to the relevant services.
|
||||
|
||||
#### Wrapping up
|
||||
|
||||
You should now have 4 servers running:
|
||||
|
||||
- Web server
|
||||
- Backend API
|
||||
- Model server
|
||||
- Background jobs
|
||||
|
||||
Now, visit http://localhost:3000 in your browser. You should see the Onyx onboarding wizard where you can connect your external LLM provider to Onyx.
|
||||
|
||||
You've successfully set up a local Onyx instance!
|
||||
|
||||
### Running in Docker
|
||||
|
||||
You can run the full Onyx application stack from pre-built images including all external software dependencies.
|
||||
|
||||
Navigate to `onyx/deployment/docker_compose` and run:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
After Docker pulls and starts these containers, navigate to http://localhost:3000 to use Onyx.
|
||||
|
||||
If you want to make changes to Onyx and run those changes in Docker, you can also build a local version of the Onyx container images that incorporates your changes:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## macOS-Specific Notes
|
||||
|
||||
### Setting up Python
|
||||
|
||||
Ensure [Homebrew](https://brew.sh/) is already set up, then install Python 3.11:
|
||||
|
||||
```bash
|
||||
brew install python@3.11
|
||||
```
|
||||
|
||||
Add Python 3.11 to your path by adding the following line to `~/.zshrc`:
|
||||
|
||||
```
|
||||
export PATH="$(brew --prefix)/opt/python@3.11/libexec/bin:$PATH"
|
||||
```
|
||||
|
||||
> **Note:** You will need to open a new terminal for the path change above to take effect.
|
||||
|
||||
### Setting up Docker
|
||||
|
||||
On macOS, you will need to install [Docker Desktop](https://www.docker.com/products/docker-desktop/) and ensure it is running before continuing with the docker commands.
|
||||
|
||||
### Formatting and Linting
|
||||
|
||||
macOS will likely require you to remove some quarantine attributes on some of the hooks for them to execute properly. After installing pre-commit, run the following command:
|
||||
|
||||
```bash
|
||||
sudo xattr -r -d com.apple.quarantine ~/.cache/pre-commit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Engineering Best Practices
|
||||
|
||||
> These are also what we adhere to as a team internally, we love to build in the open and to uplevel our community and each other through being transparent.
|
||||
|
||||
### Principles and Collaboration
|
||||
|
||||
- **Use 1-way vs 2-way doors.** For 2-way doors, move faster and iterate. For 1-way doors, be more deliberate.
|
||||
- **Consistency > being "right."** Prefer consistent patterns across the codebase. If something is truly bad, fix it everywhere.
|
||||
- **Fix what you touch (selectively).**
|
||||
- Don't feel obligated to fix every best-practice issue you notice.
|
||||
- Don't introduce new bad practices.
|
||||
- If your change touches code that violates best practices, fix it as part of the change.
|
||||
- **Don't tack features on.** When adding functionality, restructure logically as needed to avoid muddying interfaces and accumulating tech debt.
|
||||
|
||||
### Style and Maintainability
|
||||
|
||||
#### Comments and readability
|
||||
Add clear comments:
|
||||
- At logical boundaries (e.g., interfaces) so the reader doesn't need to dig 10 layers deeper.
|
||||
- Wherever assumptions are made or something non-obvious/unexpected is done.
|
||||
- For complicated flows/functions.
|
||||
- Wherever it saves time (e.g., nontrivial regex patterns).
|
||||
|
||||
#### Errors and exceptions
|
||||
- **Fail loudly** rather than silently skipping work.
|
||||
- Example: raise and let exceptions propagate instead of silently dropping a document.
|
||||
- **Don't overuse `try/except`.**
|
||||
- Put `try/except` at the correct logical level.
|
||||
- Do not mask exceptions unless it is clearly appropriate.
|
||||
|
||||
#### Typing
|
||||
- Everything should be **as strictly typed as possible**.
|
||||
- Use `cast` for annoying/loose-typed interfaces (e.g., results of `run_functions_tuples_in_parallel`).
|
||||
- Only `cast` when the type checker sees `Any` or types are too loose.
|
||||
- Prefer types that are easy to read.
|
||||
- Avoid dense types like `dict[tuple[str, str], list[list[float]]]`.
|
||||
- Prefer domain models, e.g.:
|
||||
- `EmbeddingModel(provider_name, model_name)` as a Pydantic model
|
||||
- `dict[EmbeddingModel, list[EmbeddingVector]]`
|
||||
|
||||
#### State, objects, and boundaries
|
||||
- Keep **clear logical boundaries** for state containers and objects.
|
||||
- A **config** object should never contain things like a `db_session`.
|
||||
- Avoid state containers that are overly nested, or huge + flat (use judgment).
|
||||
- Prefer **composition and functional style** over inheritance/OOP.
|
||||
- Prefer **no mutation** unless there's a strong reason.
|
||||
- State objects should be **intentional and explicit**, ideally nonmutating.
|
||||
- Use interfaces/objects to create clear separation of responsibility.
|
||||
- Prefer simplicity when there's no clear gain.
|
||||
- Avoid overcomplicated mechanisms like semaphores.
|
||||
- Prefer **hash maps (dicts)** over tree structures unless there's a strong reason.
|
||||
|
||||
#### Naming
|
||||
- Name variables carefully and intentionally.
|
||||
- Prefer long, explicit names when undecided.
|
||||
- Avoid single-character variables except for small, self-contained utilities (or not at all).
|
||||
- Keep the same object/name consistent through the call stack and within functions when reasonable.
|
||||
- Good: `for token in tokens:`
|
||||
- Bad: `for msg in tokens:` (if iterating tokens)
|
||||
- Function names should bias toward **long + descriptive** for codebase search.
|
||||
- IntelliSense can miss call sites; search works best with unique names.
|
||||
|
||||
#### Correctness by construction
|
||||
- Prefer self-contained correctness — don't rely on callers to "use it right" if you can make misuse hard.
|
||||
- Avoid redundancies: if a function takes an arg, it shouldn't also take a state object that contains that same arg.
|
||||
- No dead code (unless there's a very good reason).
|
||||
- No commented-out code in main or feature branches (unless there's a very good reason).
|
||||
- No duplicate logic:
|
||||
- Don't copy/paste into branches when shared logic can live above the conditional.
|
||||
- If you're afraid to touch the original, you don't understand it well enough.
|
||||
- LLMs often create subtle duplicate logic — review carefully and remove it.
|
||||
- Avoid "nearly identical" objects that confuse when to use which.
|
||||
- Avoid extremely long functions with chained logic:
|
||||
- Encapsulate steps into helpers for readability, even if not reused.
|
||||
- "Pythonic" multi-step expressions are OK in moderation; don't trade clarity for cleverness.
|
||||
|
||||
### Performance and Correctness
|
||||
|
||||
- Avoid holding resources for extended periods (DB sessions, locks/semaphores).
|
||||
- Validate objects on creation and right before use.
|
||||
- Connector code (data to Onyx documents):
|
||||
- Any in-memory structure that can grow without bound based on input must be periodically size-checked.
|
||||
- If a connector is OOMing (often shows up as "missing celery tasks"), this is a top thing to check retroactively.
|
||||
- Async and event loops:
|
||||
- Never introduce new async/event loop Python code, and try to make existing async code synchronous when possible if it makes sense.
|
||||
- Writing async code without 100% understanding the code and having a concrete reason to do so is likely to introduce bugs and not add any meaningful performance gains.
|
||||
|
||||
### Repository Conventions
|
||||
|
||||
#### Where code lives
|
||||
- Pydantic + data models: `models.py` files.
|
||||
- DB interface functions (excluding lazy loading): `db/` directory.
|
||||
- LLM prompts: `prompts/` directory, roughly mirroring the code layout that uses them.
|
||||
- API routes: `server/` directory.
|
||||
|
||||
#### Pydantic and modeling
|
||||
- Prefer **Pydantic** over dataclasses.
|
||||
- If absolutely required, use `allow_arbitrary_types`.
|
||||
|
||||
#### Data conventions
|
||||
- Prefer explicit `None` over sentinel empty strings (usually; depends on intent).
|
||||
- Prefer explicit identifiers: use string enums instead of integer codes.
|
||||
- Avoid magic numbers (co-location is good when necessary). **Always avoid magic strings.**
|
||||
|
||||
#### Logging
|
||||
- Log messages where they are created.
|
||||
- Don't propagate log messages around just to log them elsewhere.
|
||||
|
||||
#### Encapsulation
|
||||
- Don't use private attributes/methods/properties from other classes/modules.
|
||||
- "Private" is private — respect that boundary.
|
||||
|
||||
#### SQLAlchemy guidance
|
||||
- Lazy loading is often bad at scale, especially across multiple list relationships.
|
||||
- Be careful when accessing SQLAlchemy object attributes:
|
||||
- It can help avoid redundant DB queries,
|
||||
- but it can also fail if accessed outside an active session,
|
||||
- and lazy loading can add hidden DB dependencies to otherwise "simple" functions.
|
||||
- Reference: https://www.reddit.com/r/SQLAlchemy/comments/138f248/joinedload_vs_selectinload/
|
||||
|
||||
#### Trunk-based development and feature flags
|
||||
- **PRs should contain no more than 500 lines of real change.**
|
||||
- **Merge to main frequently.** Avoid long-lived feature branches — they create merge conflicts and integration pain.
|
||||
- **Use feature flags for incremental rollout.**
|
||||
- Large features should be merged in small, shippable increments behind a flag.
|
||||
- This allows continuous integration without exposing incomplete functionality.
|
||||
- **Keep flags short-lived.** Once a feature is fully rolled out, remove the flag and dead code paths promptly.
|
||||
- **Flag at the right level.** Prefer flagging at API/UI entry points rather than deep in business logic.
|
||||
- **Test both flag states.** Ensure the codebase works correctly with the flag on and off.
|
||||
|
||||
#### Miscellaneous
|
||||
- Any TODOs you add in the code must be accompanied by either the name/username of the owner of that TODO, or an issue number for an issue referencing that piece of work.
|
||||
- Avoid module-level logic that runs on import, which leads to import-time side effects. Essentially every piece of meaningful logic should exist within some function that has to be explicitly invoked. Acceptable exceptions may include loading environment variables or setting up loggers.
|
||||
- If you find yourself needing something like this, you may want that logic to exist in a file dedicated for manual execution (contains `if __name__ == "__main__":`) which should not be imported by anything else.
|
||||
- Do not conflate Python scripts you intend to run from the command line (contains `if __name__ == "__main__":`) with modules you intend to import from elsewhere. If for some unlikely reason they have to be the same file, any logic specific to executing the file (including imports) should be contained in the `if __name__ == "__main__":` block.
|
||||
- Generally these executable files exist in `backend/scripts/`.
|
||||
|
||||
---
|
||||
|
||||
## Release Process
|
||||
|
||||
Onyx loosely follows the SemVer versioning standard.
|
||||
A set of Docker containers will be pushed automatically to DockerHub with every tag.
|
||||
You can see the containers [here](https://hub.docker.com/search?q=onyx%2F).
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
## Getting Help 🙋
|
||||
We have support channels and generally interesting discussions on our [Discord](https://discord.gg/4NA5SbzrWb).
|
||||
|
||||
See you there!
|
||||
|
||||
---
|
||||
|
||||
## Enterprise Edition Contributions
|
||||
|
||||
If you are contributing features to Onyx Enterprise Edition (code under any `ee/` directory), you are required to sign the [IP Assignment Agreement](contributor_ip_assignment/EE_Contributor_IP_Assignment_Agreement.md) ([PDF version](contributor_ip_assignment/EE_Contributor_IP_Assignment_Agreement.pdf)).
|
||||
## Release Process
|
||||
Onyx loosely follows the SemVer versioning standard.
|
||||
Major changes are released with a "minor" version bump. Currently we use patch release versions to indicate small feature changes.
|
||||
A set of Docker containers will be pushed automatically to DockerHub with every tag.
|
||||
You can see the containers [here](https://hub.docker.com/search?q=onyx%2F).
|
||||
|
||||
102
README.md
102
README.md
@@ -4,6 +4,8 @@
|
||||
<a href="https://www.onyx.app/?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme"> <img width="50%" src="https://github.com/onyx-dot-app/onyx/blob/logo/OnyxLogoCropped.jpg?raw=true" /></a>
|
||||
</h2>
|
||||
|
||||
<p align="center">Open Source AI Platform</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/TDJ59cGV2X" target="_blank">
|
||||
<img src="https://img.shields.io/badge/discord-join-blue.svg?logo=discord&logoColor=white" alt="Discord" />
|
||||
@@ -25,94 +27,82 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# Onyx - The Open Source AI Platform
|
||||
|
||||
**[Onyx](https://www.onyx.app/?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme)** is the application layer for LLMs - bringing a feature-rich interface that can be easily hosted by anyone.
|
||||
Onyx enables LLMs through advanced capabilities like RAG, web search, code execution, file creation, deep research and more.
|
||||
**[Onyx](https://www.onyx.app/?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme)** is a feature-rich, self-hostable Chat UI that works with any LLM. It is easy to deploy and can run in a completely airgapped environment.
|
||||
|
||||
Connect your applications with over 50+ indexing based connectors provided out of the box or via MCP.
|
||||
Onyx comes loaded with advanced features like Agents, Web Search, RAG, MCP, Deep Research, Connectors to 40+ knowledge sources, and more.
|
||||
|
||||
> [!TIP]
|
||||
> Deploy with a single command:
|
||||
> Run Onyx with one command (or see deployment section below):
|
||||
> ```
|
||||
> curl -fsSL https://onyx.app/install_onyx.sh | bash
|
||||
> ```
|
||||
|
||||

|
||||
****
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
## ⭐ Features
|
||||
|
||||
- **🔍 Agentic RAG:** Get best in class search and answer quality based on hybrid index + AI Agents for information retrieval
|
||||
- Benchmark to release soon!
|
||||
- **🔬 Deep Research:** Get in depth reports with a multi-step research flow.
|
||||
- Top of [leaderboard](https://github.com/onyx-dot-app/onyx_deep_research_bench) as of Feb 2026.
|
||||
- **🤖 Custom Agents:** Build AI Agents with unique instructions, knowledge, and actions.
|
||||
- **🌍 Web Search:** Browse the web to get up to date information.
|
||||
- Supports Serper, Google PSE, Brave, SearXNG, and others.
|
||||
- Comes with an in house web crawler and support for Firecrawl/Exa.
|
||||
- **📄 Artifacts:** Generate documents, graphics, and other downloadable artifacts.
|
||||
- **▶️ Actions & MCP:** Let Onyx agents interact with external applications, comes with flexible Auth options.
|
||||
- **💻 Code Execution:** Execute code in a sandbox to analyze data, render graphs, or modify files.
|
||||
- **🎙️ Voice Mode:** Chat with Onyx via text-to-speech and speech-to-text.
|
||||
- **🤖 Custom Agents:** Build AI Agents with unique instructions, knowledge and actions.
|
||||
- **🌍 Web Search:** Browse the web with Google PSE, Exa, and Serper as well as an in-house scraper or Firecrawl.
|
||||
- **🔍 RAG:** Best in class hybrid-search + knowledge graph for uploaded files and ingested documents from connectors.
|
||||
- **🔄 Connectors:** Pull knowledge, metadata, and access information from over 40 applications.
|
||||
- **🔬 Deep Research:** Get in depth answers with an agentic multi-step search.
|
||||
- **▶️ Actions & MCP:** Give AI Agents the ability to interact with external systems.
|
||||
- **💻 Code Interpreter:** Execute code to analyze data, render graphs and create files.
|
||||
- **🎨 Image Generation:** Generate images based on user prompts.
|
||||
- **👥 Collaboration:** Chat sharing, feedback gathering, user management, usage analytics, and more.
|
||||
|
||||
Onyx supports all major LLM providers, both self-hosted (like Ollama, LiteLLM, vLLM, etc.) and proprietary (like Anthropic, OpenAI, Gemini, etc.).
|
||||
Onyx works with all LLMs (like OpenAI, Anthropic, Gemini, etc.) and self-hosted LLMs (like Ollama, vLLM, etc.)
|
||||
|
||||
To learn more - check out our [docs](https://docs.onyx.app/welcome?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme)!
|
||||
To learn more about the features, check out our [documentation](https://docs.onyx.app/welcome?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme)!
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment Modes
|
||||
|
||||
> Onyx supports deployments in Docker, Kubernetes, Helm/Terraform and provides guides for major cloud providers.
|
||||
> Detailed deployment guides found [here](https://docs.onyx.app/deployment/overview).
|
||||
## 🚀 Deployment
|
||||
Onyx supports deployments in Docker, Kubernetes, Terraform, along with guides for major cloud providers.
|
||||
|
||||
Onyx supports two separate deployment options: standard and lite.
|
||||
|
||||
#### Onyx Lite
|
||||
|
||||
The Lite mode can be thought of as a lightweight Chat UI. It requires less resources (under 1GB memory) and runs a less complex stack.
|
||||
It is great for users who want to test out Onyx quickly or for teams who are only interested in the Chat UI and Agents functionalities.
|
||||
|
||||
#### Standard Onyx
|
||||
|
||||
The complete feature set of Onyx which is recommended for serious users and larger teams. Additional components not included in Lite mode:
|
||||
- Vector + Keyword index for RAG.
|
||||
- Background containers to run job queues and workers for syncing knowledge from connectors.
|
||||
- AI model inference servers to run deep learning models used during indexing and inference.
|
||||
- Performance optimizations for large scale use via in memory cache (Redis) and blob store (MinIO).
|
||||
See guides below:
|
||||
- [Docker](https://docs.onyx.app/deployment/local/docker?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme) or [Quickstart](https://docs.onyx.app/deployment/getting_started/quickstart?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme) (best for most users)
|
||||
- [Kubernetes](https://docs.onyx.app/deployment/local/kubernetes?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme) (best for large teams)
|
||||
- [Terraform](https://docs.onyx.app/deployment/local/terraform?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme) (best for teams already using Terraform)
|
||||
- Cloud specific guides (best if specifically using [AWS EKS](https://docs.onyx.app/deployment/cloud/aws/eks?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme), [Azure VMs](https://docs.onyx.app/deployment/cloud/azure?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme), etc.)
|
||||
|
||||
> [!TIP]
|
||||
> **To try Onyx for free without deploying, visit [Onyx Cloud](https://cloud.onyx.app/signup?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme)**.
|
||||
> **To try Onyx for free without deploying, check out [Onyx Cloud](https://cloud.onyx.app/signup?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme)**.
|
||||
|
||||
---
|
||||
|
||||
## 🏢 Onyx for Enterprise
|
||||
|
||||
Onyx is built for teams of all sizes, from individual users to the largest global enterprises:
|
||||
- 👥 Collaboration: Share chats and agents with other members of your organization.
|
||||
- 🔐 Single Sign On: SSO via Google OAuth, OIDC, or SAML. Group syncing and user provisioning via SCIM.
|
||||
- 🛡️ Role Based Access Control: RBAC for sensitive resources like access to agents, actions, etc.
|
||||
- 📊 Analytics: Usage graphs broken down by teams, LLMs, or agents.
|
||||
- 🕵️ Query History: Audit usage to ensure safe adoption of AI in your organization.
|
||||
- 💻 Custom code: Run custom code to remove PII, reject sensitive queries, or to run custom analysis.
|
||||
- 🎨 Whitelabeling: Customize the look and feel of Onyx with custom naming, icons, banners, and more.
|
||||
## 🔍 Other Notable Benefits
|
||||
Onyx is built for teams of all sizes, from individual users to the largest global enterprises.
|
||||
|
||||
- **Enterprise Search**: far more than simple RAG, Onyx has custom indexing and retrieval that remains performant and accurate for scales of up to tens of millions of documents.
|
||||
- **Security**: SSO (OIDC/SAML/OAuth2), RBAC, encryption of credentials, etc.
|
||||
- **Management UI**: different user roles such as basic, curator, and admin.
|
||||
- **Document Permissioning**: mirrors user access from external apps for RAG use cases.
|
||||
|
||||
|
||||
|
||||
## 🚧 Roadmap
|
||||
To see ongoing and upcoming projects, check out our [roadmap](https://github.com/orgs/onyx-dot-app/projects/2)!
|
||||
|
||||
|
||||
|
||||
## 📚 Licensing
|
||||
|
||||
There are two editions of Onyx:
|
||||
|
||||
- Onyx Community Edition (CE) is available freely under the MIT license and covers all of the core features for Chat, RAG, Agents, and Actions.
|
||||
- Onyx Community Edition (CE) is available freely under the MIT license.
|
||||
- Onyx Enterprise Edition (EE) includes extra features that are primarily useful for larger organizations.
|
||||
|
||||
For feature details, check out [our website](https://www.onyx.app/pricing?utm_source=onyx_repo&utm_medium=github&utm_campaign=readme).
|
||||
|
||||
## 👪 Community
|
||||
|
||||
|
||||
## 👪 Community
|
||||
Join our open source community on **[Discord](https://discord.gg/TDJ59cGV2X)**!
|
||||
|
||||
## 💡 Contributing
|
||||
|
||||
|
||||
## 💡 Contributing
|
||||
Looking to contribute? Please check out the [Contribution Guide](CONTRIBUTING.md) for more details.
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Generator
|
||||
from queue import Empty
|
||||
|
||||
from onyx.chat.citation_processor import CitationMapping
|
||||
from onyx.chat.emitter import Emitter
|
||||
from onyx.context.search.models import SearchDoc
|
||||
from onyx.server.query_and_chat.placement import Placement
|
||||
from onyx.server.query_and_chat.streaming_models import OverallStop
|
||||
from onyx.server.query_and_chat.streaming_models import Packet
|
||||
from onyx.server.query_and_chat.streaming_models import PacketException
|
||||
from onyx.tools.models import ToolCallInfo
|
||||
from onyx.utils.threadpool_concurrency import run_in_background
|
||||
from onyx.utils.threadpool_concurrency import wait_on_background
|
||||
|
||||
# Type alias for search doc deduplication key
|
||||
# Simple key: just document_id (str)
|
||||
@@ -159,114 +148,3 @@ class ChatStateContainer:
|
||||
"""Thread-safe getter for emitted citations (returns a copy)."""
|
||||
with self._lock:
|
||||
return self._emitted_citations.copy()
|
||||
|
||||
|
||||
def run_chat_loop_with_state_containers(
|
||||
chat_loop_func: Callable[[Emitter, ChatStateContainer], None],
|
||||
completion_callback: Callable[[ChatStateContainer], None],
|
||||
is_connected: Callable[[], bool],
|
||||
emitter: Emitter,
|
||||
state_container: ChatStateContainer,
|
||||
) -> Generator[Packet, None]:
|
||||
"""
|
||||
Explicit wrapper function that runs a function in a background thread
|
||||
with event streaming capabilities.
|
||||
|
||||
The wrapped function should accept emitter as first arg and use it to emit
|
||||
Packet objects. This wrapper polls every 300ms to check if stop signal is set.
|
||||
|
||||
Args:
|
||||
func: The function to wrap (should accept emitter and state_container as first and second args)
|
||||
completion_callback: Callback function to call when the function completes
|
||||
emitter: Emitter instance for sending packets
|
||||
state_container: ChatStateContainer instance for accumulating state
|
||||
is_connected: Callable that returns False when stop signal is set
|
||||
|
||||
Usage:
|
||||
packets = run_chat_loop_with_state_containers(
|
||||
my_func,
|
||||
completion_callback=completion_callback,
|
||||
emitter=emitter,
|
||||
state_container=state_container,
|
||||
is_connected=check_func,
|
||||
)
|
||||
for packet in packets:
|
||||
# Process packets
|
||||
pass
|
||||
"""
|
||||
|
||||
def run_with_exception_capture() -> None:
|
||||
try:
|
||||
chat_loop_func(emitter, state_container)
|
||||
except Exception as e:
|
||||
# If execution fails, emit an exception packet
|
||||
emitter.emit(
|
||||
Packet(
|
||||
placement=Placement(turn_index=0),
|
||||
obj=PacketException(type="error", exception=e),
|
||||
)
|
||||
)
|
||||
|
||||
# Run the function in a background thread
|
||||
thread = run_in_background(run_with_exception_capture)
|
||||
|
||||
pkt: Packet | None = None
|
||||
last_turn_index = 0 # Track the highest turn_index seen for stop packet
|
||||
last_cancel_check = time.monotonic()
|
||||
cancel_check_interval = 0.3 # Check for cancellation every 300ms
|
||||
try:
|
||||
while True:
|
||||
# Poll queue with 300ms timeout for natural stop signal checking
|
||||
# the 300ms timeout is to avoid busy-waiting and to allow the stop signal to be checked regularly
|
||||
try:
|
||||
pkt = emitter.bus.get(timeout=0.3)
|
||||
except Empty:
|
||||
if not is_connected():
|
||||
# Stop signal detected
|
||||
yield Packet(
|
||||
placement=Placement(turn_index=last_turn_index + 1),
|
||||
obj=OverallStop(type="stop", stop_reason="user_cancelled"),
|
||||
)
|
||||
break
|
||||
last_cancel_check = time.monotonic()
|
||||
continue
|
||||
|
||||
if pkt is not None:
|
||||
# Track the highest turn_index for the stop packet
|
||||
if pkt.placement and pkt.placement.turn_index > last_turn_index:
|
||||
last_turn_index = pkt.placement.turn_index
|
||||
|
||||
if isinstance(pkt.obj, OverallStop):
|
||||
yield pkt
|
||||
break
|
||||
elif isinstance(pkt.obj, PacketException):
|
||||
raise pkt.obj.exception
|
||||
else:
|
||||
yield pkt
|
||||
|
||||
# Check for cancellation periodically even when packets are flowing
|
||||
# This ensures stop signal is checked during active streaming
|
||||
current_time = time.monotonic()
|
||||
if current_time - last_cancel_check >= cancel_check_interval:
|
||||
if not is_connected():
|
||||
# Stop signal detected during streaming
|
||||
yield Packet(
|
||||
placement=Placement(turn_index=last_turn_index + 1),
|
||||
obj=OverallStop(type="stop", stop_reason="user_cancelled"),
|
||||
)
|
||||
break
|
||||
last_cancel_check = current_time
|
||||
finally:
|
||||
# Wait for thread to complete on normal exit to propagate exceptions and ensure cleanup.
|
||||
# Skip waiting if user disconnected to exit quickly.
|
||||
if is_connected():
|
||||
wait_on_background(thread)
|
||||
try:
|
||||
completion_callback(state_container)
|
||||
except Exception as e:
|
||||
emitter.emit(
|
||||
Packet(
|
||||
placement=Placement(turn_index=last_turn_index + 1),
|
||||
obj=PacketException(type="error", exception=e),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
import threading
|
||||
from queue import Queue
|
||||
|
||||
from onyx.server.query_and_chat.placement import Placement
|
||||
from onyx.server.query_and_chat.streaming_models import Packet
|
||||
|
||||
|
||||
class Emitter:
|
||||
"""Use this inside tools to emit arbitrary UI progress."""
|
||||
"""Routes packets from LLM/tool execution to the ``_run_models`` drain loop.
|
||||
|
||||
def __init__(self, bus: Queue):
|
||||
self.bus = bus
|
||||
Tags every packet with ``model_index`` and places it on ``merged_queue``
|
||||
as a ``(model_idx, packet)`` tuple for ordered consumption downstream.
|
||||
|
||||
Args:
|
||||
merged_queue: Shared queue owned by ``_run_models``.
|
||||
model_idx: Index embedded in packet placements (``0`` for N=1 runs).
|
||||
drain_done: Optional event set by ``_run_models`` when the drain loop
|
||||
exits early (e.g. HTTP disconnect). When set, ``emit`` returns
|
||||
immediately so worker threads can exit fast.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
merged_queue: Queue[tuple[int, Packet | Exception | object]],
|
||||
model_idx: int = 0,
|
||||
drain_done: threading.Event | None = None,
|
||||
) -> None:
|
||||
self._model_idx = model_idx
|
||||
self._merged_queue = merged_queue
|
||||
self._drain_done = drain_done
|
||||
|
||||
def emit(self, packet: Packet) -> None:
|
||||
self.bus.put(packet) # Thread-safe
|
||||
|
||||
|
||||
def get_default_emitter() -> Emitter:
|
||||
bus: Queue[Packet] = Queue()
|
||||
emitter = Emitter(bus)
|
||||
return emitter
|
||||
if self._drain_done is not None and self._drain_done.is_set():
|
||||
return
|
||||
base = packet.placement or Placement(turn_index=0)
|
||||
tagged = Packet(
|
||||
placement=base.model_copy(update={"model_index": self._model_idx}),
|
||||
obj=packet.obj,
|
||||
)
|
||||
self._merged_queue.put((self._model_idx, tagged))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ from collections.abc import Generator
|
||||
from collections.abc import Iterator
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
from typing import Protocol
|
||||
@@ -42,9 +43,6 @@ from onyx.connectors.google_drive.file_retrieval import (
|
||||
get_all_files_in_my_drive_and_shared,
|
||||
)
|
||||
from onyx.connectors.google_drive.file_retrieval import get_external_access_for_folder
|
||||
from onyx.connectors.google_drive.file_retrieval import (
|
||||
get_files_by_web_view_links_batch,
|
||||
)
|
||||
from onyx.connectors.google_drive.file_retrieval import get_files_in_shared_drive
|
||||
from onyx.connectors.google_drive.file_retrieval import get_folder_metadata
|
||||
from onyx.connectors.google_drive.file_retrieval import get_root_folder_id
|
||||
@@ -73,13 +71,11 @@ from onyx.connectors.interfaces import CheckpointedConnectorWithPermSync
|
||||
from onyx.connectors.interfaces import CheckpointOutput
|
||||
from onyx.connectors.interfaces import GenerateSlimDocumentOutput
|
||||
from onyx.connectors.interfaces import NormalizationResult
|
||||
from onyx.connectors.interfaces import Resolver
|
||||
from onyx.connectors.interfaces import SecondsSinceUnixEpoch
|
||||
from onyx.connectors.interfaces import SlimConnectorWithPermSync
|
||||
from onyx.connectors.models import ConnectorFailure
|
||||
from onyx.connectors.models import ConnectorMissingCredentialError
|
||||
from onyx.connectors.models import Document
|
||||
from onyx.connectors.models import DocumentFailure
|
||||
from onyx.connectors.models import EntityFailure
|
||||
from onyx.connectors.models import HierarchyNode
|
||||
from onyx.connectors.models import SlimDocument
|
||||
@@ -207,9 +203,7 @@ class DriveIdStatus(Enum):
|
||||
|
||||
|
||||
class GoogleDriveConnector(
|
||||
SlimConnectorWithPermSync,
|
||||
CheckpointedConnectorWithPermSync[GoogleDriveCheckpoint],
|
||||
Resolver,
|
||||
SlimConnectorWithPermSync, CheckpointedConnectorWithPermSync[GoogleDriveCheckpoint]
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -1493,113 +1487,134 @@ class GoogleDriveConnector(
|
||||
end=end,
|
||||
)
|
||||
|
||||
def _convert_retrieved_files_to_documents(
|
||||
def _extract_docs_from_google_drive(
|
||||
self,
|
||||
drive_files_iter: Iterator[RetrievedDriveFile],
|
||||
checkpoint: GoogleDriveCheckpoint,
|
||||
start: SecondsSinceUnixEpoch | None,
|
||||
end: SecondsSinceUnixEpoch | None,
|
||||
include_permissions: bool,
|
||||
) -> Iterator[Document | ConnectorFailure | HierarchyNode]:
|
||||
"""
|
||||
Converts retrieved files to documents, yielding HierarchyNode
|
||||
objects for ancestor folders before the converted documents.
|
||||
Retrieves and converts Google Drive files to documents.
|
||||
Also yields HierarchyNode objects for ancestor folders.
|
||||
"""
|
||||
permission_sync_context = (
|
||||
PermissionSyncContext(
|
||||
primary_admin_email=self.primary_admin_email,
|
||||
google_domain=self.google_domain,
|
||||
)
|
||||
if include_permissions
|
||||
else None
|
||||
field_type = (
|
||||
DriveFileFieldType.WITH_PERMISSIONS
|
||||
if include_permissions or self.exclude_domain_link_only
|
||||
else DriveFileFieldType.STANDARD
|
||||
)
|
||||
|
||||
files_batch: list[RetrievedDriveFile] = []
|
||||
for retrieved_file in drive_files_iter:
|
||||
if self.exclude_domain_link_only and has_link_only_permission(
|
||||
retrieved_file.drive_file
|
||||
):
|
||||
continue
|
||||
if retrieved_file.error is None:
|
||||
files_batch.append(retrieved_file)
|
||||
continue
|
||||
|
||||
failure_stage = retrieved_file.completion_stage.value
|
||||
failure_message = f"retrieval failure during stage: {failure_stage},"
|
||||
failure_message += f"user: {retrieved_file.user_email},"
|
||||
failure_message += f"parent drive/folder: {retrieved_file.parent_id},"
|
||||
failure_message += f"error: {retrieved_file.error}"
|
||||
logger.error(failure_message)
|
||||
yield ConnectorFailure(
|
||||
failed_entity=EntityFailure(
|
||||
entity_id=retrieved_file.drive_file.get("id", failure_stage),
|
||||
),
|
||||
failure_message=failure_message,
|
||||
exception=retrieved_file.error,
|
||||
)
|
||||
|
||||
new_ancestors = self._get_new_ancestors_for_files(
|
||||
files=files_batch,
|
||||
seen_hierarchy_node_raw_ids=checkpoint.seen_hierarchy_node_raw_ids,
|
||||
fully_walked_hierarchy_node_raw_ids=checkpoint.fully_walked_hierarchy_node_raw_ids,
|
||||
permission_sync_context=permission_sync_context,
|
||||
add_prefix=True,
|
||||
)
|
||||
if new_ancestors:
|
||||
logger.debug(f"Yielding {len(new_ancestors)} new hierarchy nodes")
|
||||
yield from new_ancestors
|
||||
|
||||
func_with_args = [
|
||||
(
|
||||
self._convert_retrieved_file_to_document,
|
||||
(retrieved_file, permission_sync_context),
|
||||
)
|
||||
for retrieved_file in files_batch
|
||||
]
|
||||
raw_results = cast(
|
||||
list[Document | ConnectorFailure | None],
|
||||
run_functions_tuples_in_parallel(func_with_args, max_workers=8),
|
||||
)
|
||||
|
||||
results: list[Document | ConnectorFailure] = [
|
||||
r for r in raw_results if r is not None
|
||||
]
|
||||
logger.debug(f"batch has {len(results)} docs or failures")
|
||||
yield from results
|
||||
|
||||
checkpoint.retrieved_folder_and_drive_ids = self._retrieved_folder_and_drive_ids
|
||||
|
||||
def _convert_retrieved_file_to_document(
|
||||
self,
|
||||
retrieved_file: RetrievedDriveFile,
|
||||
permission_sync_context: PermissionSyncContext | None,
|
||||
) -> Document | ConnectorFailure | None:
|
||||
"""
|
||||
Converts a single retrieved file to a document.
|
||||
"""
|
||||
try:
|
||||
return convert_drive_item_to_document(
|
||||
# Build permission sync context if needed
|
||||
permission_sync_context = (
|
||||
PermissionSyncContext(
|
||||
primary_admin_email=self.primary_admin_email,
|
||||
google_domain=self.google_domain,
|
||||
)
|
||||
if include_permissions
|
||||
else None
|
||||
)
|
||||
|
||||
# Prepare a partial function with the credentials and admin email
|
||||
convert_func = partial(
|
||||
convert_drive_item_to_document,
|
||||
self.creds,
|
||||
self.allow_images,
|
||||
self.size_threshold,
|
||||
permission_sync_context,
|
||||
[retrieved_file.user_email, self.primary_admin_email]
|
||||
+ get_file_owners(retrieved_file.drive_file, self.primary_admin_email),
|
||||
retrieved_file.drive_file,
|
||||
)
|
||||
# Fetch files in batches
|
||||
batches_complete = 0
|
||||
files_batch: list[RetrievedDriveFile] = []
|
||||
|
||||
def _yield_batch(
|
||||
files_batch: list[RetrievedDriveFile],
|
||||
) -> Iterator[Document | ConnectorFailure | HierarchyNode]:
|
||||
nonlocal batches_complete
|
||||
|
||||
# First, yield any new ancestor hierarchy nodes
|
||||
new_ancestors = self._get_new_ancestors_for_files(
|
||||
files=files_batch,
|
||||
seen_hierarchy_node_raw_ids=checkpoint.seen_hierarchy_node_raw_ids,
|
||||
fully_walked_hierarchy_node_raw_ids=checkpoint.fully_walked_hierarchy_node_raw_ids,
|
||||
permission_sync_context=permission_sync_context,
|
||||
add_prefix=True, # Indexing path - prefix here
|
||||
)
|
||||
if new_ancestors:
|
||||
logger.debug(
|
||||
f"Yielding {len(new_ancestors)} new hierarchy nodes for batch {batches_complete}"
|
||||
)
|
||||
yield from new_ancestors
|
||||
|
||||
# Process the batch using run_functions_tuples_in_parallel
|
||||
func_with_args = [
|
||||
(
|
||||
convert_func,
|
||||
(
|
||||
[file.user_email, self.primary_admin_email]
|
||||
+ get_file_owners(
|
||||
file.drive_file, self.primary_admin_email
|
||||
),
|
||||
file.drive_file,
|
||||
),
|
||||
)
|
||||
for file in files_batch
|
||||
]
|
||||
results = cast(
|
||||
list[Document | ConnectorFailure | None],
|
||||
run_functions_tuples_in_parallel(func_with_args, max_workers=8),
|
||||
)
|
||||
logger.debug(
|
||||
f"finished processing batch {batches_complete} with {len(results)} results"
|
||||
)
|
||||
|
||||
docs_and_failures = [result for result in results if result is not None]
|
||||
logger.debug(
|
||||
f"batch {batches_complete} has {len(docs_and_failures)} docs or failures"
|
||||
)
|
||||
|
||||
if docs_and_failures:
|
||||
yield from docs_and_failures
|
||||
batches_complete += 1
|
||||
logger.debug(f"finished yielding batch {batches_complete}")
|
||||
|
||||
for retrieved_file in self._fetch_drive_items(
|
||||
field_type=field_type,
|
||||
checkpoint=checkpoint,
|
||||
start=start,
|
||||
end=end,
|
||||
):
|
||||
if self.exclude_domain_link_only and has_link_only_permission(
|
||||
retrieved_file.drive_file
|
||||
):
|
||||
continue
|
||||
if retrieved_file.error is None:
|
||||
files_batch.append(retrieved_file)
|
||||
continue
|
||||
|
||||
# handle retrieval errors
|
||||
failure_stage = retrieved_file.completion_stage.value
|
||||
failure_message = f"retrieval failure during stage: {failure_stage},"
|
||||
failure_message += f"user: {retrieved_file.user_email},"
|
||||
failure_message += f"parent drive/folder: {retrieved_file.parent_id},"
|
||||
failure_message += f"error: {retrieved_file.error}"
|
||||
logger.error(failure_message)
|
||||
yield ConnectorFailure(
|
||||
failed_entity=EntityFailure(
|
||||
entity_id=failure_stage,
|
||||
),
|
||||
failure_message=failure_message,
|
||||
exception=retrieved_file.error,
|
||||
)
|
||||
|
||||
yield from _yield_batch(files_batch)
|
||||
checkpoint.retrieved_folder_and_drive_ids = (
|
||||
self._retrieved_folder_and_drive_ids
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Error extracting document: "
|
||||
f"{retrieved_file.drive_file.get('name')} from Google Drive"
|
||||
)
|
||||
return ConnectorFailure(
|
||||
failed_entity=EntityFailure(
|
||||
entity_id=retrieved_file.drive_file.get("id", "unknown"),
|
||||
),
|
||||
failure_message=(
|
||||
f"Error extracting document: "
|
||||
f"{retrieved_file.drive_file.get('name')}"
|
||||
),
|
||||
exception=e,
|
||||
)
|
||||
logger.exception(f"Error extracting documents from Google Drive: {e}")
|
||||
raise e
|
||||
|
||||
def _load_from_checkpoint(
|
||||
self,
|
||||
@@ -1623,19 +1638,8 @@ class GoogleDriveConnector(
|
||||
checkpoint = copy.deepcopy(checkpoint)
|
||||
self._retrieved_folder_and_drive_ids = checkpoint.retrieved_folder_and_drive_ids
|
||||
try:
|
||||
field_type = (
|
||||
DriveFileFieldType.WITH_PERMISSIONS
|
||||
if include_permissions or self.exclude_domain_link_only
|
||||
else DriveFileFieldType.STANDARD
|
||||
)
|
||||
drive_files_iter = self._fetch_drive_items(
|
||||
field_type=field_type,
|
||||
checkpoint=checkpoint,
|
||||
start=start,
|
||||
end=end,
|
||||
)
|
||||
yield from self._convert_retrieved_files_to_documents(
|
||||
drive_files_iter, checkpoint, include_permissions
|
||||
yield from self._extract_docs_from_google_drive(
|
||||
checkpoint, start, end, include_permissions
|
||||
)
|
||||
except Exception as e:
|
||||
if MISSING_SCOPES_ERROR_STR in str(e):
|
||||
@@ -1672,82 +1676,6 @@ class GoogleDriveConnector(
|
||||
start, end, checkpoint, include_permissions=True
|
||||
)
|
||||
|
||||
@override
|
||||
def resolve_errors(
|
||||
self,
|
||||
errors: list[ConnectorFailure],
|
||||
include_permissions: bool = False,
|
||||
) -> Generator[Document | ConnectorFailure | HierarchyNode, None, None]:
|
||||
if self._creds is None or self._primary_admin_email is None:
|
||||
raise RuntimeError(
|
||||
"Credentials missing, should not call this method before calling load_credentials"
|
||||
)
|
||||
|
||||
logger.info(f"Resolving {len(errors)} errors")
|
||||
doc_ids = [
|
||||
failure.failed_document.document_id
|
||||
for failure in errors
|
||||
if failure.failed_document
|
||||
]
|
||||
service = get_drive_service(self.creds, self.primary_admin_email)
|
||||
field_type = (
|
||||
DriveFileFieldType.WITH_PERMISSIONS
|
||||
if include_permissions or self.exclude_domain_link_only
|
||||
else DriveFileFieldType.STANDARD
|
||||
)
|
||||
batch_result = get_files_by_web_view_links_batch(service, doc_ids, field_type)
|
||||
|
||||
for doc_id, error in batch_result.errors.items():
|
||||
yield ConnectorFailure(
|
||||
failed_document=DocumentFailure(
|
||||
document_id=doc_id,
|
||||
document_link=doc_id,
|
||||
),
|
||||
failure_message=f"Failed to retrieve file during error resolution: {error}",
|
||||
exception=error,
|
||||
)
|
||||
|
||||
permission_sync_context = (
|
||||
PermissionSyncContext(
|
||||
primary_admin_email=self.primary_admin_email,
|
||||
google_domain=self.google_domain,
|
||||
)
|
||||
if include_permissions
|
||||
else None
|
||||
)
|
||||
|
||||
retrieved_files = [
|
||||
RetrievedDriveFile(
|
||||
drive_file=file,
|
||||
user_email=self.primary_admin_email,
|
||||
completion_stage=DriveRetrievalStage.DONE,
|
||||
)
|
||||
for file in batch_result.files.values()
|
||||
]
|
||||
|
||||
yield from self._get_new_ancestors_for_files(
|
||||
files=retrieved_files,
|
||||
seen_hierarchy_node_raw_ids=ThreadSafeSet(),
|
||||
fully_walked_hierarchy_node_raw_ids=ThreadSafeSet(),
|
||||
permission_sync_context=permission_sync_context,
|
||||
add_prefix=True,
|
||||
)
|
||||
|
||||
func_with_args = [
|
||||
(
|
||||
self._convert_retrieved_file_to_document,
|
||||
(rf, permission_sync_context),
|
||||
)
|
||||
for rf in retrieved_files
|
||||
]
|
||||
results = cast(
|
||||
list[Document | ConnectorFailure | None],
|
||||
run_functions_tuples_in_parallel(func_with_args, max_workers=8),
|
||||
)
|
||||
for result in results:
|
||||
if result is not None:
|
||||
yield result
|
||||
|
||||
def _extract_slim_docs_from_google_drive(
|
||||
self,
|
||||
checkpoint: GoogleDriveCheckpoint,
|
||||
|
||||
@@ -4,12 +4,9 @@ from datetime import datetime
|
||||
from datetime import timezone
|
||||
from enum import Enum
|
||||
from typing import cast
|
||||
from urllib.parse import parse_qs
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from googleapiclient.discovery import Resource # type: ignore
|
||||
from googleapiclient.errors import HttpError # type: ignore
|
||||
from googleapiclient.http import BatchHttpRequest # type: ignore
|
||||
|
||||
from onyx.access.models import ExternalAccess
|
||||
from onyx.connectors.google_drive.constants import DRIVE_FOLDER_TYPE
|
||||
@@ -61,8 +58,6 @@ SLIM_FILE_FIELDS = (
|
||||
)
|
||||
FOLDER_FIELDS = "nextPageToken, files(id, name, permissions, modifiedTime, webViewLink, shortcutDetails)"
|
||||
|
||||
MAX_BATCH_SIZE = 100
|
||||
|
||||
HIERARCHY_FIELDS = "id, name, parents, webViewLink, mimeType, driveId"
|
||||
|
||||
HIERARCHY_FIELDS_WITH_PERMISSIONS = (
|
||||
@@ -219,7 +214,7 @@ def get_external_access_for_folder(
|
||||
|
||||
|
||||
def _get_fields_for_file_type(field_type: DriveFileFieldType) -> str:
|
||||
"""Get the appropriate fields string for files().list() based on the field type enum."""
|
||||
"""Get the appropriate fields string based on the field type enum"""
|
||||
if field_type == DriveFileFieldType.SLIM:
|
||||
return SLIM_FILE_FIELDS
|
||||
elif field_type == DriveFileFieldType.WITH_PERMISSIONS:
|
||||
@@ -228,25 +223,6 @@ def _get_fields_for_file_type(field_type: DriveFileFieldType) -> str:
|
||||
return FILE_FIELDS
|
||||
|
||||
|
||||
def _extract_single_file_fields(list_fields: str) -> str:
|
||||
"""Convert a files().list() fields string to one suitable for files().get().
|
||||
|
||||
List fields look like "nextPageToken, files(field1, field2, ...)"
|
||||
Single-file fields should be just "field1, field2, ..."
|
||||
"""
|
||||
start = list_fields.find("files(")
|
||||
if start == -1:
|
||||
return list_fields
|
||||
inner_start = start + len("files(")
|
||||
inner_end = list_fields.rfind(")")
|
||||
return list_fields[inner_start:inner_end]
|
||||
|
||||
|
||||
def _get_single_file_fields(field_type: DriveFileFieldType) -> str:
|
||||
"""Get the appropriate fields string for files().get() based on the field type enum."""
|
||||
return _extract_single_file_fields(_get_fields_for_file_type(field_type))
|
||||
|
||||
|
||||
def _get_files_in_parent(
|
||||
service: Resource,
|
||||
parent_id: str,
|
||||
@@ -520,112 +496,3 @@ def get_root_folder_id(service: Resource) -> str:
|
||||
.get(fileId="root", fields=GoogleFields.ID.value)
|
||||
.execute()[GoogleFields.ID.value]
|
||||
)
|
||||
|
||||
|
||||
def _extract_file_id_from_web_view_link(web_view_link: str) -> str:
|
||||
parsed = urlparse(web_view_link)
|
||||
path_parts = [part for part in parsed.path.split("/") if part]
|
||||
|
||||
if "d" in path_parts:
|
||||
idx = path_parts.index("d")
|
||||
if idx + 1 < len(path_parts):
|
||||
return path_parts[idx + 1]
|
||||
|
||||
query_params = parse_qs(parsed.query)
|
||||
for key in ("id", "fileId"):
|
||||
value = query_params.get(key)
|
||||
if value and value[0]:
|
||||
return value[0]
|
||||
|
||||
raise ValueError(
|
||||
f"Unable to extract Drive file id from webViewLink: {web_view_link}"
|
||||
)
|
||||
|
||||
|
||||
def get_file_by_web_view_link(
|
||||
service: GoogleDriveService,
|
||||
web_view_link: str,
|
||||
fields: str,
|
||||
) -> GoogleDriveFileType:
|
||||
"""Retrieve a Google Drive file using its webViewLink."""
|
||||
file_id = _extract_file_id_from_web_view_link(web_view_link)
|
||||
return (
|
||||
service.files()
|
||||
.get(
|
||||
fileId=file_id,
|
||||
supportsAllDrives=True,
|
||||
fields=fields,
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
|
||||
class BatchRetrievalResult:
|
||||
"""Result of a batch file retrieval, separating successes from errors."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.files: dict[str, GoogleDriveFileType] = {}
|
||||
self.errors: dict[str, Exception] = {}
|
||||
|
||||
|
||||
def get_files_by_web_view_links_batch(
|
||||
service: GoogleDriveService,
|
||||
web_view_links: list[str],
|
||||
field_type: DriveFileFieldType,
|
||||
) -> BatchRetrievalResult:
|
||||
"""Retrieve multiple Google Drive files by webViewLink using the batch API.
|
||||
|
||||
Returns a BatchRetrievalResult containing successful file retrievals
|
||||
and errors for any files that could not be fetched.
|
||||
Automatically splits into chunks of MAX_BATCH_SIZE.
|
||||
"""
|
||||
fields = _get_single_file_fields(field_type)
|
||||
if len(web_view_links) <= MAX_BATCH_SIZE:
|
||||
return _get_files_by_web_view_links_batch(service, web_view_links, fields)
|
||||
|
||||
combined = BatchRetrievalResult()
|
||||
for i in range(0, len(web_view_links), MAX_BATCH_SIZE):
|
||||
chunk = web_view_links[i : i + MAX_BATCH_SIZE]
|
||||
chunk_result = _get_files_by_web_view_links_batch(service, chunk, fields)
|
||||
combined.files.update(chunk_result.files)
|
||||
combined.errors.update(chunk_result.errors)
|
||||
return combined
|
||||
|
||||
|
||||
def _get_files_by_web_view_links_batch(
|
||||
service: GoogleDriveService,
|
||||
web_view_links: list[str],
|
||||
fields: str,
|
||||
) -> BatchRetrievalResult:
|
||||
"""Single-batch implementation."""
|
||||
|
||||
result = BatchRetrievalResult()
|
||||
|
||||
def callback(
|
||||
request_id: str,
|
||||
response: GoogleDriveFileType,
|
||||
exception: Exception | None,
|
||||
) -> None:
|
||||
if exception:
|
||||
logger.warning(f"Error retrieving file {request_id}: {exception}")
|
||||
result.errors[request_id] = exception
|
||||
else:
|
||||
result.files[request_id] = response
|
||||
|
||||
batch = cast(BatchHttpRequest, service.new_batch_http_request(callback=callback))
|
||||
|
||||
for web_view_link in web_view_links:
|
||||
try:
|
||||
file_id = _extract_file_id_from_web_view_link(web_view_link)
|
||||
request = service.files().get(
|
||||
fileId=file_id,
|
||||
supportsAllDrives=True,
|
||||
fields=fields,
|
||||
)
|
||||
batch.add(request, request_id=web_view_link)
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to extract file ID from {web_view_link}: {e}")
|
||||
result.errors[web_view_link] = e
|
||||
|
||||
batch.execute()
|
||||
return result
|
||||
|
||||
@@ -298,22 +298,6 @@ class CheckpointedConnectorWithPermSync(CheckpointedConnector[CT]):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Resolver(BaseConnector):
|
||||
@abc.abstractmethod
|
||||
def resolve_errors(
|
||||
self,
|
||||
errors: list[ConnectorFailure],
|
||||
include_permissions: bool = False,
|
||||
) -> Generator[Document | ConnectorFailure | HierarchyNode, None, None]:
|
||||
"""Attempts to yield back ALL the documents described by the errors, no checkpointing.
|
||||
|
||||
Caller's responsibility is to delete the old ConnectorFailures and replace with the new ones.
|
||||
If include_permissions is True, the documents will have permissions synced.
|
||||
May also yield HierarchyNode objects for ancestor folders of resolved documents.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HierarchyConnector(BaseConnector):
|
||||
@abc.abstractmethod
|
||||
def load_hierarchy(
|
||||
|
||||
@@ -8,6 +8,24 @@ from pydantic import BaseModel
|
||||
|
||||
|
||||
class LLMOverride(BaseModel):
|
||||
"""Per-request LLM settings that override persona defaults.
|
||||
|
||||
All fields are optional — only the fields that differ from the persona's
|
||||
configured LLM need to be supplied. Used both over the wire (API requests)
|
||||
and for multi-model comparison, where one override is supplied per model.
|
||||
|
||||
Attributes:
|
||||
model_provider: LLM provider slug (e.g. ``"openai"``, ``"anthropic"``).
|
||||
When ``None``, the persona's default provider is used.
|
||||
model_version: Specific model version string (e.g. ``"gpt-4o"``).
|
||||
When ``None``, the persona's default model is used.
|
||||
temperature: Sampling temperature in ``[0, 2]``. When ``None``, the
|
||||
persona's default temperature is used.
|
||||
display_name: Human-readable label shown in the UI for this model,
|
||||
e.g. ``"GPT-4 Turbo"``. Optional; falls back to ``model_version``
|
||||
when not set.
|
||||
"""
|
||||
|
||||
model_provider: str | None = None
|
||||
model_version: str | None = None
|
||||
temperature: float | None = None
|
||||
|
||||
@@ -2,11 +2,25 @@ from pydantic import BaseModel
|
||||
|
||||
|
||||
class Placement(BaseModel):
|
||||
# Which iterative block in the UI is this part of, these are ordered and smaller ones happened first
|
||||
"""Coordinates that identify where a streaming packet belongs in the UI.
|
||||
|
||||
The frontend uses these fields to route each packet to the correct turn,
|
||||
tool tab, agent sub-turn, and (in multi-model mode) response column.
|
||||
|
||||
Attributes:
|
||||
turn_index: Monotonically increasing index of the iterative reasoning block
|
||||
(e.g. tool call round) within this chat message. Lower values happened first.
|
||||
tab_index: Disambiguates parallel tool calls within the same turn so each
|
||||
tool's output can be displayed in its own tab.
|
||||
sub_turn_index: Nesting level for tools that invoke other tools. ``None`` for
|
||||
top-level packets; an integer for tool-within-tool output.
|
||||
model_index: Which model this packet belongs to. ``0`` for single-model
|
||||
responses; ``0``, ``1``, or ``2`` for multi-model comparison. ``None``
|
||||
for pre-LLM setup packets (e.g. message ID info) that are yielded
|
||||
before any Emitter runs.
|
||||
"""
|
||||
|
||||
turn_index: int
|
||||
# For parallel tool calls to preserve order of execution
|
||||
tab_index: int = 0
|
||||
# Used for tools/agents that call other tools, this currently doesn't support nested agents but can be added later
|
||||
sub_turn_index: int | None = None
|
||||
# For multi-model streaming: identifies which model (0, 1, 2) this packet belongs to.
|
||||
model_index: int | None = None
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import queue
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
@@ -708,7 +709,6 @@ def run_research_agent_calls(
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from queue import Queue
|
||||
from uuid import uuid4
|
||||
|
||||
from onyx.chat.chat_state import ChatStateContainer
|
||||
@@ -744,8 +744,8 @@ if __name__ == "__main__":
|
||||
if user is None:
|
||||
raise ValueError("No users found in database. Please create a user first.")
|
||||
|
||||
bus: Queue[Packet] = Queue()
|
||||
emitter = Emitter(bus)
|
||||
emitter_queue: queue.Queue = queue.Queue()
|
||||
emitter = Emitter(merged_queue=emitter_queue)
|
||||
state_container = ChatStateContainer()
|
||||
|
||||
tool_dict = construct_tools(
|
||||
@@ -792,4 +792,4 @@ if __name__ == "__main__":
|
||||
print(result.intermediate_report)
|
||||
print("=" * 80)
|
||||
print(f"Citations: {result.citation_mapping}")
|
||||
print(f"Total packets emitted: {bus.qsize()}")
|
||||
print(f"Total packets emitted: {emitter_queue.qsize()}")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import csv
|
||||
import json
|
||||
import queue
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
from io import StringIO
|
||||
@@ -11,7 +12,6 @@ import requests
|
||||
from requests import JSONDecodeError
|
||||
|
||||
from onyx.chat.emitter import Emitter
|
||||
from onyx.chat.emitter import get_default_emitter
|
||||
from onyx.configs.constants import FileOrigin
|
||||
from onyx.file_store.file_store import get_default_file_store
|
||||
from onyx.server.query_and_chat.placement import Placement
|
||||
@@ -296,9 +296,9 @@ def build_custom_tools_from_openapi_schema_and_headers(
|
||||
url = openapi_to_url(openapi_schema)
|
||||
method_specs = openapi_to_method_specs(openapi_schema)
|
||||
|
||||
# Use default emitter if none provided
|
||||
# Use a discard emitter if none provided (packets go nowhere)
|
||||
if emitter is None:
|
||||
emitter = get_default_emitter()
|
||||
emitter = Emitter(merged_queue=queue.Queue())
|
||||
|
||||
return [
|
||||
CustomTool(
|
||||
@@ -367,7 +367,7 @@ if __name__ == "__main__":
|
||||
tools = build_custom_tools_from_openapi_schema_and_headers(
|
||||
tool_id=0, # dummy tool id
|
||||
openapi_schema=openapi_schema,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
dynamic_schema_info=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ aiofiles==25.1.0
|
||||
# unstructured-client
|
||||
aiohappyeyeballs==2.6.1
|
||||
# via aiohttp
|
||||
aiohttp==3.13.4
|
||||
aiohttp==3.13.3
|
||||
# via
|
||||
# aiobotocore
|
||||
# discord-py
|
||||
@@ -271,7 +271,7 @@ fastapi-users-db-sqlalchemy==7.0.0
|
||||
# via onyx
|
||||
fastavro==1.12.1
|
||||
# via cohere
|
||||
fastmcp==3.2.0
|
||||
fastmcp==3.0.2
|
||||
# via onyx
|
||||
fastuuid==0.14.0
|
||||
# via litellm
|
||||
@@ -1102,8 +1102,6 @@ tzdata==2025.2
|
||||
# tzlocal
|
||||
tzlocal==5.3.1
|
||||
# via dateparser
|
||||
uncalled-for==0.2.0
|
||||
# via fastmcp
|
||||
unstructured==0.18.27
|
||||
# via onyx
|
||||
unstructured-client==0.42.6
|
||||
|
||||
@@ -10,7 +10,7 @@ aiofiles==25.1.0
|
||||
# via aioboto3
|
||||
aiohappyeyeballs==2.6.1
|
||||
# via aiohttp
|
||||
aiohttp==3.13.4
|
||||
aiohttp==3.13.3
|
||||
# via
|
||||
# aiobotocore
|
||||
# discord-py
|
||||
|
||||
@@ -10,7 +10,7 @@ aiofiles==25.1.0
|
||||
# via aioboto3
|
||||
aiohappyeyeballs==2.6.1
|
||||
# via aiohttp
|
||||
aiohttp==3.13.4
|
||||
aiohttp==3.13.3
|
||||
# via
|
||||
# aiobotocore
|
||||
# discord-py
|
||||
|
||||
@@ -12,7 +12,7 @@ aiofiles==25.1.0
|
||||
# via aioboto3
|
||||
aiohappyeyeballs==2.6.1
|
||||
# via aiohttp
|
||||
aiohttp==3.13.4
|
||||
aiohttp==3.13.3
|
||||
# via
|
||||
# aiobotocore
|
||||
# discord-py
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from onyx.connectors.google_drive.connector import GoogleDriveConnector
|
||||
from onyx.connectors.google_drive.file_retrieval import DriveFileFieldType
|
||||
from onyx.connectors.google_drive.file_retrieval import has_link_only_permission
|
||||
from onyx.connectors.google_drive.models import DriveRetrievalStage
|
||||
from onyx.connectors.google_drive.models import RetrievedDriveFile
|
||||
@@ -73,8 +75,10 @@ def test_connector_skips_link_only_files_when_enabled() -> None:
|
||||
retrieved_file = _build_retrieved_file(
|
||||
[{"type": "domain", "allowFileDiscovery": False}]
|
||||
)
|
||||
fetch_mock = MagicMock(return_value=iter([retrieved_file]))
|
||||
|
||||
with (
|
||||
patch.object(connector, "_fetch_drive_items", fetch_mock),
|
||||
patch(
|
||||
"onyx.connectors.google_drive.connector.run_functions_tuples_in_parallel",
|
||||
side_effect=_stub_run_functions,
|
||||
@@ -89,16 +93,21 @@ def test_connector_skips_link_only_files_when_enabled() -> None:
|
||||
convert_mock.return_value = "doc"
|
||||
checkpoint = connector.build_dummy_checkpoint()
|
||||
results = list(
|
||||
connector._convert_retrieved_files_to_documents(
|
||||
drive_files_iter=iter([retrieved_file]),
|
||||
connector._extract_docs_from_google_drive(
|
||||
checkpoint=checkpoint,
|
||||
start=None,
|
||||
end=None,
|
||||
include_permissions=False,
|
||||
)
|
||||
)
|
||||
|
||||
assert results == []
|
||||
convert_mock.assert_not_called()
|
||||
fetch_mock.assert_called_once()
|
||||
get_new_ancestors_mock.assert_called_once()
|
||||
assert (
|
||||
fetch_mock.call_args.kwargs["field_type"] == DriveFileFieldType.WITH_PERMISSIONS
|
||||
)
|
||||
|
||||
|
||||
def test_connector_processes_files_when_option_disabled() -> None:
|
||||
@@ -106,8 +115,10 @@ def test_connector_processes_files_when_option_disabled() -> None:
|
||||
retrieved_file = _build_retrieved_file(
|
||||
[{"type": "domain", "allowFileDiscovery": False}]
|
||||
)
|
||||
fetch_mock = MagicMock(return_value=iter([retrieved_file]))
|
||||
|
||||
with (
|
||||
patch.object(connector, "_fetch_drive_items", fetch_mock),
|
||||
patch(
|
||||
"onyx.connectors.google_drive.connector.run_functions_tuples_in_parallel",
|
||||
side_effect=_stub_run_functions,
|
||||
@@ -122,13 +133,16 @@ def test_connector_processes_files_when_option_disabled() -> None:
|
||||
convert_mock.return_value = "doc"
|
||||
checkpoint = connector.build_dummy_checkpoint()
|
||||
results = list(
|
||||
connector._convert_retrieved_files_to_documents(
|
||||
drive_files_iter=iter([retrieved_file]),
|
||||
connector._extract_docs_from_google_drive(
|
||||
checkpoint=checkpoint,
|
||||
start=None,
|
||||
end=None,
|
||||
include_permissions=False,
|
||||
)
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
convert_mock.assert_called_once()
|
||||
fetch_mock.assert_called_once()
|
||||
get_new_ancestors_mock.assert_called_once()
|
||||
assert fetch_mock.call_args.kwargs["field_type"] == DriveFileFieldType.STANDARD
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
"""Tests for GoogleDriveConnector.resolve_errors against real Google Drive."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import patch
|
||||
|
||||
from onyx.connectors.google_drive.connector import GoogleDriveConnector
|
||||
from onyx.connectors.models import ConnectorFailure
|
||||
from onyx.connectors.models import Document
|
||||
from onyx.connectors.models import DocumentFailure
|
||||
from onyx.connectors.models import HierarchyNode
|
||||
from tests.daily.connectors.google_drive.consts_and_utils import ADMIN_EMAIL
|
||||
from tests.daily.connectors.google_drive.consts_and_utils import (
|
||||
ALL_EXPECTED_HIERARCHY_NODES,
|
||||
)
|
||||
from tests.daily.connectors.google_drive.consts_and_utils import FOLDER_1_ID
|
||||
from tests.daily.connectors.google_drive.consts_and_utils import SHARED_DRIVE_1_ID
|
||||
|
||||
_DRIVE_ID_MAPPING_PATH = os.path.join(
|
||||
os.path.dirname(__file__), "drive_id_mapping.json"
|
||||
)
|
||||
|
||||
|
||||
def _load_web_view_links(file_ids: list[int]) -> list[str]:
|
||||
with open(_DRIVE_ID_MAPPING_PATH) as f:
|
||||
mapping: dict[str, str] = json.load(f)
|
||||
return [mapping[str(fid)] for fid in file_ids]
|
||||
|
||||
|
||||
def _build_failures(web_view_links: list[str]) -> list[ConnectorFailure]:
|
||||
return [
|
||||
ConnectorFailure(
|
||||
failed_document=DocumentFailure(
|
||||
document_id=link,
|
||||
document_link=link,
|
||||
),
|
||||
failure_message=f"Synthetic failure for {link}",
|
||||
)
|
||||
for link in web_view_links
|
||||
]
|
||||
|
||||
|
||||
@patch("onyx.file_processing.extract_file_text.get_unstructured_api_key")
|
||||
def test_resolve_single_file(
|
||||
mock_api_key: None, # noqa: ARG001
|
||||
google_drive_service_acct_connector_factory: Callable[..., GoogleDriveConnector],
|
||||
) -> None:
|
||||
"""Resolve a single known file and verify we get back exactly one Document."""
|
||||
connector = google_drive_service_acct_connector_factory(
|
||||
primary_admin_email=ADMIN_EMAIL,
|
||||
include_shared_drives=True,
|
||||
shared_drive_urls=None,
|
||||
include_my_drives=True,
|
||||
my_drive_emails=None,
|
||||
shared_folder_urls=None,
|
||||
include_files_shared_with_me=False,
|
||||
)
|
||||
|
||||
web_view_links = _load_web_view_links([0])
|
||||
failures = _build_failures(web_view_links)
|
||||
|
||||
results = list(connector.resolve_errors(failures))
|
||||
|
||||
docs = [r for r in results if isinstance(r, Document)]
|
||||
new_failures = [r for r in results if isinstance(r, ConnectorFailure)]
|
||||
hierarchy_nodes = [r for r in results if isinstance(r, HierarchyNode)]
|
||||
|
||||
assert len(docs) == 1
|
||||
assert len(new_failures) == 0
|
||||
assert docs[0].semantic_identifier == "file_0.txt"
|
||||
|
||||
# Should yield at least one hierarchy node (the file's parent folder chain)
|
||||
assert len(hierarchy_nodes) > 0
|
||||
|
||||
|
||||
@patch("onyx.file_processing.extract_file_text.get_unstructured_api_key")
|
||||
def test_resolve_multiple_files(
|
||||
mock_api_key: None, # noqa: ARG001
|
||||
google_drive_service_acct_connector_factory: Callable[..., GoogleDriveConnector],
|
||||
) -> None:
|
||||
"""Resolve multiple files across different folders via batch API."""
|
||||
connector = google_drive_service_acct_connector_factory(
|
||||
primary_admin_email=ADMIN_EMAIL,
|
||||
include_shared_drives=True,
|
||||
shared_drive_urls=None,
|
||||
include_my_drives=True,
|
||||
my_drive_emails=None,
|
||||
shared_folder_urls=None,
|
||||
include_files_shared_with_me=False,
|
||||
)
|
||||
|
||||
# Pick files from different folders: admin files (0-4), shared drive 1 (20-24), folder_2 (45-49)
|
||||
file_ids = [0, 1, 20, 21, 45]
|
||||
web_view_links = _load_web_view_links(file_ids)
|
||||
failures = _build_failures(web_view_links)
|
||||
|
||||
results = list(connector.resolve_errors(failures))
|
||||
|
||||
docs = [r for r in results if isinstance(r, Document)]
|
||||
new_failures = [r for r in results if isinstance(r, ConnectorFailure)]
|
||||
hierarchy_nodes = [r for r in results if isinstance(r, HierarchyNode)]
|
||||
|
||||
assert len(new_failures) == 0
|
||||
retrieved_names = {doc.semantic_identifier for doc in docs}
|
||||
expected_names = {f"file_{fid}.txt" for fid in file_ids}
|
||||
assert expected_names == retrieved_names
|
||||
|
||||
# Files span multiple folders, so we should get hierarchy nodes
|
||||
assert len(hierarchy_nodes) > 0
|
||||
|
||||
|
||||
@patch("onyx.file_processing.extract_file_text.get_unstructured_api_key")
|
||||
def test_resolve_hierarchy_nodes_are_valid(
|
||||
mock_api_key: None, # noqa: ARG001
|
||||
google_drive_service_acct_connector_factory: Callable[..., GoogleDriveConnector],
|
||||
) -> None:
|
||||
"""Verify that hierarchy nodes from resolve_errors match expected structure."""
|
||||
connector = google_drive_service_acct_connector_factory(
|
||||
primary_admin_email=ADMIN_EMAIL,
|
||||
include_shared_drives=True,
|
||||
shared_drive_urls=None,
|
||||
include_my_drives=True,
|
||||
my_drive_emails=None,
|
||||
shared_folder_urls=None,
|
||||
include_files_shared_with_me=False,
|
||||
)
|
||||
|
||||
# File in folder_1 (inside shared_drive_1) — should walk up to shared_drive_1 root
|
||||
web_view_links = _load_web_view_links([25])
|
||||
failures = _build_failures(web_view_links)
|
||||
|
||||
results = list(connector.resolve_errors(failures))
|
||||
|
||||
hierarchy_nodes = [r for r in results if isinstance(r, HierarchyNode)]
|
||||
node_ids = {node.raw_node_id for node in hierarchy_nodes}
|
||||
|
||||
# File 25 is in folder_1 which is inside shared_drive_1.
|
||||
# The parent walk must yield at least these two ancestors.
|
||||
assert (
|
||||
FOLDER_1_ID in node_ids
|
||||
), f"Expected folder_1 ({FOLDER_1_ID}) in hierarchy nodes, got: {node_ids}"
|
||||
assert (
|
||||
SHARED_DRIVE_1_ID in node_ids
|
||||
), f"Expected shared_drive_1 ({SHARED_DRIVE_1_ID}) in hierarchy nodes, got: {node_ids}"
|
||||
|
||||
for node in hierarchy_nodes:
|
||||
if node.raw_node_id not in ALL_EXPECTED_HIERARCHY_NODES:
|
||||
continue
|
||||
expected = ALL_EXPECTED_HIERARCHY_NODES[node.raw_node_id]
|
||||
assert node.display_name == expected.display_name, (
|
||||
f"Display name mismatch for {node.raw_node_id}: "
|
||||
f"expected '{expected.display_name}', got '{node.display_name}'"
|
||||
)
|
||||
assert node.node_type == expected.node_type, (
|
||||
f"Node type mismatch for {node.raw_node_id}: "
|
||||
f"expected '{expected.node_type}', got '{node.node_type}'"
|
||||
)
|
||||
|
||||
|
||||
@patch("onyx.file_processing.extract_file_text.get_unstructured_api_key")
|
||||
def test_resolve_with_invalid_link(
|
||||
mock_api_key: None, # noqa: ARG001
|
||||
google_drive_service_acct_connector_factory: Callable[..., GoogleDriveConnector],
|
||||
) -> None:
|
||||
"""Resolve with a mix of valid and invalid links — invalid ones yield ConnectorFailure."""
|
||||
connector = google_drive_service_acct_connector_factory(
|
||||
primary_admin_email=ADMIN_EMAIL,
|
||||
include_shared_drives=True,
|
||||
shared_drive_urls=None,
|
||||
include_my_drives=True,
|
||||
my_drive_emails=None,
|
||||
shared_folder_urls=None,
|
||||
include_files_shared_with_me=False,
|
||||
)
|
||||
|
||||
valid_links = _load_web_view_links([0])
|
||||
invalid_link = "https://drive.google.com/file/d/NONEXISTENT_FILE_ID_12345"
|
||||
failures = _build_failures(valid_links + [invalid_link])
|
||||
|
||||
results = list(connector.resolve_errors(failures))
|
||||
|
||||
docs = [r for r in results if isinstance(r, Document)]
|
||||
new_failures = [r for r in results if isinstance(r, ConnectorFailure)]
|
||||
|
||||
assert len(docs) == 1
|
||||
assert docs[0].semantic_identifier == "file_0.txt"
|
||||
assert len(new_failures) == 1
|
||||
assert new_failures[0].failed_document is not None
|
||||
assert new_failures[0].failed_document.document_id == invalid_link
|
||||
|
||||
|
||||
@patch("onyx.file_processing.extract_file_text.get_unstructured_api_key")
|
||||
def test_resolve_empty_errors(
|
||||
mock_api_key: None, # noqa: ARG001
|
||||
google_drive_service_acct_connector_factory: Callable[..., GoogleDriveConnector],
|
||||
) -> None:
|
||||
"""Resolving an empty error list should yield nothing."""
|
||||
connector = google_drive_service_acct_connector_factory(
|
||||
primary_admin_email=ADMIN_EMAIL,
|
||||
include_shared_drives=True,
|
||||
shared_drive_urls=None,
|
||||
include_my_drives=True,
|
||||
my_drive_emails=None,
|
||||
shared_folder_urls=None,
|
||||
include_files_shared_with_me=False,
|
||||
)
|
||||
|
||||
results = list(connector.resolve_errors([]))
|
||||
|
||||
assert len(results) == 0
|
||||
|
||||
|
||||
@patch("onyx.file_processing.extract_file_text.get_unstructured_api_key")
|
||||
def test_resolve_entity_failures_are_skipped(
|
||||
mock_api_key: None, # noqa: ARG001
|
||||
google_drive_service_acct_connector_factory: Callable[..., GoogleDriveConnector],
|
||||
) -> None:
|
||||
"""Entity failures (not document failures) should be skipped by resolve_errors."""
|
||||
from onyx.connectors.models import EntityFailure
|
||||
|
||||
connector = google_drive_service_acct_connector_factory(
|
||||
primary_admin_email=ADMIN_EMAIL,
|
||||
include_shared_drives=True,
|
||||
shared_drive_urls=None,
|
||||
include_my_drives=True,
|
||||
my_drive_emails=None,
|
||||
shared_folder_urls=None,
|
||||
include_files_shared_with_me=False,
|
||||
)
|
||||
|
||||
entity_failure = ConnectorFailure(
|
||||
failed_entity=EntityFailure(entity_id="some_stage"),
|
||||
failure_message="retrieval failure",
|
||||
)
|
||||
|
||||
results = list(connector.resolve_errors([entity_failure]))
|
||||
|
||||
assert len(results) == 0
|
||||
@@ -27,11 +27,13 @@ def create_placement(
|
||||
turn_index: int,
|
||||
tab_index: int = 0,
|
||||
sub_turn_index: int | None = None,
|
||||
model_index: int | None = 0,
|
||||
) -> Placement:
|
||||
return Placement(
|
||||
turn_index=turn_index,
|
||||
tab_index=tab_index,
|
||||
sub_turn_index=sub_turn_index,
|
||||
model_index=model_index,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ This test:
|
||||
All external HTTP calls are mocked, but Postgres and Redis are running.
|
||||
"""
|
||||
|
||||
import queue
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
@@ -20,7 +21,7 @@ from uuid import uuid4
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.chat.emitter import get_default_emitter
|
||||
from onyx.chat.emitter import Emitter
|
||||
from onyx.db.enums import MCPAuthenticationPerformer
|
||||
from onyx.db.enums import MCPAuthenticationType
|
||||
from onyx.db.enums import MCPTransport
|
||||
@@ -137,7 +138,7 @@ class TestMCPPassThroughOAuth:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
search_tool_config=search_tool_config,
|
||||
@@ -200,7 +201,7 @@ class TestMCPPassThroughOAuth:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
search_tool_config=SearchToolConfig(),
|
||||
@@ -275,7 +276,7 @@ class TestMCPPassThroughOAuth:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
search_tool_config=SearchToolConfig(),
|
||||
@@ -350,7 +351,7 @@ class TestMCPPassThroughOAuth:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
search_tool_config=SearchToolConfig(),
|
||||
@@ -458,7 +459,7 @@ class TestMCPPassThroughOAuth:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
search_tool_config=SearchToolConfig(),
|
||||
@@ -541,7 +542,7 @@ class TestMCPPassThroughOAuth:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
search_tool_config=SearchToolConfig(),
|
||||
|
||||
@@ -8,6 +8,7 @@ Tests the priority logic for OAuth tokens when constructing custom tools:
|
||||
All external HTTP calls are mocked, but Postgres and Redis are running.
|
||||
"""
|
||||
|
||||
import queue
|
||||
from typing import Any
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import patch
|
||||
@@ -16,7 +17,7 @@ from uuid import uuid4
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.chat.emitter import get_default_emitter
|
||||
from onyx.chat.emitter import Emitter
|
||||
from onyx.db.models import OAuthAccount
|
||||
from onyx.db.models import OAuthConfig
|
||||
from onyx.db.models import Persona
|
||||
@@ -174,7 +175,7 @@ class TestOAuthToolIntegrationPriority:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
search_tool_config=search_tool_config,
|
||||
@@ -232,7 +233,7 @@ class TestOAuthToolIntegrationPriority:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
)
|
||||
@@ -284,7 +285,7 @@ class TestOAuthToolIntegrationPriority:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
)
|
||||
@@ -345,7 +346,7 @@ class TestOAuthToolIntegrationPriority:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
)
|
||||
@@ -416,7 +417,7 @@ class TestOAuthToolIntegrationPriority:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
)
|
||||
@@ -483,7 +484,7 @@ class TestOAuthToolIntegrationPriority:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
)
|
||||
@@ -536,7 +537,7 @@ class TestOAuthToolIntegrationPriority:
|
||||
tool_dict = construct_tools(
|
||||
persona=persona,
|
||||
db_session=db_session,
|
||||
emitter=get_default_emitter(),
|
||||
emitter=Emitter(merged_queue=queue.Queue()),
|
||||
user=user,
|
||||
llm=llm,
|
||||
)
|
||||
|
||||
173
backend/tests/unit/onyx/chat/test_emitter.py
Normal file
173
backend/tests/unit/onyx/chat/test_emitter.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""Unit tests for the Emitter class.
|
||||
|
||||
All tests use the streaming mode (merged_queue required). Emitter has a single
|
||||
code path — no standalone bus.
|
||||
"""
|
||||
|
||||
import queue
|
||||
|
||||
from onyx.chat.emitter import Emitter
|
||||
from onyx.server.query_and_chat.placement import Placement
|
||||
from onyx.server.query_and_chat.streaming_models import OverallStop
|
||||
from onyx.server.query_and_chat.streaming_models import Packet
|
||||
from onyx.server.query_and_chat.streaming_models import ReasoningStart
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _placement(
|
||||
turn_index: int = 0,
|
||||
tab_index: int = 0,
|
||||
sub_turn_index: int | None = None,
|
||||
) -> Placement:
|
||||
return Placement(
|
||||
turn_index=turn_index,
|
||||
tab_index=tab_index,
|
||||
sub_turn_index=sub_turn_index,
|
||||
)
|
||||
|
||||
|
||||
def _packet(
|
||||
turn_index: int = 0,
|
||||
tab_index: int = 0,
|
||||
sub_turn_index: int | None = None,
|
||||
) -> Packet:
|
||||
"""Build a minimal valid packet with an OverallStop payload."""
|
||||
return Packet(
|
||||
placement=_placement(turn_index, tab_index, sub_turn_index),
|
||||
obj=OverallStop(stop_reason="test"),
|
||||
)
|
||||
|
||||
|
||||
def _make_emitter(model_idx: int = 0) -> tuple["Emitter", "queue.Queue"]:
|
||||
"""Return (emitter, queue) wired together."""
|
||||
mq: queue.Queue = queue.Queue()
|
||||
return Emitter(merged_queue=mq, model_idx=model_idx), mq
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Queue routing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEmitterQueueRouting:
|
||||
def test_emit_lands_on_merged_queue(self) -> None:
|
||||
emitter, mq = _make_emitter()
|
||||
emitter.emit(_packet())
|
||||
assert not mq.empty()
|
||||
|
||||
def test_queue_item_is_tuple_of_key_and_packet(self) -> None:
|
||||
emitter, mq = _make_emitter(model_idx=1)
|
||||
emitter.emit(_packet())
|
||||
item = mq.get_nowait()
|
||||
assert isinstance(item, tuple)
|
||||
assert len(item) == 2
|
||||
|
||||
def test_multiple_packets_delivered_fifo(self) -> None:
|
||||
emitter, mq = _make_emitter()
|
||||
p1 = _packet(turn_index=0)
|
||||
p2 = _packet(turn_index=1)
|
||||
emitter.emit(p1)
|
||||
emitter.emit(p2)
|
||||
_, t1 = mq.get_nowait()
|
||||
_, t2 = mq.get_nowait()
|
||||
assert t1.placement.turn_index == 0
|
||||
assert t2.placement.turn_index == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# model_index tagging
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEmitterModelIndexTagging:
|
||||
def test_n1_default_model_idx_tags_model_index_zero(self) -> None:
|
||||
"""N=1: default model_idx=0, so packet gets model_index=0."""
|
||||
emitter, mq = _make_emitter(model_idx=0)
|
||||
emitter.emit(_packet())
|
||||
_key, tagged = mq.get_nowait()
|
||||
assert tagged.placement.model_index == 0
|
||||
|
||||
def test_model_idx_one_tags_packet(self) -> None:
|
||||
emitter, mq = _make_emitter(model_idx=1)
|
||||
emitter.emit(_packet())
|
||||
_key, tagged = mq.get_nowait()
|
||||
assert tagged.placement.model_index == 1
|
||||
|
||||
def test_model_idx_two_tags_packet(self) -> None:
|
||||
"""Boundary: third model in a 3-model run."""
|
||||
emitter, mq = _make_emitter(model_idx=2)
|
||||
emitter.emit(_packet())
|
||||
_key, tagged = mq.get_nowait()
|
||||
assert tagged.placement.model_index == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Queue key
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEmitterQueueKey:
|
||||
def test_key_equals_model_idx(self) -> None:
|
||||
"""Drain loop uses the key to route packets; it must match model_idx."""
|
||||
emitter, mq = _make_emitter(model_idx=2)
|
||||
emitter.emit(_packet())
|
||||
key, _ = mq.get_nowait()
|
||||
assert key == 2
|
||||
|
||||
def test_n1_key_is_zero(self) -> None:
|
||||
emitter, mq = _make_emitter(model_idx=0)
|
||||
emitter.emit(_packet())
|
||||
key, _ = mq.get_nowait()
|
||||
assert key == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Placement field preservation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEmitterPlacementPreservation:
|
||||
def test_turn_index_is_preserved(self) -> None:
|
||||
emitter, mq = _make_emitter()
|
||||
emitter.emit(_packet(turn_index=5))
|
||||
_, tagged = mq.get_nowait()
|
||||
assert tagged.placement.turn_index == 5
|
||||
|
||||
def test_tab_index_is_preserved(self) -> None:
|
||||
emitter, mq = _make_emitter()
|
||||
emitter.emit(_packet(tab_index=3))
|
||||
_, tagged = mq.get_nowait()
|
||||
assert tagged.placement.tab_index == 3
|
||||
|
||||
def test_sub_turn_index_is_preserved(self) -> None:
|
||||
emitter, mq = _make_emitter()
|
||||
emitter.emit(_packet(sub_turn_index=2))
|
||||
_, tagged = mq.get_nowait()
|
||||
assert tagged.placement.sub_turn_index == 2
|
||||
|
||||
def test_sub_turn_index_none_is_preserved(self) -> None:
|
||||
emitter, mq = _make_emitter()
|
||||
emitter.emit(_packet(sub_turn_index=None))
|
||||
_, tagged = mq.get_nowait()
|
||||
assert tagged.placement.sub_turn_index is None
|
||||
|
||||
def test_packet_obj_is_not_modified(self) -> None:
|
||||
"""The payload object must survive tagging untouched."""
|
||||
emitter, mq = _make_emitter()
|
||||
original_obj = OverallStop(stop_reason="sentinel")
|
||||
pkt = Packet(placement=_placement(), obj=original_obj)
|
||||
emitter.emit(pkt)
|
||||
_, tagged = mq.get_nowait()
|
||||
assert tagged.obj is original_obj
|
||||
|
||||
def test_different_obj_types_are_handled(self) -> None:
|
||||
"""Any valid PacketObj type passes through correctly."""
|
||||
emitter, mq = _make_emitter()
|
||||
pkt = Packet(placement=_placement(), obj=ReasoningStart())
|
||||
emitter.emit(pkt)
|
||||
_, tagged = mq.get_nowait()
|
||||
assert isinstance(tagged.obj, ReasoningStart)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for memory tool streaming packet emissions."""
|
||||
|
||||
from queue import Queue
|
||||
import queue
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -18,9 +18,13 @@ from onyx.tools.tool_implementations.memory.models import MemoryToolResponse
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def emitter() -> Emitter:
|
||||
bus: Queue = Queue()
|
||||
return Emitter(bus)
|
||||
def emitter_queue() -> queue.Queue:
|
||||
return queue.Queue()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def emitter(emitter_queue: queue.Queue) -> Emitter:
|
||||
return Emitter(merged_queue=emitter_queue)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -53,24 +57,27 @@ class TestMemoryToolEmitStart:
|
||||
def test_emit_start_emits_memory_tool_start_packet(
|
||||
self,
|
||||
memory_tool: MemoryTool,
|
||||
emitter: Emitter,
|
||||
emitter_queue: queue.Queue,
|
||||
placement: Placement,
|
||||
) -> None:
|
||||
memory_tool.emit_start(placement)
|
||||
|
||||
packet = emitter.bus.get_nowait()
|
||||
_key, packet = emitter_queue.get_nowait()
|
||||
assert isinstance(packet.obj, MemoryToolStart)
|
||||
assert packet.placement == placement
|
||||
assert packet.placement is not None
|
||||
assert packet.placement.turn_index == placement.turn_index
|
||||
assert packet.placement.tab_index == placement.tab_index
|
||||
assert packet.placement.model_index == 0 # emitter stamps model_index=0
|
||||
|
||||
def test_emit_start_with_different_placement(
|
||||
self,
|
||||
memory_tool: MemoryTool,
|
||||
emitter: Emitter,
|
||||
emitter_queue: queue.Queue,
|
||||
) -> None:
|
||||
placement = Placement(turn_index=2, tab_index=1)
|
||||
memory_tool.emit_start(placement)
|
||||
|
||||
packet = emitter.bus.get_nowait()
|
||||
_key, packet = emitter_queue.get_nowait()
|
||||
assert packet.placement.turn_index == 2
|
||||
assert packet.placement.tab_index == 1
|
||||
|
||||
@@ -81,7 +88,7 @@ class TestMemoryToolRun:
|
||||
self,
|
||||
mock_process: MagicMock,
|
||||
memory_tool: MemoryTool,
|
||||
emitter: Emitter,
|
||||
emitter_queue: queue.Queue,
|
||||
placement: Placement,
|
||||
override_kwargs: MemoryToolOverrideKwargs,
|
||||
) -> None:
|
||||
@@ -93,21 +100,19 @@ class TestMemoryToolRun:
|
||||
memory="User prefers Python",
|
||||
)
|
||||
|
||||
# The delta packet should be in the queue
|
||||
packet = emitter.bus.get_nowait()
|
||||
_key, packet = emitter_queue.get_nowait()
|
||||
assert isinstance(packet.obj, MemoryToolDelta)
|
||||
assert packet.obj.memory_text == "User prefers Python"
|
||||
assert packet.obj.operation == "add"
|
||||
assert packet.obj.memory_id is None
|
||||
assert packet.obj.index is None
|
||||
assert packet.placement == placement
|
||||
|
||||
@patch("onyx.tools.tool_implementations.memory.memory_tool.process_memory_update")
|
||||
def test_run_emits_delta_for_update_operation(
|
||||
self,
|
||||
mock_process: MagicMock,
|
||||
memory_tool: MemoryTool,
|
||||
emitter: Emitter,
|
||||
emitter_queue: queue.Queue,
|
||||
placement: Placement,
|
||||
override_kwargs: MemoryToolOverrideKwargs,
|
||||
) -> None:
|
||||
@@ -119,7 +124,7 @@ class TestMemoryToolRun:
|
||||
memory="User prefers light mode",
|
||||
)
|
||||
|
||||
packet = emitter.bus.get_nowait()
|
||||
_key, packet = emitter_queue.get_nowait()
|
||||
assert isinstance(packet.obj, MemoryToolDelta)
|
||||
assert packet.obj.memory_text == "User prefers light mode"
|
||||
assert packet.obj.operation == "update"
|
||||
|
||||
184
contributing_guides/best_practices.md
Normal file
184
contributing_guides/best_practices.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Engineering Principles, Style, and Correctness Guide
|
||||
|
||||
## Principles and collaboration
|
||||
|
||||
- **Use 1-way vs 2-way doors.** For 2-way doors, move faster and iterate. For 1-way doors, be more deliberate.
|
||||
- **Consistency > being “right.”** Prefer consistent patterns across the codebase. If something is truly bad, fix it everywhere.
|
||||
- **Fix what you touch (selectively).**
|
||||
- Don’t feel obligated to fix every best-practice issue you notice.
|
||||
- Don’t introduce new bad practices.
|
||||
- If your change touches code that violates best practices, fix it as part of the change.
|
||||
- **Don’t tack features on.** When adding functionality, restructure logically as needed to avoid muddying interfaces and accumulating tech debt.
|
||||
|
||||
---
|
||||
|
||||
## Style and maintainability
|
||||
|
||||
### Comments and readability
|
||||
Add clear comments:
|
||||
- At logical boundaries (e.g., interfaces) so the reader doesn’t need to dig 10 layers deeper.
|
||||
- Wherever assumptions are made or something non-obvious/unexpected is done.
|
||||
- For complicated flows/functions.
|
||||
- Wherever it saves time (e.g., nontrivial regex patterns).
|
||||
|
||||
### Errors and exceptions
|
||||
- **Fail loudly** rather than silently skipping work.
|
||||
- Example: raise and let exceptions propagate instead of silently dropping a document.
|
||||
- **Don’t overuse `try/except`.**
|
||||
- Put `try/except` at the correct logical level.
|
||||
- Do not mask exceptions unless it is clearly appropriate.
|
||||
|
||||
### Typing
|
||||
- Everything should be **as strictly typed as possible**.
|
||||
- Use `cast` for annoying/loose-typed interfaces (e.g., results of `run_functions_tuples_in_parallel`).
|
||||
- Only `cast` when the type checker sees `Any` or types are too loose.
|
||||
- Prefer types that are easy to read.
|
||||
- Avoid dense types like `dict[tuple[str, str], list[list[float]]]`.
|
||||
- Prefer domain models, e.g.:
|
||||
- `EmbeddingModel(provider_name, model_name)` as a Pydantic model
|
||||
- `dict[EmbeddingModel, list[EmbeddingVector]]`
|
||||
|
||||
### State, objects, and boundaries
|
||||
- Keep **clear logical boundaries** for state containers and objects.
|
||||
- A **config** object should never contain things like a `db_session`.
|
||||
- Avoid state containers that are:
|
||||
- overly nested, or
|
||||
- huge + flat (use judgment).
|
||||
- Prefer **composition and functional style** over inheritance/OOP.
|
||||
- Prefer **no mutation** unless there’s a strong reason.
|
||||
- State objects should be **intentional and explicit**, ideally nonmutating.
|
||||
- Use interfaces/objects to create clear separation of responsibility.
|
||||
- Prefer simplicity when there’s no clear gain
|
||||
- Avoid overcomplicated mechanisms like semaphores.
|
||||
- Prefer **hash maps (dicts)** over tree structures unless there’s a strong reason.
|
||||
|
||||
### Naming
|
||||
- Name variables carefully and intentionally.
|
||||
- Prefer long, explicit names when undecided.
|
||||
- Avoid single-character variables except for small, self-contained utilities (or not at all).
|
||||
- Keep the same object/name consistent through the call stack and within functions when reasonable.
|
||||
- Good: `for token in tokens:`
|
||||
- Bad: `for msg in tokens:` (if iterating tokens)
|
||||
- Function names should bias toward **long + descriptive** for codebase search.
|
||||
- IntelliSense can miss call sites; search works best with unique names.
|
||||
- “Fetch versioned implementation” is an example of why this matters.
|
||||
|
||||
### Correctness by construction
|
||||
- Prefer self-contained correctness.
|
||||
- Don’t rely on callers to “use it right” if you can make misuse hard.
|
||||
- Avoid redundancies:
|
||||
- If a function takes an arg, it shouldn’t also take a state object that contains that same arg.
|
||||
- No dead code (unless there’s a very good reason).
|
||||
- No commented-out code in main or feature branches (unless there’s a very good reason).
|
||||
- No duplicate logic:
|
||||
- Don’t copy/paste into branches when shared logic can live above the conditional.
|
||||
- If you’re afraid to touch the original, you don’t understand it well enough.
|
||||
- LLMs often create subtle duplicate logic—review carefully and remove it.
|
||||
- Avoid “nearly identical” objects that confuse when to use which.
|
||||
- Avoid extremely long functions with chained logic:
|
||||
- Encapsulate steps into helpers for readability, even if not reused.
|
||||
- “Pythonic” multi-step expressions are OK in moderation; don’t trade clarity for cleverness.
|
||||
|
||||
---
|
||||
|
||||
## Performance and correctness
|
||||
|
||||
- Avoid holding resources for extended periods:
|
||||
- DB sessions
|
||||
- locks/semaphores
|
||||
- Validate objects:
|
||||
- on creation, and
|
||||
- right before use.
|
||||
- Connector code (data → Onyx documents):
|
||||
- Any in-memory structure that can grow without bound based on input must be periodically size-checked.
|
||||
- If a connector is OOMing (often shows up as “missing celery tasks”), this is a top thing to check retroactively.
|
||||
- Async and event loops:
|
||||
- Never introduce new async/event loop Python code, and try to make existing
|
||||
async code synchronous when possible if it makes sense.
|
||||
- Writing async code without 100% understanding the code and having a
|
||||
concrete reason to do so is likely to introduce bugs and not add any
|
||||
meaningful performance gains.
|
||||
|
||||
---
|
||||
|
||||
## Repository conventions: where code lives
|
||||
|
||||
- Pydantic + data models: `models.py` files.
|
||||
- DB interface functions (excluding lazy loading): `db/` directory.
|
||||
- LLM prompts: `prompts/` directory, roughly mirroring the code layout that uses them.
|
||||
- API routes: `server/` directory.
|
||||
|
||||
---
|
||||
|
||||
## Pydantic and modeling rules
|
||||
|
||||
- Prefer **Pydantic** over dataclasses.
|
||||
- If absolutely required, use `allow_arbitrary_types`.
|
||||
|
||||
---
|
||||
|
||||
## Data conventions
|
||||
|
||||
- Prefer explicit `None` over sentinel empty strings (usually; depends on intent).
|
||||
- Prefer explicit identifiers:
|
||||
- Use string enums instead of integer codes.
|
||||
- Avoid magic numbers (co-location is good when necessary). **Always avoid magic strings.**
|
||||
|
||||
---
|
||||
|
||||
## Logging
|
||||
|
||||
- Log messages where they are created.
|
||||
- Don’t propagate log messages around just to log them elsewhere.
|
||||
|
||||
---
|
||||
|
||||
## Encapsulation
|
||||
|
||||
- Don’t use private attributes/methods/properties from other classes/modules.
|
||||
- “Private” is private—respect that boundary.
|
||||
|
||||
---
|
||||
|
||||
## SQLAlchemy guidance
|
||||
|
||||
- Lazy loading is often bad at scale, especially across multiple list relationships.
|
||||
- Be careful when accessing SQLAlchemy object attributes:
|
||||
- It can help avoid redundant DB queries,
|
||||
- but it can also fail if accessed outside an active session,
|
||||
- and lazy loading can add hidden DB dependencies to otherwise “simple” functions.
|
||||
- Reference: https://www.reddit.com/r/SQLAlchemy/comments/138f248/joinedload_vs_selectinload/
|
||||
|
||||
---
|
||||
|
||||
## Trunk-based development and feature flags
|
||||
|
||||
- **PRs should contain no more than 500 lines of real change.**
|
||||
- **Merge to main frequently.** Avoid long-lived feature branches—they create merge conflicts and integration pain.
|
||||
- **Use feature flags for incremental rollout.**
|
||||
- Large features should be merged in small, shippable increments behind a flag.
|
||||
- This allows continuous integration without exposing incomplete functionality.
|
||||
- **Keep flags short-lived.** Once a feature is fully rolled out, remove the flag and dead code paths promptly.
|
||||
- **Flag at the right level.** Prefer flagging at API/UI entry points rather than deep in business logic.
|
||||
- **Test both flag states.** Ensure the codebase works correctly with the flag on and off.
|
||||
|
||||
---
|
||||
|
||||
## Misc
|
||||
|
||||
- Any TODOs you add in the code must be accompanied by either the name/username
|
||||
of the owner of that TODO, or an issue number for an issue referencing that
|
||||
piece of work.
|
||||
- Avoid module-level logic that runs on import, which leads to import-time side
|
||||
effects. Essentially every piece of meaningful logic should exist within some
|
||||
function that has to be explicitly invoked. Acceptable exceptions to this may
|
||||
include loading environment variables or setting up loggers.
|
||||
- If you find yourself needing something like this, you may want that logic to
|
||||
exist in a file dedicated for manual execution (contains `if __name__ ==
|
||||
"__main__":`) which should not be imported by anything else.
|
||||
- Related to the above, do not conflate Python scripts you intend to run from
|
||||
the command line (contains `if __name__ == "__main__":`) with modules you
|
||||
intend to import from elsewhere. If for some unlikely reason they have to be
|
||||
the same file, any logic specific to executing the file (including imports)
|
||||
should be contained in the `if __name__ == "__main__":` block.
|
||||
- Generally these executable files exist in `backend/scripts/`.
|
||||
36
contributing_guides/contributing_macos.md
Normal file
36
contributing_guides/contributing_macos.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## Some additional notes for Mac Users
|
||||
|
||||
The base instructions to set up the development environment are located in [CONTRIBUTING.md](https://github.com/onyx-dot-app/onyx/blob/main/CONTRIBUTING.md).
|
||||
|
||||
### Setting up Python
|
||||
|
||||
Ensure [Homebrew](https://brew.sh/) is already set up.
|
||||
|
||||
Then install python 3.11.
|
||||
|
||||
```bash
|
||||
brew install python@3.11
|
||||
```
|
||||
|
||||
Add python 3.11 to your path: add the following line to ~/.zshrc
|
||||
|
||||
```
|
||||
export PATH="$(brew --prefix)/opt/python@3.11/libexec/bin:$PATH"
|
||||
```
|
||||
|
||||
> **Note:**
|
||||
> You will need to open a new terminal for the path change above to take effect.
|
||||
|
||||
### Setting up Docker
|
||||
|
||||
On macOS, you will need to install [Docker Desktop](https://www.docker.com/products/docker-desktop/) and
|
||||
ensure it is running before continuing with the docker commands.
|
||||
|
||||
### Formatting and Linting
|
||||
|
||||
MacOS will likely require you to remove some quarantine attributes on some of the hooks for them to execute properly.
|
||||
After installing pre-commit, run the following command:
|
||||
|
||||
```bash
|
||||
sudo xattr -r -d com.apple.quarantine ~/.cache/pre-commit
|
||||
```
|
||||
30
contributing_guides/contributing_vscode.md
Normal file
30
contributing_guides/contributing_vscode.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# VSCode Debugging Setup
|
||||
|
||||
This guide explains how to set up and use VSCode's debugging capabilities with this project.
|
||||
|
||||
## Initial Setup
|
||||
|
||||
1. **Environment Setup**:
|
||||
- Copy `.vscode/env_template.txt` to `.vscode/.env`
|
||||
- Fill in the necessary environment variables in `.vscode/.env`
|
||||
|
||||
## Using the Debugger
|
||||
|
||||
Before starting, make sure the Docker Daemon is running.
|
||||
|
||||
1. Open the Debug view in VSCode (Cmd+Shift+D on macOS)
|
||||
2. From the dropdown at the top, select "Clear and Restart External Volumes and Containers" and press the green play button
|
||||
3. From the dropdown at the top, select "Run All Onyx Services" and press the green play button
|
||||
4. Now, you can navigate to onyx in your browser (default is http://localhost:3000) and start using the app
|
||||
5. You can set breakpoints by clicking to the left of line numbers to help debug while the app is running
|
||||
6. Use the debug toolbar to step through code, inspect variables, etc.
|
||||
|
||||
Note: Clear and Restart External Volumes and Containers will reset your postgres and Vespa (relational-db and index).
|
||||
Only run this if you are okay with wiping your data.
|
||||
|
||||
## Features
|
||||
|
||||
- Hot reload is enabled for the web server and API servers
|
||||
- Python debugging is configured with debugpy
|
||||
- Environment variables are loaded from `.vscode/.env`
|
||||
- Console output is organized in the integrated terminal with labeled tabs
|
||||
38
contributing_guides/contribution_process.md
Normal file
38
contributing_guides/contribution_process.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Contribution Process
|
||||
|
||||
## 1. Get the feature or enhancement approved
|
||||
Create a GitHub issue and see if there are upvotes. If you feel the feature is sufficiently value additive and you would like
|
||||
approval to contribute it to the repo, tag [Yuhong](https://github.com/yuhongsun96) to review.
|
||||
|
||||
If you do not get a response within a week, feel free to email yuhong@onyx.app and include the issue in the message.
|
||||
|
||||
Not all small features and enhancements will be accepted as there is a balance between feature richness and bloat.
|
||||
We strive to provide the best user experience possible so we have to be intentional about what we include in the app.
|
||||
|
||||
|
||||
## 2. Get the design approved
|
||||
The Onyx team will either provide a design doc and PRD for the feature or request one from you, the contributor.
|
||||
|
||||
The scope and detail of the design will depend on the individual feature.
|
||||
|
||||
|
||||
# 3. IP attribution for EE contributions
|
||||
If you are contributing features to Onyx Enterprise Edition, you are required to sign the IP Assignment Agreement in the
|
||||
contributing_guides directory.
|
||||
|
||||
|
||||
## 4. Review and testing
|
||||
Your features must pass all tests and all comments must be addressed prior to merging.
|
||||
|
||||
|
||||
# Implicit agreements
|
||||
If we approve an issue, we are promising you the following:
|
||||
- Your work will receive timely attention and we will put aside other high priority items to ensure you are not blocked.
|
||||
- You will receive necessary coaching on eng quality, system design, etc. to ensure the feature is completed well.
|
||||
- The Onyx team will pull resources and bandwidth from design, PM, and engineering to ensure that you have all the
|
||||
resources to build the feature to the quality required for merging.
|
||||
|
||||
Because this is a large investment from our team, we ask that you:
|
||||
- Thoroughly read all the requirements of the design docs, engineering best practices, and try to minimize overhead for
|
||||
the Onyx team.
|
||||
- Complete the feature in a timely manner to reduce context switching and an ongoing resource pull from the Onyx team.
|
||||
205
contributing_guides/dev_setup.md
Normal file
205
contributing_guides/dev_setup.md
Normal file
@@ -0,0 +1,205 @@
|
||||
## Get Started 🚀
|
||||
|
||||
Onyx being a fully functional app, relies on some external software, specifically:
|
||||
|
||||
- [Postgres](https://www.postgresql.org/) (Relational DB)
|
||||
- [Vespa](https://vespa.ai/) (Vector DB/Search Engine)
|
||||
- [Redis](https://redis.io/) (Cache)
|
||||
- [MinIO](https://min.io/) (File Store)
|
||||
- [Nginx](https://nginx.org/) (Not needed for development flows generally)
|
||||
|
||||
> **Note:**
|
||||
> This guide provides instructions to build and run Onyx locally from source with Docker containers providing the above external software. We believe this combination is easier for
|
||||
> development purposes. If you prefer to use pre-built container images, we provide instructions on running the full Onyx stack within Docker below.
|
||||
|
||||
### Local Set Up
|
||||
|
||||
Be sure to use Python version 3.11. For instructions on installing Python 3.11 on macOS, refer to the [contributing_macos.md](./contributing_macos.md) readme.
|
||||
|
||||
If using a lower version, modifications will have to be made to the code.
|
||||
If using a higher version, sometimes some libraries will not be available (i.e. we had problems with Tensorflow in the past with higher versions of python).
|
||||
|
||||
#### Backend: Python requirements
|
||||
|
||||
Currently, we use [uv](https://docs.astral.sh/uv/) and recommend creating a [virtual environment](https://docs.astral.sh/uv/pip/environments/#using-a-virtual-environment).
|
||||
|
||||
For convenience here's a command for it:
|
||||
|
||||
```bash
|
||||
uv venv .venv --python 3.11
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
_For Windows, activate the virtual environment using Command Prompt:_
|
||||
|
||||
```bash
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
If using PowerShell, the command slightly differs:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
Install the required python dependencies:
|
||||
|
||||
```bash
|
||||
uv sync --all-extras
|
||||
```
|
||||
|
||||
Install Playwright for Python (headless browser required by the Web Connector):
|
||||
|
||||
```bash
|
||||
uv run playwright install
|
||||
```
|
||||
|
||||
#### Frontend: Node dependencies
|
||||
|
||||
Onyx uses Node v22.20.0. We highly recommend you use [Node Version Manager (nvm)](https://github.com/nvm-sh/nvm)
|
||||
to manage your Node installations. Once installed, you can run
|
||||
|
||||
```bash
|
||||
nvm install 22 && nvm use 22
|
||||
node -v # verify your active version
|
||||
```
|
||||
|
||||
Navigate to `onyx/web` and run:
|
||||
|
||||
```bash
|
||||
npm i
|
||||
```
|
||||
|
||||
## Formatting and Linting
|
||||
|
||||
### Backend
|
||||
|
||||
For the backend, you'll need to setup pre-commit hooks (black / reorder-python-imports).
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
uv run pre-commit install
|
||||
```
|
||||
|
||||
Additionally, we use `mypy` for static type checking.
|
||||
Onyx is fully type-annotated, and we want to keep it that way!
|
||||
To run the mypy checks manually, run `uv run mypy .` from the `onyx/backend` directory.
|
||||
|
||||
### Web
|
||||
|
||||
We use `prettier` for formatting. The desired version will be installed via a `npm i` from the `onyx/web` directory.
|
||||
To run the formatter, use `npx prettier --write .` from the `onyx/web` directory.
|
||||
|
||||
Pre-commit will also run prettier automatically on files you've recently touched. If re-formatted, your commit will fail.
|
||||
Re-stage your changes and commit again.
|
||||
|
||||
# Running the application for development
|
||||
|
||||
## Developing using VSCode Debugger (recommended)
|
||||
|
||||
**We highly recommend using VSCode debugger for development.**
|
||||
See [contributing_vscode.md](./contributing_vscode.md) for more details.
|
||||
|
||||
Otherwise, you can follow the instructions below to run the application for development.
|
||||
|
||||
## Manually running the application for development
|
||||
### Docker containers for external software
|
||||
|
||||
You will need Docker installed to run these containers.
|
||||
|
||||
First navigate to `onyx/deployment/docker_compose`, then start up Postgres/Vespa/Redis/MinIO with:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d index relational_db cache minio
|
||||
```
|
||||
|
||||
(index refers to Vespa, relational_db refers to Postgres, and cache refers to Redis)
|
||||
|
||||
### Running Onyx locally
|
||||
|
||||
To start the frontend, navigate to `onyx/web` and run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Next, start the model server which runs the local NLP models.
|
||||
Navigate to `onyx/backend` and run:
|
||||
|
||||
```bash
|
||||
uvicorn model_server.main:app --reload --port 9000
|
||||
```
|
||||
|
||||
_For Windows (for compatibility with both PowerShell and Command Prompt):_
|
||||
|
||||
```bash
|
||||
powershell -Command "uvicorn model_server.main:app --reload --port 9000"
|
||||
```
|
||||
|
||||
The first time running Onyx, you will need to run the DB migrations for Postgres.
|
||||
After the first time, this is no longer required unless the DB models change.
|
||||
|
||||
Navigate to `onyx/backend` and with the venv active, run:
|
||||
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
Next, start the task queue which orchestrates the background jobs.
|
||||
Jobs that take more time are run async from the API server.
|
||||
|
||||
Still in `onyx/backend`, run:
|
||||
|
||||
```bash
|
||||
python ./scripts/dev_run_background_jobs.py
|
||||
```
|
||||
|
||||
To run the backend API server, navigate back to `onyx/backend` and run:
|
||||
|
||||
```bash
|
||||
AUTH_TYPE=basic uvicorn onyx.main:app --reload --port 8080
|
||||
```
|
||||
|
||||
_For Windows (for compatibility with both PowerShell and Command Prompt):_
|
||||
|
||||
```bash
|
||||
powershell -Command "
|
||||
$env:AUTH_TYPE='basic'
|
||||
uvicorn onyx.main:app --reload --port 8080
|
||||
"
|
||||
```
|
||||
|
||||
> **Note:**
|
||||
> If you need finer logging, add the additional environment variable `LOG_LEVEL=DEBUG` to the relevant services.
|
||||
|
||||
#### Wrapping up
|
||||
|
||||
You should now have 4 servers running:
|
||||
|
||||
- Web server
|
||||
- Backend API
|
||||
- Model server
|
||||
- Background jobs
|
||||
|
||||
Now, visit `http://localhost:3000` in your browser. You should see the Onyx onboarding wizard where you can connect your external LLM provider to Onyx.
|
||||
|
||||
You've successfully set up a local Onyx instance! 🏁
|
||||
|
||||
#### Running the Onyx application in a container
|
||||
|
||||
You can run the full Onyx application stack from pre-built images including all external software dependencies.
|
||||
|
||||
Navigate to `onyx/deployment/docker_compose` and run:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
After Docker pulls and starts these containers, navigate to `http://localhost:3000` to use Onyx.
|
||||
|
||||
If you want to make changes to Onyx and run those changes in Docker, you can also build a local version of the Onyx container images that incorporates your changes like so:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
@@ -31,7 +31,7 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
# Main backend application dependencies
|
||||
backend = [
|
||||
"aiohttp==3.13.4",
|
||||
"aiohttp==3.13.3",
|
||||
"alembic==1.10.4",
|
||||
"asyncpg==0.30.0",
|
||||
"atlassian-python-api==3.41.16",
|
||||
@@ -49,7 +49,7 @@ backend = [
|
||||
"fastapi-users==15.0.4",
|
||||
"fastapi-users-db-sqlalchemy==7.0.0",
|
||||
"fastapi-limiter==0.1.6",
|
||||
"fastmcp==3.2.0",
|
||||
"fastmcp==3.0.2",
|
||||
"filelock==3.20.3",
|
||||
"google-api-python-client==2.86.0",
|
||||
"google-auth-httplib2==0.1.0",
|
||||
|
||||
194
uv.lock
generated
194
uv.lock
generated
@@ -118,7 +118,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.13.4"
|
||||
version = "3.13.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -129,93 +129,93 @@ dependencies = [
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/7e/cb94129302d78c46662b47f9897d642fd0b33bdfef4b73b20c6ced35aa4c/aiohttp-3.13.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8ea0c64d1bcbf201b285c2246c51a0c035ba3bbd306640007bc5844a3b4658c1", size = 760027, upload-time = "2026-03-28T17:15:33.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/cd/2db3c9397c3bd24216b203dd739945b04f8b87bb036c640da7ddb63c75ef/aiohttp-3.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f742e1fa45c0ed522b00ede565e18f97e4cf8d1883a712ac42d0339dfb0cce7", size = 508325, upload-time = "2026-03-28T17:15:34.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/a3/d28b2722ec13107f2e37a86b8a169897308bab6a3b9e071ecead9d67bd9b/aiohttp-3.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dcfb50ee25b3b7a1222a9123be1f9f89e56e67636b561441f0b304e25aaef8f", size = 502402, upload-time = "2026-03-28T17:15:36.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/d6/acd47b5f17c4430e555590990a4746efbcb2079909bb865516892bf85f37/aiohttp-3.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3262386c4ff370849863ea93b9ea60fd59c6cf56bf8f93beac625cf4d677c04d", size = 1771224, upload-time = "2026-03-28T17:15:38.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/af/af6e20113ba6a48fd1cd9e5832c4851e7613ef50c7619acdaee6ec5f1aff/aiohttp-3.13.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:473bb5aa4218dd254e9ae4834f20e31f5a0083064ac0136a01a62ddbae2eaa42", size = 1731530, upload-time = "2026-03-28T17:15:39.988Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/16/78a2f5d9c124ad05d5ce59a9af94214b6466c3491a25fb70760e98e9f762/aiohttp-3.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56423766399b4c77b965f6aaab6c9546617b8994a956821cc507d00b91d978c", size = 1827925, upload-time = "2026-03-28T17:15:41.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/1f/79acf0974ced805e0e70027389fccbb7d728e6f30fcac725fb1071e63075/aiohttp-3.13.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8af249343fafd5ad90366a16d230fc265cf1149f26075dc9fe93cfd7c7173942", size = 1923579, upload-time = "2026-03-28T17:15:44.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/53/29f9e2054ea6900413f3b4c3eb9d8331f60678ec855f13ba8714c47fd48d/aiohttp-3.13.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc0a5cf4f10ef5a2c94fdde488734b582a3a7a000b131263e27c9295bd682d9", size = 1767655, upload-time = "2026-03-28T17:15:45.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/57/462fe1d3da08109ba4aa8590e7aed57c059af2a7e80ec21f4bac5cfe1094/aiohttp-3.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5c7ff1028e3c9fc5123a865ce17df1cb6424d180c503b8517afbe89aa566e6be", size = 1630439, upload-time = "2026-03-28T17:15:48.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/4b/4813344aacdb8127263e3eec343d24e973421143826364fa9fc847f6283f/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba5cf98b5dcb9bddd857da6713a503fa6d341043258ca823f0f5ab7ab4a94ee8", size = 1745557, upload-time = "2026-03-28T17:15:50.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/01/1ef1adae1454341ec50a789f03cfafe4c4ac9c003f6a64515ecd32fe4210/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d85965d3ba21ee4999e83e992fecb86c4614d6920e40705501c0a1f80a583c12", size = 1741796, upload-time = "2026-03-28T17:15:52.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/04/8cdd99af988d2aa6922714d957d21383c559835cbd43fbf5a47ddf2e0f05/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:49f0b18a9b05d79f6f37ddd567695943fcefb834ef480f17a4211987302b2dc7", size = 1805312, upload-time = "2026-03-28T17:15:54.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/7f/b48d5577338d4b25bbdbae35c75dbfd0493cb8886dc586fbfb2e90862239/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7f78cb080c86fbf765920e5f1ef35af3f24ec4314d6675d0a21eaf41f6f2679c", size = 1621751, upload-time = "2026-03-28T17:15:56.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/89/4eecad8c1858e6d0893c05929e22343e0ebe3aec29a8a399c65c3cc38311/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:67a3ec705534a614b68bbf1c70efa777a21c3da3895d1c44510a41f5a7ae0453", size = 1826073, upload-time = "2026-03-28T17:15:58.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5c/9dc8293ed31b46c39c9c513ac7ca152b3c3d38e0ea111a530ad12001b827/aiohttp-3.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6630ec917e85c5356b2295744c8a97d40f007f96a1c76bf1928dc2e27465393", size = 1760083, upload-time = "2026-03-28T17:16:00.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/19/8bbf6a4994205d96831f97b7d21a0feed120136e6267b5b22d229c6dc4dc/aiohttp-3.13.4-cp311-cp311-win32.whl", hash = "sha256:54049021bc626f53a5394c29e8c444f726ee5a14b6e89e0ad118315b1f90f5e3", size = 439690, upload-time = "2026-03-28T17:16:02.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/f5/ac409ecd1007528d15c3e8c3a57d34f334c70d76cfb7128a28cffdebd4c1/aiohttp-3.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:c033f2bc964156030772d31cbf7e5defea181238ce1f87b9455b786de7d30145", size = 463824, upload-time = "2026-03-28T17:16:05.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/bd/ede278648914cabbabfdf95e436679b5d4156e417896a9b9f4587169e376/aiohttp-3.13.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ee62d4471ce86b108b19c3364db4b91180d13fe3510144872d6bad5401957360", size = 752158, upload-time = "2026-03-28T17:16:06.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/de/581c053253c07b480b03785196ca5335e3c606a37dc73e95f6527f1591fe/aiohttp-3.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c0fd8f41b54b58636402eb493afd512c23580456f022c1ba2db0f810c959ed0d", size = 501037, upload-time = "2026-03-28T17:16:08.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f9/a5ede193c08f13cc42c0a5b50d1e246ecee9115e4cf6e900d8dbd8fd6acb/aiohttp-3.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4baa48ce49efd82d6b1a0be12d6a36b35e5594d1dd42f8bfba96ea9f8678b88c", size = 501556, upload-time = "2026-03-28T17:16:10.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/10/88ff67cd48a6ec36335b63a640abe86135791544863e0cfe1f065d6cef7a/aiohttp-3.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d738ebab9f71ee652d9dbd0211057690022201b11197f9a7324fd4dba128aa97", size = 1757314, upload-time = "2026-03-28T17:16:12.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/15/fdb90a5cf5a1f52845c276e76298c75fbbcc0ac2b4a86551906d54529965/aiohttp-3.13.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ce692c3468fa831af7dceed52edf51ac348cebfc8d3feb935927b63bd3e8576", size = 1731819, upload-time = "2026-03-28T17:16:14.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/df/28146785a007f7820416be05d4f28cc207493efd1e8c6c1068e9bdc29198/aiohttp-3.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e08abcfe752a454d2cb89ff0c08f2d1ecd057ae3e8cc6d84638de853530ebab", size = 1793279, upload-time = "2026-03-28T17:16:16.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/47/689c743abf62ea7a77774d5722f220e2c912a77d65d368b884d9779ef41b/aiohttp-3.13.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5977f701b3fff36367a11087f30ea73c212e686d41cd363c50c022d48b011d8d", size = 1891082, upload-time = "2026-03-28T17:16:18.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/b6/f7f4f318c7e58c23b761c9b13b9a3c9b394e0f9d5d76fbc6622fa98509f6/aiohttp-3.13.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54203e10405c06f8b6020bd1e076ae0fe6c194adcee12a5a78af3ffa3c57025e", size = 1773938, upload-time = "2026-03-28T17:16:21.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/06/f207cb3121852c989586a6fc16ff854c4fcc8651b86c5d3bd1fc83057650/aiohttp-3.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:358a6af0145bc4dda037f13167bef3cce54b132087acc4c295c739d05d16b1c3", size = 1579548, upload-time = "2026-03-28T17:16:23.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/58/e1289661a32161e24c1fe479711d783067210d266842523752869cc1d9c2/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:898ea1850656d7d61832ef06aa9846ab3ddb1621b74f46de78fbc5e1a586ba83", size = 1714669, upload-time = "2026-03-28T17:16:25.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/0a/3e86d039438a74a86e6a948a9119b22540bae037d6ba317a042ae3c22711/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7bc30cceb710cf6a44e9617e43eebb6e3e43ad855a34da7b4b6a73537d8a6763", size = 1754175, upload-time = "2026-03-28T17:16:28.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/30/e717fc5df83133ba467a560b6d8ef20197037b4bb5d7075b90037de1018e/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4a31c0c587a8a038f19a4c7e60654a6c899c9de9174593a13e7cc6e15ff271f9", size = 1762049, upload-time = "2026-03-28T17:16:30.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/28/8f7a2d4492e336e40005151bdd94baf344880a4707573378579f833a64c1/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2062f675f3fe6e06d6113eb74a157fb9df58953ffed0cdb4182554b116545758", size = 1570861, upload-time = "2026-03-28T17:16:32.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/45/12e1a3d0645968b1c38de4b23fdf270b8637735ea057d4f84482ff918ad9/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d1ba8afb847ff80626d5e408c1fdc99f942acc877d0702fe137015903a220a9", size = 1790003, upload-time = "2026-03-28T17:16:35.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/0f/60374e18d590de16dcb39d6ff62f39c096c1b958e6f37727b5870026ea30/aiohttp-3.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b08149419994cdd4d5eecf7fd4bc5986b5a9380285bcd01ab4c0d6bfca47b79d", size = 1737289, upload-time = "2026-03-28T17:16:38.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/bf/535e58d886cfbc40a8b0013c974afad24ef7632d645bca0b678b70033a60/aiohttp-3.13.4-cp312-cp312-win32.whl", hash = "sha256:fc432f6a2c4f720180959bc19aa37259651c1a4ed8af8afc84dd41c60f15f791", size = 434185, upload-time = "2026-03-28T17:16:40.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/1a/d92e3325134ebfff6f4069f270d3aac770d63320bd1fcd0eca023e74d9a8/aiohttp-3.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:6148c9ae97a3e8bff9a1fc9c757fa164116f86c100468339730e717590a3fb77", size = 461285, upload-time = "2026-03-28T17:16:42.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/29/6657cc37ae04cacc2dbf53fb730a06b6091cc4cbe745028e047c53e6d840/aiohttp-3.13.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:e0a2c961fc92abeff61d6444f2ce6ad35bb982db9fc8ff8a47455beacf454a57", size = 749363, upload-time = "2026-03-28T17:17:24.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/7f/30ccdf67ca3d24b610067dc63d64dcb91e5d88e27667811640644aa4a85d/aiohttp-3.13.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:153274535985a0ff2bff1fb6c104ed547cec898a09213d21b0f791a44b14d933", size = 499317, upload-time = "2026-03-28T17:17:26.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/13/e372dd4e68ad04ee25dafb050c7f98b0d91ea643f7352757e87231102555/aiohttp-3.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:351f3171e2458da3d731ce83f9e6b9619e325c45cbd534c7759750cabf453ad7", size = 500477, upload-time = "2026-03-28T17:17:28.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/fe/ee6298e8e586096fb6f5eddd31393d8544f33ae0792c71ecbb4c2bef98ac/aiohttp-3.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f989ac8bc5595ff761a5ccd32bdb0768a117f36dd1504b1c2c074ed5d3f4df9c", size = 1737227, upload-time = "2026-03-28T17:17:30.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/b9/a7a0463a09e1a3fe35100f74324f23644bfc3383ac5fd5effe0722a5f0b7/aiohttp-3.13.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d36fc1709110ec1e87a229b201dd3ddc32aa01e98e7868083a794609b081c349", size = 1694036, upload-time = "2026-03-28T17:17:33.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/7c/8972ae3fb7be00a91aee6b644b2a6a909aedb2c425269a3bfd90115e6f8f/aiohttp-3.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42adaeea83cbdf069ab94f5103ce0787c21fb1a0153270da76b59d5578302329", size = 1786814, upload-time = "2026-03-28T17:17:36.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/01/c81e97e85c774decbaf0d577de7d848934e8166a3a14ad9f8aa5be329d28/aiohttp-3.13.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:92deb95469928cc41fd4b42a95d8012fa6df93f6b1c0a83af0ffbc4a5e218cde", size = 1866676, upload-time = "2026-03-28T17:17:38.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/5f/5b46fe8694a639ddea2cd035bf5729e4677ea882cb251396637e2ef1590d/aiohttp-3.13.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0c7c07c4257ef3a1df355f840bc62d133bcdef5c1c5ba75add3c08553e2eed", size = 1740842, upload-time = "2026-03-28T17:17:40.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/a2/0d4b03d011cca6b6b0acba8433193c1e484efa8d705ea58295590fe24203/aiohttp-3.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f062c45de8a1098cb137a1898819796a2491aec4e637a06b03f149315dff4d8f", size = 1566508, upload-time = "2026-03-28T17:17:43.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/17/e689fd500da52488ec5f889effd6404dece6a59de301e380f3c64f167beb/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:76093107c531517001114f0ebdb4f46858ce818590363e3e99a4a2280334454a", size = 1700569, upload-time = "2026-03-28T17:17:46.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/0d/66402894dbcf470ef7db99449e436105ea862c24f7ea4c95c683e635af35/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6f6ec32162d293b82f8b63a16edc80769662fbd5ae6fbd4936d3206a2c2cc63b", size = 1707407, upload-time = "2026-03-28T17:17:48.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/eb/af0ab1a3650092cbd8e14ef29e4ab0209e1460e1c299996c3f8288b3f1ff/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5903e2db3d202a00ad9f0ec35a122c005e85d90c9836ab4cda628f01edf425e2", size = 1752214, upload-time = "2026-03-28T17:17:51.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/bf/72326f8a98e4c666f292f03c385545963cc65e358835d2a7375037a97b57/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2d5bea57be7aca98dbbac8da046d99b5557c5cf4e28538c4c786313078aca09e", size = 1562162, upload-time = "2026-03-28T17:17:53.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/9f/13b72435f99151dd9a5469c96b3b5f86aa29b7e785ca7f35cf5e538f74c0/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bcf0c9902085976edc0232b75006ef38f89686901249ce14226b6877f88464fb", size = 1768904, upload-time = "2026-03-28T17:17:55.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/bc/28d4970e7d5452ac7776cdb5431a1164a0d9cf8bd2fffd67b4fb463aa56d/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3295f98bfeed2e867cab588f2a146a9db37a85e3ae9062abf46ba062bd29165", size = 1723378, upload-time = "2026-03-28T17:17:58.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/74/b32458ca1a7f34d65bdee7aef2036adbe0438123d3d53e2b083c453c24dd/aiohttp-3.13.4-cp314-cp314-win32.whl", hash = "sha256:a598a5c5767e1369d8f5b08695cab1d8160040f796c4416af76fd773d229b3c9", size = 438711, upload-time = "2026-03-28T17:18:00.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/b2/54b487316c2df3e03a8f3435e9636f8a81a42a69d942164830d193beb56a/aiohttp-3.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:c555db4bc7a264bead5a7d63d92d41a1122fcd39cc62a4db815f45ad46f9c2c8", size = 464977, upload-time = "2026-03-28T17:18:03.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/fb/e41b63c6ce71b07a59243bb8f3b457ee0c3402a619acb9d2c0d21ef0e647/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45abbbf09a129825d13c18c7d3182fecd46d9da3cfc383756145394013604ac1", size = 781549, upload-time = "2026-03-28T17:18:05.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/53/532b8d28df1e17e44c4d9a9368b78dcb6bf0b51037522136eced13afa9e8/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:74c80b2bc2c2adb7b3d1941b2b60701ee2af8296fc8aad8b8bc48bc25767266c", size = 514383, upload-time = "2026-03-28T17:18:08.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/1f/62e5d400603e8468cd635812d99cb81cfdc08127a3dc474c647615f31339/aiohttp-3.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c97989ae40a9746650fa196894f317dafc12227c808c774929dda0ff873a5954", size = 518304, upload-time = "2026-03-28T17:18:10.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/57/2326b37b10896447e3c6e0cbef4fe2486d30913639a5cfd1332b5d870f82/aiohttp-3.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dae86be9811493f9990ef44fff1685f5c1a3192e9061a71a109d527944eed551", size = 1893433, upload-time = "2026-03-28T17:18:13.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/b4/a24d82112c304afdb650167ef2fe190957d81cbddac7460bedd245f765aa/aiohttp-3.13.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1db491abe852ca2fa6cc48a3341985b0174b3741838e1341b82ac82c8bd9e871", size = 1755901, upload-time = "2026-03-28T17:18:16.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/2d/0883ef9d878d7846287f036c162a951968f22aabeef3ac97b0bea6f76d5d/aiohttp-3.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e5d701c0aad02a7dce72eef6b93226cf3734330f1a31d69ebbf69f33b86666e", size = 1876093, upload-time = "2026-03-28T17:18:18.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/52/9204bb59c014869b71971addad6778f005daa72a96eed652c496789d7468/aiohttp-3.13.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ac32a189081ae0a10ba18993f10f338ec94341f0d5df8fff348043962f3c6f8", size = 1970815, upload-time = "2026-03-28T17:18:21.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/b5/e4eb20275a866dde0f570f411b36c6b48f7b53edfe4f4071aa1b0728098a/aiohttp-3.13.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e968cdaba43e45c73c3f306fca418c8009a957733bac85937c9f9cf3f4de27", size = 1816223, upload-time = "2026-03-28T17:18:24.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/23/e98075c5bb146aa61a1239ee1ac7714c85e814838d6cebbe37d3fe19214a/aiohttp-3.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca114790c9144c335d538852612d3e43ea0f075288f4849cf4b05d6cd2238ce7", size = 1649145, upload-time = "2026-03-28T17:18:27.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/c1/7bad8be33bb06c2bb224b6468874346026092762cbec388c3bdb65a368ee/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ea2e071661ba9cfe11eabbc81ac5376eaeb3061f6e72ec4cc86d7cdd1ffbdbbb", size = 1816562, upload-time = "2026-03-28T17:18:29.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/10/c00323348695e9a5e316825969c88463dcc24c7e9d443244b8a2c9cf2eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:34e89912b6c20e0fd80e07fa401fd218a410aa1ce9f1c2f1dad6db1bd0ce0927", size = 1800333, upload-time = "2026-03-28T17:18:32.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/43/9b2147a1df3559f49bd723e22905b46a46c068a53adb54abdca32c4de180/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0e217cf9f6a42908c52b46e42c568bd57adc39c9286ced31aaace614b6087965", size = 1820617, upload-time = "2026-03-28T17:18:35.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/7f/b3481a81e7a586d02e99387b18c6dafff41285f6efd3daa2124c01f87eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:0c296f1221e21ba979f5ac1964c3b78cfde15c5c5f855ffd2caab337e9cd9182", size = 1643417, upload-time = "2026-03-28T17:18:37.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/72/07181226bc99ce1124e0f89280f5221a82d3ae6a6d9d1973ce429d48e52b/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d99a9d168ebaffb74f36d011750e490085ac418f4db926cce3989c8fe6cb6b1b", size = 1849286, upload-time = "2026-03-28T17:18:40.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/e6/1b3566e103eca6da5be4ae6713e112a053725c584e96574caf117568ffef/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cb19177205d93b881f3f89e6081593676043a6828f59c78c17a0fd6c1fbed2ba", size = 1782635, upload-time = "2026-03-28T17:18:43.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/58/1b11c71904b8d079eb0c39fe664180dd1e14bebe5608e235d8bfbadc8929/aiohttp-3.13.4-cp314-cp314t-win32.whl", hash = "sha256:c606aa5656dab6552e52ca368e43869c916338346bfaf6304e15c58fb113ea30", size = 472537, upload-time = "2026-03-28T17:18:46.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/8f/87c56a1a1977d7dddea5b31e12189665a140fdb48a71e9038ff90bb564ec/aiohttp-3.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:014dcc10ec8ab8db681f0d68e939d1e9286a5aa2b993cbbdb0db130853e02144", size = 506381, upload-time = "2026-03-28T17:18:48.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1809,7 +1809,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastmcp"
|
||||
version = "3.2.0"
|
||||
version = "3.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "authlib" },
|
||||
@@ -1829,14 +1829,13 @@ dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "rich" },
|
||||
{ name = "uncalled-for" },
|
||||
{ name = "uvicorn" },
|
||||
{ name = "watchfiles" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/32/4f1b2cfd7b50db89114949f90158b1dcc2c92a1917b9f57c0ff24e47a2f4/fastmcp-3.2.0.tar.gz", hash = "sha256:d4830b8ffc3592d3d9c76dc0f398904cf41f04910e41a0de38cc1004e0903bef", size = 26318581, upload-time = "2026-03-30T20:25:37.692Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/6b/1a7ec89727797fb07ec0928e9070fa2f45e7b35718e1fe01633a34c35e45/fastmcp-3.0.2.tar.gz", hash = "sha256:6bd73b4a3bab773ee6932df5249dcbcd78ed18365ed0aeeb97bb42702a7198d7", size = 17239351, upload-time = "2026-02-22T16:32:28.843Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/67/684fa2d2de1e7504549d4ca457b4f854ccec3cd3be03bd86b33b599fbf58/fastmcp-3.2.0-py3-none-any.whl", hash = "sha256:e71aba3df16f86f546a4a9e513261d3233bcc92bef0dfa647bac3fa33623f681", size = 705550, upload-time = "2026-03-30T20:25:35.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/5a/f410a9015cfde71adf646dab4ef2feae49f92f34f6050fcfb265eb126b30/fastmcp-3.0.2-py3-none-any.whl", hash = "sha256:f513d80d4b30b54749fe8950116b1aab843f3c293f5cb971fc8665cb48dbb028", size = 606268, upload-time = "2026-02-22T16:32:30.992Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4391,7 +4390,7 @@ requires-dist = [
|
||||
{ name = "accelerate", marker = "extra == 'model-server'", specifier = "==1.6.0" },
|
||||
{ name = "agent-client-protocol", specifier = ">=0.7.1" },
|
||||
{ name = "aioboto3", specifier = "==15.1.0" },
|
||||
{ name = "aiohttp", marker = "extra == 'backend'", specifier = "==3.13.4" },
|
||||
{ name = "aiohttp", marker = "extra == 'backend'", specifier = "==3.13.3" },
|
||||
{ name = "alembic", marker = "extra == 'backend'", specifier = "==1.10.4" },
|
||||
{ name = "asana", marker = "extra == 'backend'", specifier = "==5.0.8" },
|
||||
{ name = "asyncpg", marker = "extra == 'backend'", specifier = "==0.30.0" },
|
||||
@@ -4422,7 +4421,7 @@ requires-dist = [
|
||||
{ name = "fastapi-limiter", marker = "extra == 'backend'", specifier = "==0.1.6" },
|
||||
{ name = "fastapi-users", marker = "extra == 'backend'", specifier = "==15.0.4" },
|
||||
{ name = "fastapi-users-db-sqlalchemy", marker = "extra == 'backend'", specifier = "==7.0.0" },
|
||||
{ name = "fastmcp", marker = "extra == 'backend'", specifier = "==3.2.0" },
|
||||
{ name = "fastmcp", marker = "extra == 'backend'", specifier = "==3.0.2" },
|
||||
{ name = "filelock", marker = "extra == 'backend'", specifier = "==3.20.3" },
|
||||
{ name = "google-api-python-client", marker = "extra == 'backend'", specifier = "==2.86.0" },
|
||||
{ name = "google-auth-httplib2", marker = "extra == 'backend'", specifier = "==0.1.0" },
|
||||
@@ -7574,15 +7573,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uncalled-for"
|
||||
version = "0.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unstructured"
|
||||
version = "0.18.27"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* ---------------------------------------------------------------------------
|
||||
Foldable — CSS grid collapse/expand animation.
|
||||
|
||||
Expands when an ancestor `.interactive` element is hovered, focused
|
||||
within, or has `data-interaction="hover"` / `data-interaction="active"`.
|
||||
Expands when an ancestor `.interactive` element is hovered or has
|
||||
`data-interaction="hover"` / `data-interaction="active"`.
|
||||
|
||||
Structure:
|
||||
.interactive-foldable-host — flex parent, gap transitions 0 → 0.25rem
|
||||
@@ -19,7 +19,6 @@
|
||||
}
|
||||
|
||||
.interactive:hover:not([data-disabled]) .interactive-foldable-host,
|
||||
.interactive:focus-within:not([data-disabled]) .interactive-foldable-host,
|
||||
.interactive[data-interaction="hover"]:not([data-disabled])
|
||||
.interactive-foldable-host,
|
||||
.interactive[data-interaction="active"]:not([data-disabled])
|
||||
@@ -44,9 +43,8 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Expanded: hovered, focused within, or interaction override */
|
||||
/* Expanded: hovered or interaction override */
|
||||
.interactive:hover:not([data-disabled]) .interactive-foldable,
|
||||
.interactive:focus-within:not([data-disabled]) .interactive-foldable,
|
||||
.interactive[data-interaction="hover"]:not([data-disabled])
|
||||
.interactive-foldable,
|
||||
.interactive[data-interaction="active"]:not([data-disabled])
|
||||
|
||||
@@ -1,52 +1,43 @@
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { SlackBot, SlackChannelConfig } from "@/lib/types";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
export const useSlackChannelConfigs = () => {
|
||||
const swrResponse = useSWR<SlackChannelConfig[]>(
|
||||
SWR_KEYS.slackChannels,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
const url = "/api/manage/admin/slack-app/channel";
|
||||
const swrResponse = useSWR<SlackChannelConfig[]>(url, errorHandlingFetcher);
|
||||
|
||||
return {
|
||||
...swrResponse,
|
||||
refreshSlackChannelConfigs: () => mutate(SWR_KEYS.slackChannels),
|
||||
refreshSlackChannelConfigs: () => mutate(url),
|
||||
};
|
||||
};
|
||||
|
||||
export const useSlackBots = () => {
|
||||
const swrResponse = useSWR<SlackBot[]>(
|
||||
SWR_KEYS.slackBots,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
const url = "/api/manage/admin/slack-app/bots";
|
||||
const swrResponse = useSWR<SlackBot[]>(url, errorHandlingFetcher);
|
||||
|
||||
return {
|
||||
...swrResponse,
|
||||
refreshSlackBots: () => mutate(SWR_KEYS.slackBots),
|
||||
refreshSlackBots: () => mutate(url),
|
||||
};
|
||||
};
|
||||
|
||||
export const useSlackBot = (botId: number) => {
|
||||
const swrResponse = useSWR<SlackBot>(
|
||||
SWR_KEYS.slackBot(botId),
|
||||
errorHandlingFetcher
|
||||
);
|
||||
const url = `/api/manage/admin/slack-app/bots/${botId}`;
|
||||
const swrResponse = useSWR<SlackBot>(url, errorHandlingFetcher);
|
||||
|
||||
return {
|
||||
...swrResponse,
|
||||
refreshSlackBot: () => mutate(SWR_KEYS.slackBot(botId)),
|
||||
refreshSlackBot: () => mutate(url),
|
||||
};
|
||||
};
|
||||
|
||||
export const useSlackChannelConfigsByBot = (botId: number) => {
|
||||
const swrResponse = useSWR<SlackChannelConfig[]>(
|
||||
SWR_KEYS.slackBotConfig(botId),
|
||||
errorHandlingFetcher
|
||||
);
|
||||
const url = `/api/manage/admin/slack-app/bots/${botId}/config`;
|
||||
const swrResponse = useSWR<SlackChannelConfig[]>(url, errorHandlingFetcher);
|
||||
|
||||
return {
|
||||
...swrResponse,
|
||||
refreshSlackChannelConfigs: () => mutate(SWR_KEYS.slackBotConfig(botId)),
|
||||
refreshSlackChannelConfigs: () => mutate(url),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import CardSection from "@/components/admin/CardSection";
|
||||
import { Button } from "@opal/components";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
@@ -23,7 +22,7 @@ function Main() {
|
||||
isLoading,
|
||||
} = useSWR<{
|
||||
unstructured_api_key: string | null;
|
||||
}>(SWR_KEYS.unstructuredApiKeySet, (url: string) =>
|
||||
}>("/api/search-settings/unstructured-api-key-set", (url: string) =>
|
||||
fetch(url).then((res) => res.json())
|
||||
);
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import Button from "@/refresh-components/buttons/Button";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import { useMemo, useState } from "react";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { ReindexingProgressTable } from "../../../../components/embedding/ReindexingProgressTable";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import {
|
||||
@@ -39,7 +38,7 @@ export default function UpgradingPage({
|
||||
|
||||
const { data: connectors, isLoading: isLoadingConnectors } = useSWR<
|
||||
Connector<any>[]
|
||||
>(vectorDbEnabled ? SWR_KEYS.connector : null, errorHandlingFetcher, {
|
||||
>(vectorDbEnabled ? "/api/manage/connector" : null, errorHandlingFetcher, {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
@@ -70,7 +69,7 @@ export default function UpgradingPage({
|
||||
method: "POST",
|
||||
});
|
||||
if (response.ok) {
|
||||
mutate(SWR_KEYS.secondarySearchSettings);
|
||||
mutate("/api/search-settings/get-secondary-search-settings");
|
||||
} else {
|
||||
alert(
|
||||
`Failed to cancel embedding model update - ${await response.text()}`
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Text } from "@opal/components";
|
||||
import Title from "@/components/ui/title";
|
||||
import { Button } from "@opal/components";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { ModelPreview } from "@/components/embedding/ModelSelector";
|
||||
import {
|
||||
HostedEmbeddingModel,
|
||||
@@ -44,14 +43,14 @@ function Main() {
|
||||
isLoading: isLoadingCurrentModel,
|
||||
error: currentEmeddingModelError,
|
||||
} = useSWR<CloudEmbeddingModel | HostedEmbeddingModel | null>(
|
||||
SWR_KEYS.currentSearchSettings,
|
||||
"/api/search-settings/get-current-search-settings",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
|
||||
const { data: searchSettings, isLoading: isLoadingSearchSettings } =
|
||||
useSWR<SavedSearchSettings | null>(
|
||||
SWR_KEYS.currentSearchSettings,
|
||||
"/api/search-settings/get-current-search-settings",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
@@ -61,7 +60,7 @@ function Main() {
|
||||
isLoading: isLoadingFutureModel,
|
||||
error: futureEmeddingModelError,
|
||||
} = useSWR<CloudEmbeddingModel | HostedEmbeddingModel | null>(
|
||||
SWR_KEYS.secondarySearchSettings,
|
||||
"/api/search-settings/get-secondary-search-settings",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from "@/lib/connectors/credentials";
|
||||
import { refreshAllGoogleData } from "@/lib/googleConnector";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { buildSimilarCredentialInfoURL } from "@/app/admin/connector/[ccPairId]/lib";
|
||||
import { FiFile, FiCheck, FiLink, FiAlertTriangle } from "react-icons/fi";
|
||||
import { cn, truncateString } from "@/lib/utils";
|
||||
@@ -77,7 +76,7 @@ export const DriveJsonUpload = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
);
|
||||
if (response.ok) {
|
||||
toast.success("Successfully uploaded app credentials");
|
||||
mutate(SWR_KEYS.googleConnectorAppCredential("google-drive"));
|
||||
mutate("/api/manage/admin/connector/google-drive/app-credential");
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
@@ -100,7 +99,9 @@ export const DriveJsonUpload = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
);
|
||||
if (response.ok) {
|
||||
toast.success("Successfully uploaded service account key");
|
||||
mutate(SWR_KEYS.googleConnectorServiceAccountKey("google-drive"));
|
||||
mutate(
|
||||
"/api/manage/admin/connector/google-drive/service-account-key"
|
||||
);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
@@ -318,10 +319,8 @@ export const DriveJsonUploadSection = ({
|
||||
onClick={async () => {
|
||||
const endpoint =
|
||||
localServiceAccountData?.service_account_email
|
||||
? SWR_KEYS.googleConnectorServiceAccountKey(
|
||||
"google-drive"
|
||||
)
|
||||
: SWR_KEYS.googleConnectorAppCredential("google-drive");
|
||||
? "/api/manage/admin/connector/google-drive/service-account-key"
|
||||
: "/api/manage/admin/connector/google-drive/app-credential";
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "DELETE",
|
||||
@@ -335,14 +334,14 @@ export const DriveJsonUploadSection = ({
|
||||
);
|
||||
|
||||
// Add additional mutations to refresh all credential-related endpoints
|
||||
mutate(SWR_KEYS.googleConnectorCredentials("google-drive"));
|
||||
mutate(
|
||||
SWR_KEYS.googleConnectorPublicCredential("google-drive")
|
||||
"/api/manage/admin/connector/google-drive/credentials"
|
||||
);
|
||||
mutate(
|
||||
SWR_KEYS.googleConnectorServiceAccountCredential(
|
||||
"google-drive"
|
||||
)
|
||||
"/api/manage/admin/connector/google-drive/public-credential"
|
||||
);
|
||||
mutate(
|
||||
"/api/manage/admin/connector/google-drive/service-account-credential"
|
||||
);
|
||||
|
||||
toast.success(
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
} from "@/lib/connectors/credentials";
|
||||
import { refreshAllGoogleData } from "@/lib/googleConnector";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { buildSimilarCredentialInfoURL } from "@/app/admin/connector/[ccPairId]/lib";
|
||||
import { FiFile, FiCheck, FiLink, FiAlertTriangle } from "react-icons/fi";
|
||||
import { cn, truncateString } from "@/lib/utils";
|
||||
@@ -80,7 +79,7 @@ const GmailCredentialUpload = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
);
|
||||
if (response.ok) {
|
||||
toast.success("Successfully uploaded app credentials");
|
||||
mutate(SWR_KEYS.googleConnectorAppCredential("gmail"));
|
||||
mutate("/api/manage/admin/connector/gmail/app-credential");
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
@@ -103,7 +102,7 @@ const GmailCredentialUpload = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
);
|
||||
if (response.ok) {
|
||||
toast.success("Successfully uploaded service account key");
|
||||
mutate(SWR_KEYS.googleConnectorServiceAccountKey("gmail"));
|
||||
mutate("/api/manage/admin/connector/gmail/service-account-key");
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
@@ -320,8 +319,8 @@ export const GmailJsonUploadSection = ({
|
||||
onClick={async () => {
|
||||
const endpoint =
|
||||
localServiceAccountData?.service_account_email
|
||||
? SWR_KEYS.googleConnectorServiceAccountKey("gmail")
|
||||
: SWR_KEYS.googleConnectorAppCredential("gmail");
|
||||
? "/api/manage/admin/connector/gmail/service-account-key"
|
||||
: "/api/manage/admin/connector/gmail/app-credential";
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: "DELETE",
|
||||
@@ -333,10 +332,12 @@ export const GmailJsonUploadSection = ({
|
||||
mutate(buildSimilarCredentialInfoURL(ValidSources.Gmail));
|
||||
|
||||
// Add additional mutations to refresh all credential-related endpoints
|
||||
mutate(SWR_KEYS.googleConnectorCredentials("gmail"));
|
||||
mutate(SWR_KEYS.googleConnectorPublicCredential("gmail"));
|
||||
mutate("/api/manage/admin/connector/gmail/credentials");
|
||||
mutate(
|
||||
SWR_KEYS.googleConnectorServiceAccountCredential("gmail")
|
||||
"/api/manage/admin/connector/gmail/public-credential"
|
||||
);
|
||||
mutate(
|
||||
"/api/manage/admin/connector/gmail/service-account-credential"
|
||||
);
|
||||
|
||||
toast.success(
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { ADMIN_ROUTES } from "@/lib/admin-routes";
|
||||
|
||||
@@ -32,7 +31,7 @@ function formatTimestamp(iso: string): string {
|
||||
|
||||
function MigrationStatusSection() {
|
||||
const { data, isLoading, error } = useSWR<MigrationStatus>(
|
||||
SWR_KEYS.opensearchMigrationStatus,
|
||||
"/api/admin/opensearch-migration/status",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
@@ -122,7 +121,7 @@ function MigrationStatusSection() {
|
||||
|
||||
function RetrievalSourceSection() {
|
||||
const { data, isLoading, error, mutate } = useSWR<RetrievalStatus>(
|
||||
SWR_KEYS.opensearchMigrationRetrieval,
|
||||
"/api/admin/opensearch-migration/retrieval",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
const [selectedSource, setSelectedSource] = useState<string | null>(null);
|
||||
@@ -137,13 +136,16 @@ function RetrievalSourceSection() {
|
||||
async function handleUpdate() {
|
||||
setUpdating(true);
|
||||
try {
|
||||
const response = await fetch(SWR_KEYS.opensearchMigrationRetrieval, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
enable_opensearch_retrieval: currentValue === "opensearch",
|
||||
}),
|
||||
});
|
||||
const response = await fetch(
|
||||
"/api/admin/opensearch-migration/retrieval",
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
enable_opensearch_retrieval: currentValue === "opensearch",
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to update retrieval setting");
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
CloudEmbeddingProvider,
|
||||
getFormattedProviderName,
|
||||
} from "@/components/embedding/interfaces";
|
||||
import { EMBEDDING_PROVIDERS_ADMIN_URL } from "@/lib/llmConfig/constants";
|
||||
import {
|
||||
EMBEDDING_PROVIDERS_ADMIN_URL,
|
||||
LLM_PROVIDERS_ADMIN_URL,
|
||||
} from "@/lib/llmConfig/constants";
|
||||
import { mutate } from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { testEmbedding } from "@/app/admin/embeddings/pages/utils";
|
||||
import { SvgSettings } from "@opal/icons";
|
||||
|
||||
@@ -100,7 +102,7 @@ export default function ChangeCredentialsModal({
|
||||
return;
|
||||
}
|
||||
|
||||
mutate(SWR_KEYS.adminLlmProviders);
|
||||
mutate(LLM_PROVIDERS_ADMIN_URL);
|
||||
onDeleted();
|
||||
} catch (error) {
|
||||
setDeletionError(
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import AdvancedEmbeddingFormPage from "./AdvancedEmbeddingFormPage";
|
||||
import {
|
||||
@@ -120,7 +119,7 @@ export default function EmbeddingForm() {
|
||||
isLoading: isLoadingCurrentModel,
|
||||
error: currentEmbeddingModelError,
|
||||
} = useSWR<CloudEmbeddingModel | HostedEmbeddingModel | null>(
|
||||
SWR_KEYS.currentSearchSettings,
|
||||
"/api/search-settings/get-current-search-settings",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
@@ -131,7 +130,7 @@ export default function EmbeddingForm() {
|
||||
|
||||
const { data: searchSettings, isLoading: isLoadingSearchSettings } =
|
||||
useSWR<SavedSearchSettings | null>(
|
||||
SWR_KEYS.currentSearchSettings,
|
||||
"/api/search-settings/get-current-search-settings",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default } from "@/ee/refresh-pages/admin/HooksPage";
|
||||
export { default } from "@/refresh-pages/admin/HooksPage";
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
import { sanitizeKGConfig } from "@/app/admin/kg/utils";
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import Title from "@/components/ui/title";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -217,13 +216,13 @@ function Main() {
|
||||
data: configData,
|
||||
isLoading: configIsLoading,
|
||||
mutate: configMutate,
|
||||
} = useSWR<KGConfigRaw>(SWR_KEYS.kgConfig, errorHandlingFetcher);
|
||||
} = useSWR<KGConfigRaw>("/api/admin/kg/config", errorHandlingFetcher);
|
||||
const {
|
||||
data: sourceAndEntityTypesData,
|
||||
isLoading: entityTypesIsLoading,
|
||||
mutate: entityTypesMutate,
|
||||
} = useSWR<SourceAndEntityTypeView>(
|
||||
SWR_KEYS.kgEntityTypes,
|
||||
"/api/admin/kg/entity-types",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { KGConfig, KGConfigRaw } from "./interfaces";
|
||||
|
||||
export type KgExposedStatus = { kgExposed: boolean; isLoading: boolean };
|
||||
@@ -9,7 +8,7 @@ export type KgExposedStatus = { kgExposed: boolean; isLoading: boolean };
|
||||
export function useIsKGExposed(): KgExposedStatus {
|
||||
const { isAdmin } = useUser();
|
||||
const { data: kgExposedRaw, isLoading } = useSWR<boolean>(
|
||||
isAdmin ? SWR_KEYS.kgExposed : null,
|
||||
isAdmin ? "/api/admin/kg/exposed" : null,
|
||||
errorHandlingFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SvgDownload, SvgKey, SvgRefreshCw } from "@opal/icons";
|
||||
import { Interactive, Hoverable } from "@opal/core";
|
||||
import { Interactive } from "@opal/core";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import { Button } from "@opal/components";
|
||||
import { Disabled } from "@opal/core";
|
||||
@@ -83,30 +83,27 @@ export default function ScimModal({
|
||||
onClose={onClose}
|
||||
/>
|
||||
<Modal.Body>
|
||||
<Hoverable.Root group="token">
|
||||
<Interactive.Stateless
|
||||
onClick={() => copyToClipboard(view.rawToken)}
|
||||
>
|
||||
<InputTextArea
|
||||
value={view.rawToken}
|
||||
readOnly
|
||||
autoResize
|
||||
resizable={false}
|
||||
rows={2}
|
||||
className="font-main-ui-mono break-all cursor-pointer [&_textarea]:cursor-pointer"
|
||||
rightSection={
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Hoverable.Item
|
||||
group="token"
|
||||
variant="opacity-on-hover"
|
||||
>
|
||||
<CopyIconButton getCopyText={() => view.rawToken} />
|
||||
</Hoverable.Item>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Interactive.Stateless>
|
||||
</Hoverable.Root>
|
||||
<Interactive.Stateless
|
||||
group="group/token"
|
||||
onClick={() => copyToClipboard(view.rawToken)}
|
||||
>
|
||||
<InputTextArea
|
||||
value={view.rawToken}
|
||||
readOnly
|
||||
autoResize
|
||||
resizable={false}
|
||||
rows={2}
|
||||
className="font-main-ui-mono break-all cursor-pointer [&_textarea]:cursor-pointer"
|
||||
rightSection={
|
||||
<div
|
||||
className="opacity-0 group-hover/token:opacity-100 transition-opacity"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<CopyIconButton getCopyText={() => view.rawToken} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Interactive.Stateless>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<BasicModalFooter
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import { Scope, TokenRateLimit } from "./types";
|
||||
import { GenericTokenRateLimitTable } from "./TokenRateLimitTables";
|
||||
import { mutate } from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import CreateRateLimitModal from "./CreateRateLimitModal";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
@@ -22,9 +21,10 @@ import { Section } from "@/layouts/general-layouts";
|
||||
import { ADMIN_ROUTES } from "@/lib/admin-routes";
|
||||
|
||||
const route = ADMIN_ROUTES.TOKEN_RATE_LIMITS;
|
||||
const GLOBAL_TOKEN_FETCH_URL = SWR_KEYS.globalTokenRateLimits;
|
||||
const USER_TOKEN_FETCH_URL = SWR_KEYS.userTokenRateLimits;
|
||||
const USER_GROUP_FETCH_URL = SWR_KEYS.userGroupTokenRateLimits;
|
||||
const BASE_URL = "/api/admin/token-rate-limits";
|
||||
const GLOBAL_TOKEN_FETCH_URL = `${BASE_URL}/global`;
|
||||
const USER_TOKEN_FETCH_URL = `${BASE_URL}/users`;
|
||||
const USER_GROUP_FETCH_URL = `${BASE_URL}/user-groups`;
|
||||
|
||||
const GLOBAL_DESCRIPTION =
|
||||
"Global rate limits apply to all users, user groups, and API keys. When the global \
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ImageShape } from "@/app/app/services/streamingModels";
|
||||
import { FullImageModal } from "@/app/app/components/files/images/FullImageModal";
|
||||
import { buildImgUrl } from "@/app/app/components/files/images/utils";
|
||||
import { Button } from "@opal/components";
|
||||
import { Hoverable } from "@opal/core";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DEFAULT_SHAPE: ImageShape = "square";
|
||||
@@ -77,42 +76,42 @@ export const InMessageImage = memo(function InMessageImage({
|
||||
onOpenChange={(open) => setFullImageShowing(open)}
|
||||
/>
|
||||
|
||||
<Hoverable.Root group="messageImage" widthVariant="fit">
|
||||
<div className={cn("relative", shapeContainerClasses)}>
|
||||
{!imageLoaded && (
|
||||
<div className="absolute inset-0 bg-background-tint-02 animate-pulse rounded-lg" />
|
||||
<div className={cn("relative group", shapeContainerClasses)}>
|
||||
{!imageLoaded && (
|
||||
<div className="absolute inset-0 bg-background-tint-02 animate-pulse rounded-lg" />
|
||||
)}
|
||||
|
||||
<img
|
||||
width={1200}
|
||||
height={1200}
|
||||
alt="Chat Message Image"
|
||||
onLoad={() => {
|
||||
loadedImages.add(fileId);
|
||||
setImageLoaded(true);
|
||||
}}
|
||||
className={cn(
|
||||
"object-contain object-left overflow-hidden rounded-lg w-full h-full transition-opacity duration-300 cursor-pointer",
|
||||
shapeImageClasses,
|
||||
imageLoaded ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
onClick={() => setFullImageShowing(true)}
|
||||
src={buildImgUrl(fileId)}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
<img
|
||||
width={1200}
|
||||
height={1200}
|
||||
alt="Chat Message Image"
|
||||
onLoad={() => {
|
||||
loadedImages.add(fileId);
|
||||
setImageLoaded(true);
|
||||
}}
|
||||
className={cn(
|
||||
"object-contain object-left overflow-hidden rounded-lg w-full h-full transition-opacity duration-300 cursor-pointer",
|
||||
shapeImageClasses,
|
||||
imageLoaded ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
onClick={() => setFullImageShowing(true)}
|
||||
src={buildImgUrl(fileId)}
|
||||
loading="lazy"
|
||||
{/* Download button - appears on hover */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-2 right-2 opacity-0 group-hover:opacity-100 z-10"
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
icon={SvgDownload}
|
||||
tooltip="Download"
|
||||
onClick={handleDownload}
|
||||
/>
|
||||
|
||||
{/* Download button - appears on hover */}
|
||||
<div className="absolute bottom-2 right-2 z-10">
|
||||
<Hoverable.Item group="messageImage" variant="opacity-on-hover">
|
||||
<Button
|
||||
icon={SvgDownload}
|
||||
tooltip="Download"
|
||||
onClick={handleDownload}
|
||||
/>
|
||||
</Hoverable.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Hoverable.Root>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,6 @@ import IconButton from "@/refresh-components/buttons/IconButton";
|
||||
import ButtonRenaming from "@/refresh-components/buttons/ButtonRenaming";
|
||||
import { UserFileStatus } from "../../projects/projectsService";
|
||||
import { SvgAddLines, SvgEdit, SvgFiles, SvgFolderOpen } from "@opal/icons";
|
||||
import { Hoverable } from "@opal/core";
|
||||
|
||||
export interface ProjectContextPanelProps {
|
||||
projectTokenCount?: number;
|
||||
@@ -134,40 +133,34 @@ export default function ProjectContextPanel({
|
||||
<div className="flex flex-col gap-6 w-full max-w-[var(--app-page-main-content-width)] mx-auto p-4 pt-14 pb-6">
|
||||
<div className="flex flex-col gap-1 text-text-04">
|
||||
<SvgFolderOpen className="h-8 w-8 text-text-04" />
|
||||
<Hoverable.Root group="projectName" widthVariant="fit">
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditingName ? (
|
||||
<ButtonRenaming
|
||||
initialName={projectName}
|
||||
onRename={async (newName) => {
|
||||
if (currentProjectId) {
|
||||
await renameProject(currentProjectId, newName);
|
||||
}
|
||||
}}
|
||||
onClose={cancelEditing}
|
||||
className="font-heading-h2 text-text-04"
|
||||
<div className="group flex items-center gap-2">
|
||||
{isEditingName ? (
|
||||
<ButtonRenaming
|
||||
initialName={projectName}
|
||||
onRename={async (newName) => {
|
||||
if (currentProjectId) {
|
||||
await renameProject(currentProjectId, newName);
|
||||
}
|
||||
}}
|
||||
onClose={cancelEditing}
|
||||
className="font-heading-h2 text-text-04"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Text as="p" headingH2 className="font-heading-h2">
|
||||
{projectName}
|
||||
</Text>
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<IconButton
|
||||
icon={SvgEdit}
|
||||
internal
|
||||
onClick={startEditing}
|
||||
className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity"
|
||||
tooltip="Edit project name"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Text as="p" headingH2 className="font-heading-h2">
|
||||
{projectName}
|
||||
</Text>
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Hoverable.Item
|
||||
group="projectName"
|
||||
variant="opacity-on-hover"
|
||||
>
|
||||
<IconButton
|
||||
icon={SvgEdit}
|
||||
internal
|
||||
onClick={startEditing}
|
||||
tooltip="Edit project name"
|
||||
/>
|
||||
</Hoverable.Item>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Hoverable.Root>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="py-0" />
|
||||
|
||||
@@ -10,7 +10,6 @@ import useScreenSize from "@/hooks/useScreenSize";
|
||||
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
|
||||
import { Button } from "@opal/components";
|
||||
import { SvgEdit } from "@opal/icons";
|
||||
import { Hoverable } from "@opal/core";
|
||||
import FileDisplay from "./FileDisplay";
|
||||
|
||||
interface MessageEditingProps {
|
||||
@@ -171,9 +170,9 @@ const HumanMessage = React.memo(function HumanMessage({
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const copyEditButtonContent = useMemo(
|
||||
const copyEditButton = useMemo(
|
||||
() => (
|
||||
<div className="flex flex-row flex-shrink px-1">
|
||||
<div className="flex flex-row flex-shrink px-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<CopyIconButton
|
||||
getCopyText={() => content}
|
||||
prominence="tertiary"
|
||||
@@ -191,94 +190,86 @@ const HumanMessage = React.memo(function HumanMessage({
|
||||
[content]
|
||||
);
|
||||
|
||||
const copyEditButton = (
|
||||
<Hoverable.Item group="humanMessage" variant="opacity-on-hover">
|
||||
{copyEditButtonContent}
|
||||
</Hoverable.Item>
|
||||
);
|
||||
|
||||
return (
|
||||
<Hoverable.Root group="humanMessage" widthVariant="full">
|
||||
<div
|
||||
id="onyx-human-message"
|
||||
className="flex flex-col justify-end w-full relative"
|
||||
>
|
||||
<FileDisplay files={files || []} />
|
||||
{isEditing ? (
|
||||
<MessageEditing
|
||||
content={content}
|
||||
onSubmitEdit={(editedContent) => {
|
||||
// Don't update UI for edits that can't be persisted
|
||||
if (messageId === undefined || messageId === null) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
onEdit?.(editedContent, messageId);
|
||||
setContent(editedContent);
|
||||
<div
|
||||
id="onyx-human-message"
|
||||
className="group flex flex-col justify-end w-full relative"
|
||||
>
|
||||
<FileDisplay files={files || []} />
|
||||
{isEditing ? (
|
||||
<MessageEditing
|
||||
content={content}
|
||||
onSubmitEdit={(editedContent) => {
|
||||
// Don't update UI for edits that can't be persisted
|
||||
if (messageId === undefined || messageId === null) {
|
||||
setIsEditing(false);
|
||||
}}
|
||||
onCancelEdit={() => setIsEditing(false)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex justify-end">
|
||||
{onEdit && !isMobile && copyEditButton}
|
||||
<div className="md:max-w-[37.5rem]">
|
||||
<div
|
||||
className={
|
||||
"max-w-[30rem] md:max-w-[37.5rem] whitespace-break-spaces break-anywhere rounded-t-16 rounded-bl-16 bg-background-tint-02 py-2 px-3"
|
||||
return;
|
||||
}
|
||||
onEdit?.(editedContent, messageId);
|
||||
setContent(editedContent);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
onCancelEdit={() => setIsEditing(false)}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex justify-end">
|
||||
{onEdit && !isMobile && copyEditButton}
|
||||
<div className="md:max-w-[37.5rem]">
|
||||
<div
|
||||
className={
|
||||
"max-w-[30rem] md:max-w-[37.5rem] whitespace-break-spaces break-anywhere rounded-t-16 rounded-bl-16 bg-background-tint-02 py-2 px-3"
|
||||
}
|
||||
onCopy={(e) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
e.preventDefault();
|
||||
const text = selection
|
||||
.toString()
|
||||
.replace(/\n{2,}/g, "\n")
|
||||
.trim();
|
||||
e.clipboardData.setData("text/plain", text);
|
||||
}
|
||||
onCopy={(e) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
e.preventDefault();
|
||||
const text = selection
|
||||
.toString()
|
||||
.replace(/\n{2,}/g, "\n")
|
||||
.trim();
|
||||
e.clipboardData.setData("text/plain", text);
|
||||
}
|
||||
}}
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
as="p"
|
||||
className="inline-block align-middle"
|
||||
mainContentBody
|
||||
>
|
||||
<Text
|
||||
as="p"
|
||||
className="inline-block align-middle"
|
||||
mainContentBody
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
</div>
|
||||
{content}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end pt-1">
|
||||
{!isEditing && onEdit && isMobile && copyEditButton}
|
||||
{currentMessageInd !== undefined &&
|
||||
onMessageSelection &&
|
||||
otherMessagesCanSwitchTo &&
|
||||
otherMessagesCanSwitchTo.length > 1 && (
|
||||
<MessageSwitcher
|
||||
disableForStreaming={disableSwitchingForStreaming}
|
||||
currentPage={currentMessageInd + 1}
|
||||
totalPages={otherMessagesCanSwitchTo.length}
|
||||
handlePrevious={() => {
|
||||
stopGenerating();
|
||||
const prevMessage = getPreviousMessage();
|
||||
if (prevMessage !== undefined) {
|
||||
onMessageSelection(prevMessage);
|
||||
}
|
||||
}}
|
||||
handleNext={() => {
|
||||
stopGenerating();
|
||||
const nextMessage = getNextMessage();
|
||||
if (nextMessage !== undefined) {
|
||||
onMessageSelection(nextMessage);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end pt-1">
|
||||
{!isEditing && onEdit && isMobile && copyEditButton}
|
||||
{currentMessageInd !== undefined &&
|
||||
onMessageSelection &&
|
||||
otherMessagesCanSwitchTo &&
|
||||
otherMessagesCanSwitchTo.length > 1 && (
|
||||
<MessageSwitcher
|
||||
disableForStreaming={disableSwitchingForStreaming}
|
||||
currentPage={currentMessageInd + 1}
|
||||
totalPages={otherMessagesCanSwitchTo.length}
|
||||
handlePrevious={() => {
|
||||
stopGenerating();
|
||||
const prevMessage = getPreviousMessage();
|
||||
if (prevMessage !== undefined) {
|
||||
onMessageSelection(prevMessage);
|
||||
}
|
||||
}}
|
||||
handleNext={() => {
|
||||
stopGenerating();
|
||||
const nextMessage = getNextMessage();
|
||||
if (nextMessage !== undefined) {
|
||||
onMessageSelection(nextMessage);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Hoverable.Root>
|
||||
</div>
|
||||
);
|
||||
}, arePropsEqual);
|
||||
|
||||
|
||||
@@ -182,7 +182,8 @@ export async function* sendMessage({
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.detail ?? `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
yield* handleSSEStream<PacketType>(response, signal);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { memo, useState, useEffect, useCallback } from "react";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import {
|
||||
useSession,
|
||||
useWebappNeedsRefresh,
|
||||
@@ -204,7 +203,7 @@ const BuildOutputPanel = memo(({ onClose, isOpen }: BuildOutputPanelProps) => {
|
||||
!isWebappReady && pollingDeadline !== null && Date.now() < pollingDeadline;
|
||||
|
||||
const { data: webappInfo, mutate } = useSWR(
|
||||
shouldFetchWebapp ? SWR_KEYS.buildSessionWebappInfo(session.id) : null,
|
||||
shouldFetchWebapp ? `/api/build/sessions/${session.id}/webapp-info` : null,
|
||||
() => (session?.id ? fetchWebappInfo(session.id) : null),
|
||||
{
|
||||
refreshInterval: shouldPoll ? 2000 : 0,
|
||||
@@ -351,7 +350,7 @@ const BuildOutputPanel = memo(({ onClose, isOpen }: BuildOutputPanelProps) => {
|
||||
activeTab === "artifacts";
|
||||
|
||||
const { data: polledArtifacts } = useSWR(
|
||||
shouldFetchArtifacts ? SWR_KEYS.buildSessionArtifacts(session.id) : null,
|
||||
shouldFetchArtifacts ? `/api/build/sessions/${session.id}/artifacts` : null,
|
||||
() => (session?.id ? fetchArtifacts(session.id) : null),
|
||||
{
|
||||
refreshInterval: 5000, // Refresh every 5 seconds to catch new artifacts
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Button } from "@opal/components";
|
||||
import {
|
||||
@@ -41,7 +40,10 @@ export default function ArtifactsTab({
|
||||
const filesNeedsRefresh = useFilesNeedsRefresh();
|
||||
const { data: outputsListing } = useSWR(
|
||||
sessionId
|
||||
? [SWR_KEYS.buildSessionOutputFiles(sessionId), filesNeedsRefresh]
|
||||
? [
|
||||
`/api/build/sessions/${sessionId}/files?path=outputs`,
|
||||
filesNeedsRefresh,
|
||||
]
|
||||
: null,
|
||||
() => (sessionId ? fetchDirectoryListing(sessionId, "outputs") : null),
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { fetchFileContent } from "@/app/craft/services/apiServices";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { SvgFileText } from "@opal/icons";
|
||||
@@ -155,7 +154,7 @@ function FetchedFilePreview({
|
||||
refreshKey,
|
||||
}: FetchedFilePreviewProps) {
|
||||
const { data, error, isLoading, mutate } = useSWR(
|
||||
SWR_KEYS.buildSessionArtifactFile(sessionId, filePath),
|
||||
`/api/build/sessions/${sessionId}/artifacts/${filePath}`,
|
||||
() => fetchFileContent(sessionId, filePath),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import {
|
||||
useBuildSessionStore,
|
||||
useFilesTabState,
|
||||
@@ -87,7 +86,7 @@ export default function FilesTab({
|
||||
error,
|
||||
mutate,
|
||||
} = useSWR(
|
||||
sessionId ? SWR_KEYS.buildSessionFiles(sessionId) : null,
|
||||
sessionId ? `/api/build/sessions/${sessionId}/files?path=` : null,
|
||||
() => (sessionId ? fetchDirectoryListing(sessionId, "") : null),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { SvgChevronLeft, SvgChevronRight, SvgFileText } from "@opal/icons";
|
||||
@@ -30,7 +29,7 @@ export default function PptxPreview({
|
||||
const [imageLoading, setImageLoading] = useState(true);
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR(
|
||||
SWR_KEYS.buildSessionPptxPreview(sessionId, filePath),
|
||||
`/api/build/sessions/${sessionId}/pptx-preview/${filePath}`,
|
||||
() => fetchPptxPreview(sessionId, filePath),
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import {
|
||||
BuildConnectorConfig,
|
||||
ConnectorStatus,
|
||||
@@ -24,7 +23,7 @@ interface BuildConnectorListResponse {
|
||||
*/
|
||||
export function useBuildConnectors() {
|
||||
const { data, isLoading, mutate } = useSWR<BuildConnectorListResponse>(
|
||||
SWR_KEYS.buildConnectors,
|
||||
"/api/build/connectors",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 30000 } // 30 seconds - matches configure page
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useCallback, useRef, useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import {
|
||||
fetchLibraryTree,
|
||||
uploadLibraryFiles,
|
||||
@@ -95,7 +94,7 @@ export default function UserLibraryModal({
|
||||
error,
|
||||
isLoading,
|
||||
mutate,
|
||||
} = useSWR(open ? SWR_KEYS.buildUserLibraryTree : null, fetchLibraryTree, {
|
||||
} = useSWR(open ? "/api/build/user-library/tree" : null, fetchLibraryTree, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import BackButton from "@/refresh-components/buttons/BackButton";
|
||||
import { FeedbackBadge } from "../FeedbackBadge";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
@@ -73,7 +72,7 @@ export default function QueryPage(props: { params: Promise<{ id: string }> }) {
|
||||
isLoading,
|
||||
error,
|
||||
} = useSWR<ChatSessionSnapshot>(
|
||||
SWR_KEYS.adminChatSession(params.id),
|
||||
`/api/admin/chat-session-history/${params.id}`,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import Button from "@/refresh-components/buttons/Button";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import { Disabled } from "@opal/core";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import React, { useState } from "react";
|
||||
import { UsageReport } from "./types";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
@@ -226,7 +225,7 @@ function GenerateReportInput({
|
||||
);
|
||||
}
|
||||
|
||||
const USAGE_REPORT_URL = SWR_KEYS.usageReport;
|
||||
const USAGE_REPORT_URL = "/api/admin/usage-report";
|
||||
|
||||
function UsageReportsTable({
|
||||
refreshTrigger,
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { StandardAnswerCategory, StandardAnswer } from "@/lib/types";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
export const useStandardAnswerCategories = () => {
|
||||
const url = "/api/manage/admin/standard-answer/category";
|
||||
const swrResponse = useSWR<StandardAnswerCategory[]>(
|
||||
SWR_KEYS.standardAnswerCategories,
|
||||
url,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
return {
|
||||
...swrResponse,
|
||||
refreshStandardAnswerCategories: () =>
|
||||
mutate(SWR_KEYS.standardAnswerCategories),
|
||||
refreshStandardAnswerCategories: () => mutate(url),
|
||||
};
|
||||
};
|
||||
|
||||
export const useStandardAnswers = () => {
|
||||
const swrResponse = useSWR<StandardAnswer[]>(
|
||||
SWR_KEYS.standardAnswers,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
const url = "/api/manage/admin/standard-answer";
|
||||
const swrResponse = useSWR<StandardAnswer[]>(url, errorHandlingFetcher);
|
||||
|
||||
return {
|
||||
...swrResponse,
|
||||
refreshStandardAnswers: () => mutate(SWR_KEYS.standardAnswers),
|
||||
refreshStandardAnswers: () => mutate(url),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@ import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { EnterpriseSettings } from "@/interfaces/settings";
|
||||
import { mutate } from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
const route = ADMIN_ROUTES.THEME;
|
||||
|
||||
@@ -55,7 +54,7 @@ export default function ThemePage() {
|
||||
}),
|
||||
});
|
||||
if (response.ok) {
|
||||
await mutate(SWR_KEYS.enterpriseSettings);
|
||||
await mutate("/api/enterprise-settings");
|
||||
return true;
|
||||
} else {
|
||||
const errorMsg = (await response.json()).detail;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useCallback } from "react";
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { UserStatus } from "@/lib/types";
|
||||
import type { UserRole, InvitedUserSnapshot } from "@/lib/types";
|
||||
import type {
|
||||
@@ -76,7 +75,10 @@ export default function useAdminUsers() {
|
||||
isLoading: acceptedLoading,
|
||||
error: acceptedError,
|
||||
mutate: acceptedMutate,
|
||||
} = useSWR<FullUserSnapshot[]>(SWR_KEYS.acceptedUsers, errorHandlingFetcher);
|
||||
} = useSWR<FullUserSnapshot[]>(
|
||||
"/api/manage/users/accepted/all",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const {
|
||||
data: invitedData,
|
||||
@@ -84,7 +86,7 @@ export default function useAdminUsers() {
|
||||
error: invitedError,
|
||||
mutate: invitedMutate,
|
||||
} = useSWR<InvitedUserSnapshot[]>(
|
||||
SWR_KEYS.invitedUsers,
|
||||
"/api/manage/users/invited",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
@@ -94,7 +96,7 @@ export default function useAdminUsers() {
|
||||
error: requestedError,
|
||||
mutate: requestedMutate,
|
||||
} = useSWR<InvitedUserSnapshot[]>(
|
||||
NEXT_PUBLIC_CLOUD_ENABLED ? SWR_KEYS.pendingTenantUsers : null,
|
||||
NEXT_PUBLIC_CLOUD_ENABLED ? "/api/tenants/users/pending" : null,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import useSWR from "swr";
|
||||
import { CCPairBasicInfo } from "@/lib/types";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
/**
|
||||
* Hook for fetching connector-credential pairs (CC Pairs).
|
||||
@@ -69,7 +68,7 @@ import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
*/
|
||||
export default function useCCPairs(enabled: boolean = true) {
|
||||
const { data, error, isLoading, mutate } = useSWR<CCPairBasicInfo[]>(
|
||||
enabled ? SWR_KEYS.connectorStatus : null,
|
||||
enabled ? "/api/manage/connector-status" : null,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
|
||||
@@ -901,6 +901,11 @@ export default function useChatController({
|
||||
});
|
||||
}
|
||||
}
|
||||
// Surface FIFO errors (e.g. 429 before any packets arrive) so the
|
||||
// catch block replaces the thinking placeholder with an error message.
|
||||
if (stack.error) {
|
||||
throw new Error(stack.error);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log("Error:", e);
|
||||
const errorMsg = e.message;
|
||||
|
||||
@@ -5,7 +5,6 @@ import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { UserGroup } from "@/lib/types";
|
||||
import { useContext } from "react";
|
||||
import { SettingsContext } from "@/providers/SettingsProvider";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
/**
|
||||
* Fetches all user groups in the organization.
|
||||
@@ -44,12 +43,13 @@ export default function useGroups() {
|
||||
combinedSettings &&
|
||||
combinedSettings.enterpriseSettings !== null;
|
||||
|
||||
const GROUPS_URL = "/api/manage/admin/user-group";
|
||||
const { data, error, isLoading } = useSWR<UserGroup[]>(
|
||||
isPaidEnterpriseFeaturesEnabled ? SWR_KEYS.adminUserGroups : null,
|
||||
isPaidEnterpriseFeaturesEnabled ? GROUPS_URL : null,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const refreshGroups = () => mutate(SWR_KEYS.adminUserGroups);
|
||||
const refreshGroups = () => mutate(GROUPS_URL);
|
||||
|
||||
if (settingsLoading) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import useSWR from "swr";
|
||||
import { fetchExecutionLogs } from "@/ee/refresh-pages/admin/HooksPage/svc";
|
||||
import type { HookExecutionRecord } from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
import { fetchExecutionLogs } from "@/refresh-pages/admin/HooksPage/svc";
|
||||
import type { HookExecutionRecord } from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
const ONE_HOUR_MS = 60 * 60 * 1000;
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { HookPointMeta } from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { HookPointMeta } from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
export function useHookSpecs() {
|
||||
const { data, isLoading, error } = useSWR<HookPointMeta[]>(
|
||||
SWR_KEYS.hookSpecs,
|
||||
"/api/admin/hooks/specs",
|
||||
errorHandlingFetcher,
|
||||
{ revalidateOnFocus: false }
|
||||
);
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { HookResponse } from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { HookResponse } from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
export function useHooks() {
|
||||
const { data, isLoading, error, mutate } = useSWR<HookResponse[]>(
|
||||
SWR_KEYS.hooks,
|
||||
"/api/admin/hooks",
|
||||
errorHandlingFetcher,
|
||||
{ revalidateOnFocus: false }
|
||||
);
|
||||
@@ -3,7 +3,6 @@
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { MCPServersResponse } from "@/lib/tools/interfaces";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
/**
|
||||
* Fetches MCP (Model Context Protocol) servers configuration.
|
||||
@@ -29,7 +28,7 @@ export default function useMcpServers() {
|
||||
isLoading: isMcpLoading,
|
||||
mutate: mutateMcpServers,
|
||||
} = useSWR<MCPServersResponse>(
|
||||
SWR_KEYS.adminMcpServers,
|
||||
"/api/admin/mcp/servers",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { MCPServersResponse } from "@/lib/tools/interfaces";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
/**
|
||||
* Fetch MCP servers for non-admin UIs (e.g. agent editor).
|
||||
@@ -17,7 +16,7 @@ export default function useMcpServersForAgentEditor() {
|
||||
error,
|
||||
isLoading: isMcpLoading,
|
||||
mutate: mutateMcpServers,
|
||||
} = useSWR<MCPServersResponse>(SWR_KEYS.mcpServers, errorHandlingFetcher);
|
||||
} = useSWR<MCPServersResponse>("/api/mcp/servers", errorHandlingFetcher);
|
||||
|
||||
return {
|
||||
mcpData: mcpData ?? null,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { ToolSnapshot } from "@/lib/tools/interfaces";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
/**
|
||||
* Fetches OpenAPI tools configuration.
|
||||
@@ -28,7 +27,7 @@ export default function useOpenApiTools() {
|
||||
error,
|
||||
isLoading: isOpenApiLoading,
|
||||
mutate: mutateOpenApiTools,
|
||||
} = useSWR<ToolSnapshot[]>(SWR_KEYS.openApiTools, errorHandlingFetcher);
|
||||
} = useSWR<ToolSnapshot[]>("/api/tool/openapi", errorHandlingFetcher);
|
||||
|
||||
return {
|
||||
openApiTools: openApiTools ?? null,
|
||||
|
||||
@@ -2,11 +2,12 @@ import useSWR from "swr";
|
||||
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import type { ScimTokenResponse } from "@/app/admin/scim/interfaces";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
const TOKEN_URL = "/api/admin/enterprise-settings/scim/token";
|
||||
|
||||
export function useScimToken() {
|
||||
const { data, error, isLoading, mutate } = useSWR<ScimTokenResponse>(
|
||||
SWR_KEYS.scimToken,
|
||||
TOKEN_URL,
|
||||
errorHandlingFetcher,
|
||||
{ shouldRetryOnError: false }
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import useSWR, { mutate } from "swr";
|
||||
import { useContext } from "react";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { SettingsContext } from "@/providers/SettingsProvider";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
export interface MinimalUserGroupSnapshot {
|
||||
id: number;
|
||||
@@ -22,12 +21,13 @@ export default function useShareableGroups() {
|
||||
combinedSettings &&
|
||||
combinedSettings.enterpriseSettings !== null;
|
||||
|
||||
const SHAREABLE_GROUPS_URL = "/api/manage/user-groups/minimal";
|
||||
const { data, error, isLoading } = useSWR<MinimalUserGroupSnapshot[]>(
|
||||
isPaidEnterpriseFeaturesEnabled ? SWR_KEYS.shareableGroups : null,
|
||||
isPaidEnterpriseFeaturesEnabled ? SHAREABLE_GROUPS_URL : null,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const refreshShareableGroups = () => mutate(SWR_KEYS.shareableGroups);
|
||||
const refreshShareableGroups = () => mutate(SHAREABLE_GROUPS_URL);
|
||||
|
||||
if (settingsLoading) {
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,6 @@ import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import type { InvitedUserSnapshot } from "@/lib/types";
|
||||
import { NEXT_PUBLIC_CLOUD_ENABLED } from "@/lib/constants";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import type { StatusCountMap } from "@/refresh-pages/admin/UsersPage/interfaces";
|
||||
|
||||
type UserCountsResponse = {
|
||||
@@ -23,15 +22,18 @@ type UserCounts = {
|
||||
|
||||
export default function useUserCounts(): UserCounts {
|
||||
const { data: countsData, mutate: refreshCounts } =
|
||||
useSWR<UserCountsResponse>(SWR_KEYS.userCounts, errorHandlingFetcher);
|
||||
useSWR<UserCountsResponse>(
|
||||
"/api/manage/users/counts",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const { data: invitedUsers } = useSWR<InvitedUserSnapshot[]>(
|
||||
SWR_KEYS.invitedUsers,
|
||||
"/api/manage/users/invited",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const { data: pendingUsers } = useSWR<InvitedUserSnapshot[]>(
|
||||
NEXT_PUBLIC_CLOUD_ENABLED ? SWR_KEYS.pendingTenantUsers : null,
|
||||
NEXT_PUBLIC_CLOUD_ENABLED ? "/api/tenants/users/pending" : null,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Credential } from "@/lib/connectors/credentials";
|
||||
import { ConnectorSnapshot } from "@/lib/connectors/connectors";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { buildSimilarCredentialInfoURL } from "@/app/admin/connector/[ccPairId]/lib";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
// Constants for service names to avoid typos
|
||||
export const GOOGLE_SERVICES = {
|
||||
@@ -116,6 +115,6 @@ export const refreshAllGoogleData = (
|
||||
source === ValidSources.Gmail
|
||||
? GOOGLE_SERVICES.GMAIL
|
||||
: GOOGLE_SERVICES.GOOGLE_DRIVE;
|
||||
mutate(SWR_KEYS.googleConnectorAppCredential(service));
|
||||
mutate(SWR_KEYS.googleConnectorServiceAccountKey(service));
|
||||
mutate(`/api/manage/admin/connector/${service}/app-credential`);
|
||||
mutate(`/api/manage/admin/connector/${service}/service-account-key`);
|
||||
};
|
||||
|
||||
@@ -39,18 +39,19 @@ import { useUser } from "@/providers/UserProvider";
|
||||
import { SEARCH_TOOL_ID } from "@/app/app/components/tools/constants";
|
||||
import { updateTemperatureOverrideForChatSession } from "@/app/app/services/lib";
|
||||
import { useLLMProviders } from "@/hooks/useLLMProviders";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
const CREDENTIAL_URL = "/api/manage/admin/credential";
|
||||
|
||||
export const usePublicCredentials = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const swrResponse = useSWR<Credential<any>[]>(
|
||||
SWR_KEYS.adminCredentials,
|
||||
CREDENTIAL_URL,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
return {
|
||||
...swrResponse,
|
||||
refreshCredentials: () => mutate(SWR_KEYS.adminCredentials),
|
||||
refreshCredentials: () => mutate(CREDENTIAL_URL),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -86,6 +87,9 @@ export const useObjectState = <T>(
|
||||
return [state, set];
|
||||
};
|
||||
|
||||
const INDEXING_STATUS_URL = "/api/manage/admin/connector/indexing-status";
|
||||
const CONNECTOR_STATUS_URL = "/api/manage/admin/connector/status";
|
||||
|
||||
export const useConnectorIndexingStatusWithPagination = (
|
||||
filters: Omit<IndexingStatusRequest, "source" | "source_to_page"> = {},
|
||||
refreshInterval = 30000,
|
||||
@@ -122,7 +126,7 @@ export const useConnectorIndexingStatusWithPagination = (
|
||||
);
|
||||
|
||||
const swrKey = enabled
|
||||
? [SWR_KEYS.indexingStatus, JSON.stringify(mainRequest)]
|
||||
? [INDEXING_STATUS_URL, JSON.stringify(mainRequest)]
|
||||
: null;
|
||||
|
||||
// Main data fetch with auto-refresh
|
||||
@@ -211,7 +215,7 @@ export const useConnectorStatus = (
|
||||
enabled: boolean = true
|
||||
) => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const url = SWR_KEYS.adminConnectorStatus;
|
||||
const url = CONNECTOR_STATUS_URL;
|
||||
const swrResponse = useSWR<ConnectorStatus<any, any>[]>(
|
||||
enabled ? url : null,
|
||||
errorHandlingFetcher,
|
||||
@@ -225,7 +229,7 @@ export const useConnectorStatus = (
|
||||
};
|
||||
|
||||
export const useBasicConnectorStatus = (enabled: boolean = true) => {
|
||||
const url = SWR_KEYS.connectorStatus;
|
||||
const url = "/api/manage/connector-status";
|
||||
const swrResponse = useSWR<CCPairBasicInfo[]>(
|
||||
enabled ? url : null,
|
||||
errorHandlingFetcher
|
||||
@@ -238,7 +242,7 @@ export const useBasicConnectorStatus = (enabled: boolean = true) => {
|
||||
|
||||
export const useFederatedConnectors = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const url = SWR_KEYS.federatedConnectors;
|
||||
const url = "/api/federated";
|
||||
const swrResponse = useSWR<FederatedConnectorDetail[]>(
|
||||
url,
|
||||
errorHandlingFetcher
|
||||
@@ -253,16 +257,16 @@ export const useFederatedConnectors = () => {
|
||||
export const useLabels = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data: labels, error } = useSWR<PersonaLabel[]>(
|
||||
SWR_KEYS.personaLabels,
|
||||
"/api/persona/labels",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const refreshLabels = async () => {
|
||||
return mutate(SWR_KEYS.personaLabels);
|
||||
return mutate("/api/persona/labels");
|
||||
};
|
||||
|
||||
const createLabel = async (name: string): Promise<PersonaLabel | null> => {
|
||||
const response = await fetch(SWR_KEYS.personaLabels, {
|
||||
const response = await fetch("/api/persona/labels", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
@@ -274,7 +278,7 @@ export const useLabels = () => {
|
||||
|
||||
const newLabel: PersonaLabel = await response.json();
|
||||
mutate(
|
||||
SWR_KEYS.personaLabels,
|
||||
"/api/persona/labels",
|
||||
(currentLabels: PersonaLabel[] | undefined) => [
|
||||
...(currentLabels || []),
|
||||
newLabel,
|
||||
@@ -293,7 +297,7 @@ export const useLabels = () => {
|
||||
|
||||
if (response.ok) {
|
||||
mutate(
|
||||
SWR_KEYS.personaLabels,
|
||||
"/api/persona/labels",
|
||||
labels?.map((label) => (label.id === id ? { ...label, name } : label)),
|
||||
false
|
||||
);
|
||||
@@ -310,7 +314,7 @@ export const useLabels = () => {
|
||||
|
||||
if (response.ok) {
|
||||
mutate(
|
||||
SWR_KEYS.personaLabels,
|
||||
"/api/persona/labels",
|
||||
labels?.filter((label) => label.id !== id),
|
||||
false
|
||||
);
|
||||
@@ -872,7 +876,7 @@ export function useLlmManager(
|
||||
|
||||
export function useAuthType(): AuthType | null {
|
||||
const { data, error } = useSWR<{ auth_type: AuthType }>(
|
||||
SWR_KEYS.authType,
|
||||
"/api/auth/type",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
@@ -891,6 +895,8 @@ export function useAuthType(): AuthType | null {
|
||||
EE Only APIs
|
||||
*/
|
||||
|
||||
const USER_GROUP_URL = "/api/manage/admin/user-group";
|
||||
|
||||
export const useUserGroups = (): {
|
||||
data: UserGroup[] | undefined;
|
||||
isLoading: boolean;
|
||||
@@ -905,11 +911,11 @@ export const useUserGroups = (): {
|
||||
combinedSettings.enterpriseSettings !== null;
|
||||
|
||||
const swrResponse = useSWR<UserGroup[]>(
|
||||
isPaidEnterpriseFeaturesEnabled ? SWR_KEYS.adminUserGroups : null,
|
||||
isPaidEnterpriseFeaturesEnabled ? USER_GROUP_URL : null,
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const refreshUserGroups = () => mutate(SWR_KEYS.adminUserGroups);
|
||||
const refreshUserGroups = () => mutate(USER_GROUP_URL);
|
||||
|
||||
if (isLoading) {
|
||||
return {
|
||||
@@ -939,7 +945,7 @@ export const fetchConnectorIndexingStatus = async (
|
||||
request: IndexingStatusRequest = {},
|
||||
sourcePages: Record<ValidSources, number> | null = null
|
||||
): Promise<ConnectorIndexingStatusLiteResponse[]> => {
|
||||
const response = await fetch(SWR_KEYS.indexingStatus, {
|
||||
const response = await fetch(INDEXING_STATUS_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -3,7 +3,6 @@ import useSWR from "swr";
|
||||
import { errorHandlingFetcher, skipRetryOnAuthError } from "@/lib/fetcher";
|
||||
import { initiateOAuthFlow } from "@/lib/oauth/api";
|
||||
import { OAuthTokenStatus, ToolSnapshot } from "@/lib/tools/interfaces";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
export interface ToolAuthStatus {
|
||||
// whether or not the user has EVER auth'd
|
||||
@@ -19,7 +18,7 @@ export function useToolOAuthStatus(agentId?: number) {
|
||||
error: swrError,
|
||||
mutate,
|
||||
} = useSWR<OAuthTokenStatus[]>(
|
||||
SWR_KEYS.oauthTokenStatus,
|
||||
"/api/user-oauth-token/status",
|
||||
errorHandlingFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ScopedMutator } from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import {
|
||||
LLM_CHAT_PROVIDERS_URL,
|
||||
LLM_PROVIDERS_ADMIN_URL,
|
||||
} from "@/lib/llmConfig/constants";
|
||||
|
||||
const PERSONA_PROVIDER_ENDPOINT_PATTERN =
|
||||
/^\/api\/llm\/persona\/\d+\/providers$/;
|
||||
@@ -8,8 +11,8 @@ export async function refreshLlmProviderCaches(
|
||||
mutate: ScopedMutator
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
mutate(SWR_KEYS.adminLlmProviders),
|
||||
mutate(SWR_KEYS.llmProviders),
|
||||
mutate(LLM_PROVIDERS_ADMIN_URL),
|
||||
mutate(LLM_CHAT_PROVIDERS_URL),
|
||||
mutate(
|
||||
(key) =>
|
||||
typeof key === "string" && PERSONA_PROVIDER_ENDPOINT_PATTERN.test(key)
|
||||
|
||||
@@ -10,9 +10,6 @@ export const SWR_KEYS = {
|
||||
// ── User ──────────────────────────────────────────────────────────────────
|
||||
me: "/api/me",
|
||||
|
||||
// ── Health ────────────────────────────────────────────────────────────────
|
||||
health: "/api/health",
|
||||
|
||||
// ── Settings ──────────────────────────────────────────────────────────────
|
||||
settings: "/api/settings",
|
||||
enterpriseSettings: "/api/enterprise-settings",
|
||||
@@ -24,125 +21,37 @@ export const SWR_KEYS = {
|
||||
persona: (id: number) => `/api/persona/${id}`,
|
||||
agentPreferences: "/api/user/assistant/preferences",
|
||||
defaultAssistantConfig: "/api/admin/default-assistant/configuration",
|
||||
personaLabels: "/api/persona/labels",
|
||||
|
||||
// ── LLM Providers ─────────────────────────────────────────────────────────
|
||||
llmProviders: "/api/llm/provider",
|
||||
llmProvidersForPersona: (personaId: number) =>
|
||||
`/api/llm/persona/${personaId}/providers`,
|
||||
adminLlmProviders: "/api/admin/llm/provider",
|
||||
llmProvidersWithImageGen: "/api/admin/llm/provider?include_image_gen=true",
|
||||
wellKnownLlmProviders: "/api/admin/llm/built-in/options",
|
||||
wellKnownLlmProvider: (providerEndpoint: string) =>
|
||||
`/api/admin/llm/built-in/options/${providerEndpoint}`,
|
||||
|
||||
// ── Image Generation ──────────────────────────────────────────────────────
|
||||
imageGenConfig: "/api/admin/image-generation/config",
|
||||
|
||||
// ── Documents ─────────────────────────────────────────────────────────────
|
||||
documentSets: "/api/manage/document-set",
|
||||
documentSetsEditable: "/api/manage/document-set?get_editable=true",
|
||||
tags: "/api/query/valid-tags",
|
||||
connectorStatus: "/api/manage/connector-status",
|
||||
|
||||
// ── Credentials & Connectors ──────────────────────────────────────────────
|
||||
adminCredentials: "/api/manage/admin/credential",
|
||||
indexingStatus: "/api/manage/admin/connector/indexing-status",
|
||||
adminConnectorStatus: "/api/manage/admin/connector/status",
|
||||
federatedConnectors: "/api/federated",
|
||||
|
||||
// ── Google Connectors ─────────────────────────────────────────────────────
|
||||
googleConnectorAppCredential: (service: "gmail" | "google-drive") =>
|
||||
`/api/manage/admin/connector/${service}/app-credential`,
|
||||
googleConnectorServiceAccountKey: (service: "gmail" | "google-drive") =>
|
||||
`/api/manage/admin/connector/${service}/service-account-key`,
|
||||
googleConnectorCredentials: (service: "gmail" | "google-drive") =>
|
||||
`/api/manage/admin/connector/${service}/credentials`,
|
||||
googleConnectorPublicCredential: (service: "gmail" | "google-drive") =>
|
||||
`/api/manage/admin/connector/${service}/public-credential`,
|
||||
googleConnectorServiceAccountCredential: (
|
||||
service: "gmail" | "google-drive"
|
||||
) => `/api/manage/admin/connector/${service}/service-account-credential`,
|
||||
|
||||
// ── Search Settings ───────────────────────────────────────────────────────
|
||||
currentSearchSettings: "/api/search-settings/get-current-search-settings",
|
||||
secondarySearchSettings: "/api/search-settings/get-secondary-search-settings",
|
||||
|
||||
// ── Chat Sessions ─────────────────────────────────────────────────────────
|
||||
chatSessions: "/api/chat/get-user-chat-sessions",
|
||||
|
||||
// ── Projects & Files ──────────────────────────────────────────────────────
|
||||
userProjects: "/api/user/projects",
|
||||
recentFiles: "/api/user/files/recent",
|
||||
userPats: "/api/user/pats",
|
||||
notifications: "/api/notifications",
|
||||
|
||||
// ── Users ─────────────────────────────────────────────────────────────────
|
||||
acceptedUsers: "/api/manage/users/accepted/all",
|
||||
invitedUsers: "/api/manage/users/invited",
|
||||
pendingTenantUsers: "/api/tenants/users/pending",
|
||||
userCounts: "/api/manage/users/counts",
|
||||
|
||||
// ── API Keys ──────────────────────────────────────────────────────────────
|
||||
adminApiKeys: "/api/admin/api-key",
|
||||
|
||||
// ── Groups ────────────────────────────────────────────────────────────────
|
||||
adminUserGroups: "/api/manage/admin/user-group",
|
||||
shareableGroups: "/api/manage/user-groups/minimal",
|
||||
scimToken: "/api/admin/enterprise-settings/scim/token",
|
||||
|
||||
// ── MCP Servers ───────────────────────────────────────────────────────────
|
||||
adminMcpServers: "/api/admin/mcp/servers",
|
||||
mcpServers: "/api/mcp/servers",
|
||||
|
||||
// ── Tools ─────────────────────────────────────────────────────────────────
|
||||
tools: "/api/tool",
|
||||
openApiTools: "/api/tool/openapi",
|
||||
oauthTokenStatus: "/api/user-oauth-token/status",
|
||||
|
||||
// ── Voice ─────────────────────────────────────────────────────────────────
|
||||
voiceProviders: "/api/admin/voice/providers",
|
||||
voiceStatus: "/api/voice/status",
|
||||
|
||||
// ── Build (Craft) ─────────────────────────────────────────────────────────
|
||||
buildConnectors: "/api/build/connectors",
|
||||
buildUserLibraryTree: "/api/build/user-library/tree",
|
||||
buildSessionFiles: (sessionId: string) =>
|
||||
`/api/build/sessions/${sessionId}/files?path=`,
|
||||
buildSessionOutputFiles: (sessionId: string) =>
|
||||
`/api/build/sessions/${sessionId}/files?path=outputs`,
|
||||
buildSessionWebappInfo: (sessionId: string) =>
|
||||
`/api/build/sessions/${sessionId}/webapp-info`,
|
||||
buildSessionArtifacts: (sessionId: string) =>
|
||||
`/api/build/sessions/${sessionId}/artifacts`,
|
||||
buildSessionArtifactFile: (sessionId: string, filePath: string) =>
|
||||
`/api/build/sessions/${sessionId}/artifacts/${filePath}`,
|
||||
buildSessionPptxPreview: (sessionId: string, filePath: string) =>
|
||||
`/api/build/sessions/${sessionId}/pptx-preview/${filePath}`,
|
||||
|
||||
// ── Knowledge Graph ───────────────────────────────────────────────────────
|
||||
kgConfig: "/api/admin/kg/config",
|
||||
kgEntityTypes: "/api/admin/kg/entity-types",
|
||||
kgExposed: "/api/admin/kg/exposed",
|
||||
|
||||
// ── OpenSearch Migration ──────────────────────────────────────────────────
|
||||
opensearchMigrationStatus: "/api/admin/opensearch-migration/status",
|
||||
opensearchMigrationRetrieval: "/api/admin/opensearch-migration/retrieval",
|
||||
|
||||
// ── Token Rate Limits ─────────────────────────────────────────────────────
|
||||
globalTokenRateLimits: "/api/admin/token-rate-limits/global",
|
||||
userTokenRateLimits: "/api/admin/token-rate-limits/users",
|
||||
userGroupTokenRateLimits: "/api/admin/token-rate-limits/user-groups",
|
||||
userGroupTokenRateLimit: (groupId: number) =>
|
||||
`/api/admin/token-rate-limits/user-group/${groupId}`,
|
||||
|
||||
// ── Usage Reports ─────────────────────────────────────────────────────────
|
||||
usageReport: "/api/admin/usage-report",
|
||||
|
||||
// ── Web Search ────────────────────────────────────────────────────────────
|
||||
webSearchContentProviders: "/api/admin/web-search/content-providers",
|
||||
webSearchSearchProviders: "/api/admin/web-search/search-providers",
|
||||
|
||||
// ── Prompt shortcuts ──────────────────────────────────────────────────────
|
||||
promptShortcuts: "/api/input_prompt",
|
||||
|
||||
@@ -154,28 +63,4 @@ export const SWR_KEYS = {
|
||||
// ── Admin ─────────────────────────────────────────────────────────────────
|
||||
hooks: "/api/admin/hooks",
|
||||
hookSpecs: "/api/admin/hooks/specs",
|
||||
|
||||
// ── Slack Bots ────────────────────────────────────────────────────────────
|
||||
slackChannels: "/api/manage/admin/slack-app/channel",
|
||||
slackBots: "/api/manage/admin/slack-app/bots",
|
||||
slackBot: (botId: number) => `/api/manage/admin/slack-app/bots/${botId}`,
|
||||
slackBotConfig: (botId: number) =>
|
||||
`/api/manage/admin/slack-app/bots/${botId}/config`,
|
||||
|
||||
// ── Standard Answers (EE) ─────────────────────────────────────────────────
|
||||
standardAnswerCategories: "/api/manage/admin/standard-answer/category",
|
||||
standardAnswers: "/api/manage/admin/standard-answer",
|
||||
|
||||
// ── Query History (EE) ────────────────────────────────────────────────────
|
||||
adminChatSessionHistory: "/api/admin/chat-session-history",
|
||||
adminChatSession: (id: string) => `/api/admin/chat-session-history/${id}`,
|
||||
|
||||
// ── MCP Server (per-ID) ───────────────────────────────────────────────────
|
||||
adminMcpServer: (id: number) => `/api/admin/mcp/servers/${id}`,
|
||||
|
||||
// ── Document Processing ───────────────────────────────────────────────────
|
||||
unstructuredApiKeySet: "/api/search-settings/unstructured-api-key-set",
|
||||
|
||||
// ── Connectors ────────────────────────────────────────────────────────────
|
||||
connector: "/api/manage/connector",
|
||||
} as const;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { cn, noProp } from "@/lib/utils";
|
||||
import { SvgPlus, SvgX } from "@opal/icons";
|
||||
import { Hoverable } from "@opal/core";
|
||||
import IconButton from "@/refresh-components/buttons/IconButton";
|
||||
import SimpleTooltip from "@/refresh-components/SimpleTooltip";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
@@ -173,106 +173,103 @@ export default function InputImage({
|
||||
const dropzoneProps = onDrop ? getRootProps() : {};
|
||||
|
||||
return (
|
||||
<Hoverable.Root group="inputImage" widthVariant="fit">
|
||||
<div
|
||||
className={cn("relative", className)}
|
||||
style={{ width: size, height: size }}
|
||||
{...dropzoneProps}
|
||||
<div
|
||||
className={cn("relative group", className)}
|
||||
style={{ width: size, height: size }}
|
||||
{...dropzoneProps}
|
||||
>
|
||||
{/* Hidden input for file selection */}
|
||||
{onDrop && <input {...getInputProps()} />}
|
||||
|
||||
{/* Main container */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"relative w-full h-full rounded-full overflow-hidden",
|
||||
"border flex items-center justify-center",
|
||||
"transition-all duration-150",
|
||||
containerClass
|
||||
)}
|
||||
aria-label={
|
||||
isInteractive ? (hasImage ? "Edit image" : "Upload image") : undefined
|
||||
}
|
||||
>
|
||||
{/* Hidden input for file selection */}
|
||||
{onDrop && <input {...getInputProps()} />}
|
||||
{/* Content */}
|
||||
{hasImage ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
||||
/>
|
||||
) : (
|
||||
<SvgPlus
|
||||
className={cn("w-6 h-6", placeholderClass, "pointer-events-none")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Main container */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"group relative w-full h-full rounded-full overflow-hidden",
|
||||
"border flex items-center justify-center",
|
||||
"transition-all duration-150",
|
||||
containerClass
|
||||
)}
|
||||
aria-label={
|
||||
isInteractive
|
||||
? hasImage
|
||||
? "Edit image"
|
||||
: "Upload image"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{/* Content */}
|
||||
{hasImage ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="absolute inset-0 w-full h-full object-cover pointer-events-none"
|
||||
/>
|
||||
) : (
|
||||
<SvgPlus
|
||||
className={cn("w-6 h-6", placeholderClass, "pointer-events-none")}
|
||||
/>
|
||||
)}
|
||||
{/* Drag overlay indicator */}
|
||||
{isDragActive && (
|
||||
<div className="absolute inset-0 bg-action-link-05/10 flex items-center justify-center rounded-full pointer-events-none">
|
||||
<SvgPlus className="w-8 h-8 stroke-action-link-05" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Drag overlay indicator */}
|
||||
{isDragActive && (
|
||||
<div className="absolute inset-0 bg-action-link-05/10 flex items-center justify-center rounded-full pointer-events-none">
|
||||
<SvgPlus className="w-8 h-8 stroke-action-link-05" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit overlay - shows on hover/focus when image is uploaded */}
|
||||
{showEditOverlay && isInteractive && hasImage && !isDragActive && (
|
||||
<div className="absolute bottom-0 left-0 right-0 pointer-events-none">
|
||||
<Hoverable.Item group="inputImage" variant="opacity-on-hover">
|
||||
{/* Edit overlay - shows on hover/focus when image is uploaded */}
|
||||
{showEditOverlay && isInteractive && hasImage && !isDragActive && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute bottom-0 left-0 right-0",
|
||||
"flex items-center justify-center",
|
||||
"pb-2.5 pt-1.5",
|
||||
"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100",
|
||||
"transition-opacity duration-150",
|
||||
"backdrop-blur-sm bg-mask-01",
|
||||
"pointer-events-none"
|
||||
)}
|
||||
>
|
||||
<div className="pointer-events-auto">
|
||||
<SimpleTooltip tooltip="Edit" side="top">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
"pb-2.5 pt-1.5",
|
||||
"backdrop-blur-sm bg-mask-01",
|
||||
"pointer-events-none"
|
||||
"px-1 py-0.5 rounded-08"
|
||||
)}
|
||||
>
|
||||
<div className="pointer-events-auto">
|
||||
<SimpleTooltip tooltip="Edit" side="top">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
"px-1 py-0.5 rounded-08"
|
||||
)}
|
||||
>
|
||||
<Text
|
||||
className="text-text-03 font-secondary-action"
|
||||
style={{ fontSize: "12px", lineHeight: "16px" }}
|
||||
>
|
||||
Edit
|
||||
</Text>
|
||||
</div>
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
<Text
|
||||
className="text-text-03 font-secondary-action"
|
||||
style={{ fontSize: "12px", lineHeight: "16px" }}
|
||||
>
|
||||
Edit
|
||||
</Text>
|
||||
</div>
|
||||
</Hoverable.Item>
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Remove button - top left corner (only when image is uploaded) */}
|
||||
{isInteractive && hasImage && onRemove && (
|
||||
<div className="absolute top-1 left-1">
|
||||
<Hoverable.Item group="inputImage" variant="opacity-on-hover">
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<IconButton
|
||||
icon={SvgX}
|
||||
onClick={noProp(onRemove)}
|
||||
type="button"
|
||||
primary
|
||||
className="!w-5 !h-5 !p-0.5 !rounded-04"
|
||||
aria-label="Remove image"
|
||||
/>
|
||||
</Hoverable.Item>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Hoverable.Root>
|
||||
</button>
|
||||
|
||||
{/* Remove button - top left corner (only when image is uploaded) */}
|
||||
{isInteractive && hasImage && onRemove && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-1 left-1",
|
||||
"opacity-0 group-hover:opacity-100 group-focus-within:opacity-100",
|
||||
"transition-opacity duration-150"
|
||||
)}
|
||||
>
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<IconButton
|
||||
icon={SvgX}
|
||||
onClick={noProp(onRemove)}
|
||||
type="button"
|
||||
primary
|
||||
className="!w-5 !h-5 !p-0.5 !rounded-04"
|
||||
aria-label="Remove image"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { FunctionComponent } from "react";
|
||||
import { cn, noProp } from "@/lib/utils";
|
||||
import { SvgMaximize2, SvgTextLines, SvgX } from "@opal/icons";
|
||||
import type { IconProps } from "@opal/types";
|
||||
import { Hoverable } from "@opal/core";
|
||||
import IconButton from "../buttons/IconButton";
|
||||
import Text from "../texts/Text";
|
||||
import Truncated from "../texts/Truncated";
|
||||
@@ -33,32 +32,25 @@ interface RemoveButtonProps {
|
||||
|
||||
function RemoveButton({ onRemove }: RemoveButtonProps) {
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
title="Remove"
|
||||
aria-label="Remove"
|
||||
className={cn(
|
||||
"absolute -left-1 -top-1 z-10",
|
||||
"pointer-events-none focus-within:pointer-events-auto"
|
||||
"absolute -left-1 -top-1 z-10 h-4 w-4",
|
||||
"flex items-center justify-center",
|
||||
"rounded-full bg-theme-primary-05 text-text-inverted-05",
|
||||
"opacity-0 group-hover/Tile:opacity-100 focus:opacity-100",
|
||||
"pointer-events-none group-hover/Tile:pointer-events-auto focus:pointer-events-auto",
|
||||
"transition-opacity duration-150"
|
||||
)}
|
||||
>
|
||||
<Hoverable.Item group="fileTile" variant="opacity-on-hover">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
title="Remove"
|
||||
aria-label="Remove"
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
"flex items-center justify-center",
|
||||
"rounded-full bg-theme-primary-05 text-text-inverted-05",
|
||||
"pointer-events-auto"
|
||||
)}
|
||||
>
|
||||
<SvgX size={10} />
|
||||
</button>
|
||||
</Hoverable.Item>
|
||||
</div>
|
||||
<SvgX size={10} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,7 +70,7 @@ export default function FileTile({
|
||||
const isMuted = state === "processing" || state === "disabled";
|
||||
|
||||
return (
|
||||
<Hoverable.Root group="fileTile" widthVariant="fit">
|
||||
<div className="group/Tile">
|
||||
<div
|
||||
onClick={onOpen && state !== "disabled" ? () => onOpen() : undefined}
|
||||
className={cn(
|
||||
@@ -91,8 +83,8 @@ export default function FileTile({
|
||||
? "bg-background-neutral-02 border-border-01"
|
||||
: "bg-background-tint-00 border-border-01",
|
||||
// Hover overrides (disabled gets none)
|
||||
state !== "disabled" && "hover:border-border-02",
|
||||
state === "default" && "hover:bg-background-tint-02",
|
||||
state !== "disabled" && "group-hover/Tile:border-border-02",
|
||||
state === "default" && "group-hover/Tile:bg-background-tint-02",
|
||||
// Clickable cursor when onOpen is provided and not disabled
|
||||
onOpen && state !== "disabled" && "cursor-pointer"
|
||||
)}
|
||||
@@ -122,7 +114,7 @@ export default function FileTile({
|
||||
text02
|
||||
className={cn(
|
||||
"truncate",
|
||||
state === "processing" && "hover:text-text-03"
|
||||
state === "processing" && "group-hover/Tile:text-text-03"
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
@@ -134,7 +126,7 @@ export default function FileTile({
|
||||
text02
|
||||
className={cn(
|
||||
"line-clamp-2",
|
||||
state === "processing" && "hover:text-text-03"
|
||||
state === "processing" && "group-hover/Tile:text-text-03"
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
@@ -167,6 +159,6 @@ export default function FileTile({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Hoverable.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import * as GeneralLayouts from "@/layouts/general-layouts";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import { Disabled, Hoverable } from "@opal/core";
|
||||
import { Disabled } from "@opal/core";
|
||||
import { FullPersona } from "@/app/admin/agents/interfaces";
|
||||
import { buildImgUrl } from "@/app/app/components/files/images/utils";
|
||||
import { Formik, Form, FieldArray } from "formik";
|
||||
@@ -212,25 +212,22 @@ function AgentIconEditor({ existingAgent }: AgentIconEditorProps) {
|
||||
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<Hoverable.Root group="inputAvatar" widthVariant="fit">
|
||||
<InputAvatar className="relative flex flex-col items-center justify-center h-[7.5rem] w-[7.5rem]">
|
||||
{/* We take the `InputAvatar`'s height/width (in REM) and multiply it by 16 (the REM -> px conversion factor). */}
|
||||
<CustomAgentAvatar
|
||||
size={imageSrc ? 7.5 * 16 : 40}
|
||||
src={imageSrc}
|
||||
iconName={values.icon_name ?? undefined}
|
||||
name={values.name}
|
||||
/>
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 mb-2">
|
||||
<Hoverable.Item group="inputAvatar" variant="opacity-on-hover">
|
||||
<Button className="h-[1.75rem]" secondary>
|
||||
Edit
|
||||
</Button>
|
||||
</Hoverable.Item>
|
||||
</div>
|
||||
</InputAvatar>
|
||||
</Hoverable.Root>
|
||||
<InputAvatar className="group/InputAvatar relative flex flex-col items-center justify-center h-[7.5rem] w-[7.5rem]">
|
||||
{/* We take the `InputAvatar`'s height/width (in REM) and multiply it by 16 (the REM -> px conversion factor). */}
|
||||
<CustomAgentAvatar
|
||||
size={imageSrc ? 7.5 * 16 : 40}
|
||||
src={imageSrc}
|
||||
iconName={values.icon_name ?? undefined}
|
||||
name={values.name}
|
||||
/>
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button
|
||||
className="absolute bottom-0 left-1/2 -translate-x-1/2 h-[1.75rem] mb-2 invisible group-hover/InputAvatar:visible"
|
||||
secondary
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</InputAvatar>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content>
|
||||
<PopoverMenu>
|
||||
|
||||
@@ -32,7 +32,6 @@ import { deleteAllChatSessions } from "@/app/app/services/lib";
|
||||
import { useAuthType, useLlmManager } from "@/lib/hooks";
|
||||
import useChatSessions from "@/hooks/useChatSessions";
|
||||
import useSWR from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import useFilter from "@/hooks/useFilter";
|
||||
import CreateButton from "@/refresh-components/buttons/CreateButton";
|
||||
@@ -1081,7 +1080,7 @@ function AccountsAccessSettings() {
|
||||
error,
|
||||
isLoading,
|
||||
} = useSWR<PAT[]>(
|
||||
showTokensSection ? SWR_KEYS.userPats : null,
|
||||
showTokensSection ? "/api/user/pats" : null,
|
||||
errorHandlingFetcher,
|
||||
{
|
||||
revalidateOnFocus: true,
|
||||
|
||||
@@ -19,9 +19,9 @@ export default function AgentsPage() {
|
||||
description="Customize AI behavior and knowledge with agents. Manage agents in your organization."
|
||||
icon={SvgOnyxOctagon}
|
||||
rightChildren={
|
||||
<Button href="/app/agents/create?admin=true" icon={SvgPlus}>
|
||||
New Agent
|
||||
</Button>
|
||||
<Link href="/app/agents/create?admin=true">
|
||||
<Button icon={SvgPlus}>New Agent</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<SettingsLayouts.Body>
|
||||
|
||||
@@ -55,7 +55,7 @@ import useOpenApiTools from "@/hooks/useOpenApiTools";
|
||||
import * as ExpandableCard from "@/layouts/expandable-card-layouts";
|
||||
import * as ActionsLayouts from "@/layouts/actions-layouts";
|
||||
import { getActionIcon } from "@/lib/tools/mcpUtils";
|
||||
import { Disabled, Hoverable } from "@opal/core";
|
||||
import { Disabled } from "@opal/core";
|
||||
import IconButton from "@/refresh-components/buttons/IconButton";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import useFilter from "@/hooks/useFilter";
|
||||
@@ -282,7 +282,7 @@ function NumericLimitField({
|
||||
};
|
||||
|
||||
return (
|
||||
<Hoverable.Root group="numericLimit" widthVariant="full">
|
||||
<div className="group w-full">
|
||||
<InputTypeInField
|
||||
name={name}
|
||||
inputMode="numeric"
|
||||
@@ -292,7 +292,7 @@ function NumericLimitField({
|
||||
variant={isOverMax ? "error" : undefined}
|
||||
rightSection={
|
||||
(value || "") !== defaultValue ? (
|
||||
<Hoverable.Item group="numericLimit" variant="opacity-on-hover">
|
||||
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity">
|
||||
<IconButton
|
||||
icon={SvgRefreshCw}
|
||||
tooltip="Restore default"
|
||||
@@ -300,12 +300,12 @@ function NumericLimitField({
|
||||
type="button"
|
||||
onClick={handleRestore}
|
||||
/>
|
||||
</Hoverable.Item>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
</Hoverable.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -417,7 +417,7 @@ function ChatPreferencesForm() {
|
||||
// Default agent configuration (system prompt)
|
||||
const { data: defaultAgentConfig, mutate: mutateDefaultAgent } =
|
||||
useSWR<DefaultAgentConfiguration>(
|
||||
SWR_KEYS.defaultAssistantConfig,
|
||||
"/api/admin/default-assistant/configuration",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import Separator from "@/refresh-components/Separator";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import useAdminUsers from "@/hooks/useAdminUsers";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import type { ApiKeyDescriptor, MemberRow } from "./interfaces";
|
||||
import {
|
||||
createGroup,
|
||||
@@ -48,7 +47,7 @@ function CreateGroupPage() {
|
||||
data: apiKeys,
|
||||
isLoading: apiKeysLoading,
|
||||
error: apiKeysError,
|
||||
} = useSWR<ApiKeyDescriptor[]>(SWR_KEYS.adminApiKeys, errorHandlingFetcher);
|
||||
} = useSWR<ApiKeyDescriptor[]>("/api/admin/api-key", errorHandlingFetcher);
|
||||
|
||||
const isLoading = usersLoading || apiKeysLoading;
|
||||
const error = usersError ?? apiKeysError;
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
PAGE_SIZE,
|
||||
} from "./shared";
|
||||
import {
|
||||
USER_GROUP_URL,
|
||||
renameGroup,
|
||||
updateGroup,
|
||||
deleteGroup,
|
||||
@@ -41,7 +42,6 @@ import {
|
||||
updateDocSetGroupSharing,
|
||||
saveTokenLimits,
|
||||
} from "./svc";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import SharedGroupResources from "@/refresh-pages/admin/GroupsPage/SharedGroupResources";
|
||||
import TokenLimitSection from "./TokenLimitSection";
|
||||
import type { TokenLimit } from "./TokenLimitSection";
|
||||
@@ -66,7 +66,7 @@ function EditGroupPage({ groupId }: EditGroupPageProps) {
|
||||
data: groups,
|
||||
isLoading: groupLoading,
|
||||
error: groupError,
|
||||
} = useSWR<UserGroup[]>(SWR_KEYS.adminUserGroups, errorHandlingFetcher, {
|
||||
} = useSWR<UserGroup[]>(USER_GROUP_URL, errorHandlingFetcher, {
|
||||
refreshInterval: (latestData) => {
|
||||
const g = latestData?.find((g) => g.id === groupId);
|
||||
return g && !g.is_up_to_date ? 5000 : 0;
|
||||
@@ -83,7 +83,7 @@ function EditGroupPage({ groupId }: EditGroupPageProps) {
|
||||
// Fetch token rate limits for this group
|
||||
const { data: tokenRateLimits, isLoading: tokenLimitsLoading } = useSWR<
|
||||
TokenRateLimitDisplay[]
|
||||
>(SWR_KEYS.userGroupTokenRateLimit(groupId), errorHandlingFetcher);
|
||||
>(`/api/admin/token-rate-limits/user-group/${groupId}`, errorHandlingFetcher);
|
||||
|
||||
// Form state
|
||||
const [groupName, setGroupName] = useState("");
|
||||
@@ -111,7 +111,7 @@ function EditGroupPage({ groupId }: EditGroupPageProps) {
|
||||
data: apiKeys,
|
||||
isLoading: apiKeysLoading,
|
||||
error: apiKeysError,
|
||||
} = useSWR<ApiKeyDescriptor[]>(SWR_KEYS.adminApiKeys, errorHandlingFetcher);
|
||||
} = useSWR<ApiKeyDescriptor[]>("/api/admin/api-key", errorHandlingFetcher);
|
||||
|
||||
const isLoading =
|
||||
groupLoading || usersLoading || apiKeysLoading || tokenLimitsLoading;
|
||||
@@ -216,9 +216,7 @@ function EditGroupPage({ groupId }: EditGroupPageProps) {
|
||||
}
|
||||
|
||||
// Re-fetch group to check sync status before saving
|
||||
const freshGroups = await fetch(SWR_KEYS.adminUserGroups).then((r) =>
|
||||
r.json()
|
||||
);
|
||||
const freshGroups = await fetch(USER_GROUP_URL).then((r) => r.json());
|
||||
const freshGroup = freshGroups.find((g: UserGroup) => g.id === groupId);
|
||||
if (freshGroup && !freshGroup.is_up_to_date) {
|
||||
toast.error(
|
||||
@@ -259,8 +257,8 @@ function EditGroupPage({ groupId }: EditGroupPageProps) {
|
||||
initialAgentIdsRef.current = selectedAgentIds;
|
||||
initialDocSetIdsRef.current = selectedDocSetIds;
|
||||
|
||||
mutate(SWR_KEYS.adminUserGroups);
|
||||
mutate(SWR_KEYS.userGroupTokenRateLimit(groupId));
|
||||
mutate(USER_GROUP_URL);
|
||||
mutate(`/api/admin/token-rate-limits/user-group/${groupId}`);
|
||||
toast.success(`Group "${trimmed}" updated`);
|
||||
router.push("/admin/groups");
|
||||
} catch (e) {
|
||||
@@ -275,7 +273,7 @@ function EditGroupPage({ groupId }: EditGroupPageProps) {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteGroup(groupId);
|
||||
mutate(SWR_KEYS.adminUserGroups);
|
||||
mutate(USER_GROUP_URL);
|
||||
toast.success(`Group "${group?.name}" deleted`);
|
||||
router.push("/admin/groups");
|
||||
} catch (e) {
|
||||
|
||||
@@ -14,10 +14,9 @@ import {
|
||||
buildGroupDescription,
|
||||
formatMemberCount,
|
||||
} from "./utils";
|
||||
import { renameGroup } from "./svc";
|
||||
import { renameGroup, USER_GROUP_URL } from "./svc";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
interface GroupCardProps {
|
||||
group: UserGroup;
|
||||
@@ -34,7 +33,7 @@ function GroupCard({ group }: GroupCardProps) {
|
||||
async function handleRename(newName: string) {
|
||||
try {
|
||||
await renameGroup(group.id, newName);
|
||||
mutate(SWR_KEYS.adminUserGroups);
|
||||
mutate(USER_GROUP_URL);
|
||||
toast.success(`Group renamed to "${newName}"`);
|
||||
} catch (e) {
|
||||
console.error("Failed to rename group:", e);
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import type { UserGroup } from "@/lib/types";
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
import { USER_GROUP_URL } from "./svc";
|
||||
import GroupsList from "./GroupsList";
|
||||
import AdminListHeader from "@/sections/admin/AdminListHeader";
|
||||
import { IllustrationContent } from "@opal/layouts";
|
||||
@@ -23,7 +23,7 @@ function GroupsPage() {
|
||||
data: groups,
|
||||
error,
|
||||
isLoading,
|
||||
} = useSWR<UserGroup[]>(SWR_KEYS.adminUserGroups, errorHandlingFetcher);
|
||||
} = useSWR<UserGroup[]>(USER_GROUP_URL, errorHandlingFetcher);
|
||||
|
||||
return (
|
||||
<SettingsLayouts.Root>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/** API helpers for the Groups pages. */
|
||||
|
||||
import { SWR_KEYS } from "@/lib/swr-keys";
|
||||
|
||||
const USER_GROUP_URL = SWR_KEYS.adminUserGroups;
|
||||
const USER_GROUP_URL = "/api/manage/admin/user-group";
|
||||
|
||||
async function renameGroup(groupId: number, newName: string): Promise<void> {
|
||||
const res = await fetch(`${USER_GROUP_URL}/rename`, {
|
||||
@@ -282,6 +280,7 @@ async function saveTokenLimits(
|
||||
}
|
||||
|
||||
export {
|
||||
USER_GROUP_URL,
|
||||
renameGroup,
|
||||
createGroup,
|
||||
updateGroup,
|
||||
|
||||
@@ -22,15 +22,15 @@ import Modal, { BasicModalFooter } from "@/refresh-components/Modal";
|
||||
import type {
|
||||
HookPointMeta,
|
||||
HookResponse,
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
import {
|
||||
activateHook,
|
||||
deactivateHook,
|
||||
deleteHook,
|
||||
validateHook,
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/svc";
|
||||
import { getHookPointIcon } from "@/ee/refresh-pages/admin/HooksPage/hookPointIcons";
|
||||
import HookStatusPopover from "@/ee/refresh-pages/admin/HooksPage/HookStatusPopover";
|
||||
} from "@/refresh-pages/admin/HooksPage/svc";
|
||||
import { getHookPointIcon } from "@/refresh-pages/admin/HooksPage/hookPointIcons";
|
||||
import HookStatusPopover from "@/refresh-pages/admin/HooksPage/HookStatusPopover";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sub-component: disconnect confirmation modal
|
||||
@@ -23,14 +23,14 @@ import {
|
||||
HookAuthError,
|
||||
HookTimeoutError,
|
||||
HookConnectError,
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/svc";
|
||||
} from "@/refresh-pages/admin/HooksPage/svc";
|
||||
import type {
|
||||
HookFailStrategy,
|
||||
HookFormState,
|
||||
HookPointMeta,
|
||||
HookResponse,
|
||||
HookUpdateRequest,
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -5,7 +5,7 @@ import { SvgDownload, SvgTextLines } from "@opal/icons";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
|
||||
import { useHookExecutionLogs } from "@/ee/hooks/useHookExecutionLogs";
|
||||
import { useHookExecutionLogs } from "@/hooks/useHookExecutionLogs";
|
||||
import { formatDateTimeLog } from "@/lib/dateUtils";
|
||||
import { downloadFile } from "@/lib/download";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
HookExecutionRecord,
|
||||
HookPointMeta,
|
||||
HookResponse,
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
interface HookLogsModalProps {
|
||||
open: boolean;
|
||||
@@ -16,12 +16,12 @@ import {
|
||||
SvgXOctagon,
|
||||
} from "@opal/icons";
|
||||
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
|
||||
import { useHookExecutionLogs } from "@/ee/hooks/useHookExecutionLogs";
|
||||
import HookLogsModal from "@/ee/refresh-pages/admin/HooksPage/HookLogsModal";
|
||||
import { useHookExecutionLogs } from "@/hooks/useHookExecutionLogs";
|
||||
import HookLogsModal from "@/refresh-pages/admin/HooksPage/HookLogsModal";
|
||||
import type {
|
||||
HookPointMeta,
|
||||
HookResponse,
|
||||
} from "@/ee/refresh-pages/admin/HooksPage/interfaces";
|
||||
} from "@/refresh-pages/admin/HooksPage/interfaces";
|
||||
|
||||
interface HookStatusPopoverProps {
|
||||
hook: HookResponse;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user