mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-04 23:25:46 +00:00
Compare commits
8 Commits
main
...
nikg/std-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe942daf30 | ||
|
|
edad23a7b7 | ||
|
|
94ab65e47d | ||
|
|
cce2a2c2d4 | ||
|
|
628b88740f | ||
|
|
06cf7c5bdd | ||
|
|
b9c7c1cd3b | ||
|
|
4dfc64d6cf |
1
.github/workflows/pr-integration-tests.yml
vendored
1
.github/workflows/pr-integration-tests.yml
vendored
@@ -335,6 +335,7 @@ jobs:
|
||||
# TODO(Nik): https://linear.app/onyx-app/issue/ENG-1/update-test-infra-to-use-test-license
|
||||
LICENSE_ENFORCEMENT_ENABLED=false
|
||||
CHECK_TTL_MANAGEMENT_TASK_FREQUENCY_IN_HOURS=0.001
|
||||
USE_LIGHTWEIGHT_BACKGROUND_WORKER=false
|
||||
EOF
|
||||
fi
|
||||
|
||||
|
||||
108
.github/workflows/pr-playwright-tests.yml
vendored
108
.github/workflows/pr-playwright-tests.yml
vendored
@@ -268,11 +268,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup node
|
||||
# zizmor: ignore[cache-poisoning] ephemeral runners; no release artifacts
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm" # zizmor: ignore[cache-poisoning]
|
||||
cache: "npm"
|
||||
cache-dependency-path: ./web/package-lock.json
|
||||
|
||||
- name: Install node dependencies
|
||||
@@ -280,7 +279,6 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Cache playwright cache
|
||||
# zizmor: ignore[cache-poisoning] ephemeral runners; no release artifacts
|
||||
uses: runs-on/cache@50350ad4242587b6c8c2baa2e740b1bc11285ff4 # ratchet:runs-on/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
@@ -592,108 +590,6 @@ jobs:
|
||||
name: docker-logs-${{ matrix.project }}-${{ github.run_id }}
|
||||
path: ${{ github.workspace }}/docker-compose.log
|
||||
|
||||
playwright-tests-lite:
|
||||
needs: [build-web-image, build-backend-image]
|
||||
name: Playwright Tests (lite)
|
||||
runs-on:
|
||||
- runs-on
|
||||
- runner=4cpu-linux-arm64
|
||||
- "run-id=${{ github.run_id }}-playwright-tests-lite"
|
||||
- "extras=ecr-cache"
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup node
|
||||
# zizmor: ignore[cache-poisoning] ephemeral runners; no release artifacts
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm" # zizmor: ignore[cache-poisoning]
|
||||
cache-dependency-path: ./web/package-lock.json
|
||||
|
||||
- name: Install node dependencies
|
||||
working-directory: ./web
|
||||
run: npm ci
|
||||
|
||||
- name: Cache playwright cache
|
||||
# zizmor: ignore[cache-poisoning] ephemeral runners; no release artifacts
|
||||
uses: runs-on/cache@50350ad4242587b6c8c2baa2e740b1bc11285ff4 # ratchet:runs-on/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-npm-${{ hashFiles('web/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-playwright-npm-
|
||||
|
||||
- name: Install playwright browsers
|
||||
working-directory: ./web
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Create .env file for Docker Compose
|
||||
env:
|
||||
OPENAI_API_KEY_VALUE: ${{ env.OPENAI_API_KEY }}
|
||||
ECR_CACHE: ${{ env.RUNS_ON_ECR_CACHE }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
cat <<EOF > deployment/docker_compose/.env
|
||||
ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=true
|
||||
LICENSE_ENFORCEMENT_ENABLED=false
|
||||
AUTH_TYPE=basic
|
||||
INTEGRATION_TESTS_MODE=true
|
||||
GEN_AI_API_KEY=${OPENAI_API_KEY_VALUE}
|
||||
MOCK_LLM_RESPONSE=true
|
||||
REQUIRE_EMAIL_VERIFICATION=false
|
||||
DISABLE_TELEMETRY=true
|
||||
ONYX_BACKEND_IMAGE=${ECR_CACHE}:playwright-test-backend-${RUN_ID}
|
||||
ONYX_WEB_SERVER_IMAGE=${ECR_CACHE}:playwright-test-web-${RUN_ID}
|
||||
EOF
|
||||
|
||||
# needed for pulling external images otherwise, we hit the "Unauthenticated users" limit
|
||||
# https://docs.docker.com/docker-hub/usage/
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # ratchet:docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Start Docker containers (lite)
|
||||
run: |
|
||||
cd deployment/docker_compose
|
||||
docker compose -f docker-compose.yml -f docker-compose.onyx-lite.yml -f docker-compose.dev.yml up -d
|
||||
id: start_docker
|
||||
|
||||
- name: Run Playwright tests (lite)
|
||||
working-directory: ./web
|
||||
run: npx playwright test --project lite
|
||||
|
||||
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-test-results-lite-${{ github.run_id }}
|
||||
path: ./web/output/playwright/
|
||||
retention-days: 30
|
||||
|
||||
- name: Save Docker logs
|
||||
if: success() || failure()
|
||||
env:
|
||||
WORKSPACE: ${{ github.workspace }}
|
||||
run: |
|
||||
cd deployment/docker_compose
|
||||
docker compose logs > docker-compose.log
|
||||
mv docker-compose.log ${WORKSPACE}/docker-compose.log
|
||||
|
||||
- name: Upload logs
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
||||
with:
|
||||
name: docker-logs-lite-${{ github.run_id }}
|
||||
path: ${{ github.workspace }}/docker-compose.log
|
||||
|
||||
# Post a single combined visual regression comment after all matrix jobs finish
|
||||
visual-regression-comment:
|
||||
needs: [playwright-tests]
|
||||
@@ -790,7 +686,7 @@ jobs:
|
||||
# NOTE: Github-hosted runners have about 20s faster queue times and are preferred here.
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 45
|
||||
needs: [playwright-tests, playwright-tests-lite]
|
||||
needs: [playwright-tests]
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: Check job status
|
||||
|
||||
43
.vscode/launch.json
vendored
43
.vscode/launch.json
vendored
@@ -40,7 +40,19 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Celery",
|
||||
"name": "Celery (lightweight mode)",
|
||||
"configurations": [
|
||||
"Celery primary",
|
||||
"Celery background",
|
||||
"Celery beat"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "1"
|
||||
},
|
||||
"stopAll": true
|
||||
},
|
||||
{
|
||||
"name": "Celery (standard mode)",
|
||||
"configurations": [
|
||||
"Celery primary",
|
||||
"Celery light",
|
||||
@@ -241,6 +253,35 @@
|
||||
},
|
||||
"consoleTitle": "Celery light Console"
|
||||
},
|
||||
{
|
||||
"name": "Celery background",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "celery",
|
||||
"cwd": "${workspaceFolder}/backend",
|
||||
"envFile": "${workspaceFolder}/.vscode/.env",
|
||||
"env": {
|
||||
"LOG_LEVEL": "INFO",
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"PYTHONPATH": "."
|
||||
},
|
||||
"args": [
|
||||
"-A",
|
||||
"onyx.background.celery.versioned_apps.background",
|
||||
"worker",
|
||||
"--pool=threads",
|
||||
"--concurrency=20",
|
||||
"--prefetch-multiplier=4",
|
||||
"--loglevel=INFO",
|
||||
"--hostname=background@%n",
|
||||
"-Q",
|
||||
"vespa_metadata_sync,connector_deletion,doc_permissions_upsert,checkpoint_cleanup,index_attempt_cleanup,docprocessing,connector_doc_fetching,connector_pruning,connector_doc_permissions_sync,connector_external_group_sync,csv_generation,kg_processing,monitoring,user_file_processing,user_file_project_sync,user_file_delete,opensearch_migration"
|
||||
],
|
||||
"presentation": {
|
||||
"group": "2"
|
||||
},
|
||||
"consoleTitle": "Celery background Console"
|
||||
},
|
||||
{
|
||||
"name": "Celery heavy",
|
||||
"type": "debugpy",
|
||||
|
||||
31
AGENTS.md
31
AGENTS.md
@@ -86,6 +86,37 @@ Onyx uses Celery for asynchronous task processing with multiple specialized work
|
||||
- Monitoring tasks (every 5 minutes)
|
||||
- Cleanup tasks (hourly)
|
||||
|
||||
#### Worker Deployment Modes
|
||||
|
||||
Onyx supports two deployment modes for background workers, controlled by the `USE_LIGHTWEIGHT_BACKGROUND_WORKER` environment variable:
|
||||
|
||||
**Lightweight Mode** (default, `USE_LIGHTWEIGHT_BACKGROUND_WORKER=true`):
|
||||
|
||||
- Runs a single consolidated `background` worker that handles all background tasks:
|
||||
- Light worker tasks (Vespa operations, permissions sync, deletion)
|
||||
- Document processing (indexing pipeline)
|
||||
- Document fetching (connector data retrieval)
|
||||
- Pruning operations (from `heavy` worker)
|
||||
- Knowledge graph processing (from `kg_processing` worker)
|
||||
- Monitoring tasks (from `monitoring` worker)
|
||||
- User file processing (from `user_file_processing` worker)
|
||||
- Lower resource footprint (fewer worker processes)
|
||||
- Suitable for smaller deployments or development environments
|
||||
- Default concurrency: 20 threads (increased to handle combined workload)
|
||||
|
||||
**Standard Mode** (`USE_LIGHTWEIGHT_BACKGROUND_WORKER=false`):
|
||||
|
||||
- Runs separate specialized workers as documented above (light, docprocessing, docfetching, heavy, kg_processing, monitoring, user_file_processing)
|
||||
- Better isolation and scalability
|
||||
- Can scale individual workers independently based on workload
|
||||
- Suitable for production deployments with higher load
|
||||
|
||||
The deployment mode affects:
|
||||
|
||||
- **Backend**: Worker processes spawned by supervisord or dev scripts
|
||||
- **Helm**: Which Kubernetes deployments are created
|
||||
- **Dev Environment**: Which workers `dev_run_background_jobs.py` spawns
|
||||
|
||||
#### Key Features
|
||||
|
||||
- **Thread-based Workers**: All workers use thread pools (not processes) for stability
|
||||
|
||||
15
backend/ee/onyx/background/celery/apps/background.py
Normal file
15
backend/ee/onyx/background/celery/apps/background.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from onyx.background.celery.apps import app_base
|
||||
from onyx.background.celery.apps.background import celery_app
|
||||
|
||||
|
||||
celery_app.autodiscover_tasks(
|
||||
app_base.filter_task_modules(
|
||||
[
|
||||
"ee.onyx.background.celery.tasks.doc_permission_syncing",
|
||||
"ee.onyx.background.celery.tasks.external_group_syncing",
|
||||
"ee.onyx.background.celery.tasks.cleanup",
|
||||
"ee.onyx.background.celery.tasks.tenant_provisioning",
|
||||
"ee.onyx.background.celery.tasks.query_history",
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -15,7 +15,6 @@ from sqlalchemy.orm import Session
|
||||
from ee.onyx.server.user_group.models import SetCuratorRequest
|
||||
from ee.onyx.server.user_group.models import UserGroupCreate
|
||||
from ee.onyx.server.user_group.models import UserGroupUpdate
|
||||
from onyx.configs.app_configs import DISABLE_VECTOR_DB
|
||||
from onyx.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
from onyx.db.enums import AccessType
|
||||
from onyx.db.enums import ConnectorCredentialPairStatus
|
||||
@@ -472,9 +471,7 @@ def _add_user_group__cc_pair_relationships__no_commit(
|
||||
|
||||
def insert_user_group(db_session: Session, user_group: UserGroupCreate) -> UserGroup:
|
||||
db_user_group = UserGroup(
|
||||
name=user_group.name,
|
||||
time_last_modified_by_user=func.now(),
|
||||
is_up_to_date=DISABLE_VECTOR_DB,
|
||||
name=user_group.name, time_last_modified_by_user=func.now()
|
||||
)
|
||||
db_session.add(db_user_group)
|
||||
db_session.flush() # give the group an ID
|
||||
@@ -777,7 +774,8 @@ def update_user_group(
|
||||
cc_pair_ids=user_group_update.cc_pair_ids,
|
||||
)
|
||||
|
||||
if cc_pairs_updated and not DISABLE_VECTOR_DB:
|
||||
# only needs to sync with Vespa if the cc_pairs have been updated
|
||||
if cc_pairs_updated:
|
||||
db_user_group.is_up_to_date = False
|
||||
|
||||
removed_users = db_session.scalars(
|
||||
|
||||
@@ -223,15 +223,6 @@ def get_active_scim_token(
|
||||
token = dal.get_active_token()
|
||||
if not token:
|
||||
raise HTTPException(status_code=404, detail="No active SCIM token")
|
||||
|
||||
# Derive the IdP domain from the first synced user as a heuristic.
|
||||
idp_domain: str | None = None
|
||||
mappings, _total = dal.list_user_mappings(start_index=1, count=1)
|
||||
if mappings:
|
||||
user = dal.get_user(mappings[0].user_id)
|
||||
if user and "@" in user.email:
|
||||
idp_domain = user.email.rsplit("@", 1)[1]
|
||||
|
||||
return ScimTokenResponse(
|
||||
id=token.id,
|
||||
name=token.name,
|
||||
@@ -239,7 +230,6 @@ def get_active_scim_token(
|
||||
is_active=token.is_active,
|
||||
created_at=token.created_at,
|
||||
last_used_at=token.last_used_at,
|
||||
idp_domain=idp_domain,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -365,7 +365,6 @@ class ScimTokenResponse(BaseModel):
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
last_used_at: datetime | None = None
|
||||
idp_domain: str | None = None
|
||||
|
||||
|
||||
class ScimTokenCreatedResponse(ScimTokenResponse):
|
||||
|
||||
@@ -5,8 +5,6 @@ from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ee.onyx.db.user_group import add_users_to_user_group
|
||||
from ee.onyx.db.user_group import delete_user_group as db_delete_user_group
|
||||
from ee.onyx.db.user_group import fetch_user_group
|
||||
from ee.onyx.db.user_group import fetch_user_groups
|
||||
from ee.onyx.db.user_group import fetch_user_groups_for_user
|
||||
from ee.onyx.db.user_group import insert_user_group
|
||||
@@ -22,7 +20,6 @@ from ee.onyx.server.user_group.models import UserGroupUpdate
|
||||
from onyx.auth.users import current_admin_user
|
||||
from onyx.auth.users import current_curator_or_admin_user
|
||||
from onyx.auth.users import current_user
|
||||
from onyx.configs.app_configs import DISABLE_VECTOR_DB
|
||||
from onyx.configs.constants import PUBLIC_API_TAGS
|
||||
from onyx.db.engine.sql_engine import get_session
|
||||
from onyx.db.models import User
|
||||
@@ -156,8 +153,3 @@ def delete_user_group(
|
||||
prepare_user_group_for_deletion(db_session, user_group_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
|
||||
if DISABLE_VECTOR_DB:
|
||||
user_group = fetch_user_group(db_session, user_group_id)
|
||||
if user_group:
|
||||
db_delete_user_group(db_session, user_group)
|
||||
|
||||
142
backend/onyx/background/celery/apps/background.py
Normal file
142
backend/onyx/background/celery/apps/background.py
Normal file
@@ -0,0 +1,142 @@
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
|
||||
from celery import Celery
|
||||
from celery import signals
|
||||
from celery import Task
|
||||
from celery.apps.worker import Worker
|
||||
from celery.signals import celeryd_init
|
||||
from celery.signals import worker_init
|
||||
from celery.signals import worker_process_init
|
||||
from celery.signals import worker_ready
|
||||
from celery.signals import worker_shutdown
|
||||
|
||||
import onyx.background.celery.apps.app_base as app_base
|
||||
from onyx.background.celery.celery_utils import httpx_init_vespa_pool
|
||||
from onyx.configs.app_configs import MANAGED_VESPA
|
||||
from onyx.configs.app_configs import VESPA_CLOUD_CERT_PATH
|
||||
from onyx.configs.app_configs import VESPA_CLOUD_KEY_PATH
|
||||
from onyx.configs.constants import POSTGRES_CELERY_WORKER_BACKGROUND_APP_NAME
|
||||
from onyx.db.engine.sql_engine import SqlEngine
|
||||
from onyx.utils.logger import setup_logger
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
celery_app = Celery(__name__)
|
||||
celery_app.config_from_object("onyx.background.celery.configs.background")
|
||||
celery_app.Task = app_base.TenantAwareTask # type: ignore [misc]
|
||||
|
||||
|
||||
@signals.task_prerun.connect
|
||||
def on_task_prerun(
|
||||
sender: Any | None = None,
|
||||
task_id: str | None = None,
|
||||
task: Task | None = None,
|
||||
args: tuple | None = None,
|
||||
kwargs: dict | None = None,
|
||||
**kwds: Any,
|
||||
) -> None:
|
||||
app_base.on_task_prerun(sender, task_id, task, args, kwargs, **kwds)
|
||||
|
||||
|
||||
@signals.task_postrun.connect
|
||||
def on_task_postrun(
|
||||
sender: Any | None = None,
|
||||
task_id: str | None = None,
|
||||
task: Task | None = None,
|
||||
args: tuple | None = None,
|
||||
kwargs: dict | None = None,
|
||||
retval: Any | None = None,
|
||||
state: str | None = None,
|
||||
**kwds: Any,
|
||||
) -> None:
|
||||
app_base.on_task_postrun(sender, task_id, task, args, kwargs, retval, state, **kwds)
|
||||
|
||||
|
||||
@celeryd_init.connect
|
||||
def on_celeryd_init(sender: str, conf: Any = None, **kwargs: Any) -> None:
|
||||
app_base.on_celeryd_init(sender, conf, **kwargs)
|
||||
|
||||
|
||||
@worker_init.connect
|
||||
def on_worker_init(sender: Worker, **kwargs: Any) -> None:
|
||||
EXTRA_CONCURRENCY = 8 # small extra fudge factor for connection limits
|
||||
|
||||
logger.info("worker_init signal received for consolidated background worker.")
|
||||
|
||||
SqlEngine.set_app_name(POSTGRES_CELERY_WORKER_BACKGROUND_APP_NAME)
|
||||
pool_size = cast(int, sender.concurrency) # type: ignore
|
||||
SqlEngine.init_engine(pool_size=pool_size, max_overflow=EXTRA_CONCURRENCY)
|
||||
|
||||
# Initialize Vespa httpx pool (needed for light worker tasks)
|
||||
if MANAGED_VESPA:
|
||||
httpx_init_vespa_pool(
|
||||
sender.concurrency + EXTRA_CONCURRENCY, # type: ignore
|
||||
ssl_cert=VESPA_CLOUD_CERT_PATH,
|
||||
ssl_key=VESPA_CLOUD_KEY_PATH,
|
||||
)
|
||||
else:
|
||||
httpx_init_vespa_pool(sender.concurrency + EXTRA_CONCURRENCY) # type: ignore
|
||||
|
||||
app_base.wait_for_redis(sender, **kwargs)
|
||||
app_base.wait_for_db(sender, **kwargs)
|
||||
app_base.wait_for_vespa_or_shutdown(sender, **kwargs)
|
||||
|
||||
# Less startup checks in multi-tenant case
|
||||
if MULTI_TENANT:
|
||||
return
|
||||
|
||||
app_base.on_secondary_worker_init(sender, **kwargs)
|
||||
|
||||
|
||||
@worker_ready.connect
|
||||
def on_worker_ready(sender: Any, **kwargs: Any) -> None:
|
||||
app_base.on_worker_ready(sender, **kwargs)
|
||||
|
||||
|
||||
@worker_shutdown.connect
|
||||
def on_worker_shutdown(sender: Any, **kwargs: Any) -> None:
|
||||
app_base.on_worker_shutdown(sender, **kwargs)
|
||||
|
||||
|
||||
@worker_process_init.connect
|
||||
def init_worker(**kwargs: Any) -> None: # noqa: ARG001
|
||||
SqlEngine.reset_engine()
|
||||
|
||||
|
||||
@signals.setup_logging.connect
|
||||
def on_setup_logging(
|
||||
loglevel: Any, logfile: Any, format: Any, colorize: Any, **kwargs: Any
|
||||
) -> None:
|
||||
app_base.on_setup_logging(loglevel, logfile, format, colorize, **kwargs)
|
||||
|
||||
|
||||
base_bootsteps = app_base.get_bootsteps()
|
||||
for bootstep in base_bootsteps:
|
||||
celery_app.steps["worker"].add(bootstep)
|
||||
|
||||
celery_app.autodiscover_tasks(
|
||||
app_base.filter_task_modules(
|
||||
[
|
||||
# Original background worker tasks
|
||||
"onyx.background.celery.tasks.pruning",
|
||||
"onyx.background.celery.tasks.monitoring",
|
||||
"onyx.background.celery.tasks.user_file_processing",
|
||||
"onyx.background.celery.tasks.llm_model_update",
|
||||
# Light worker tasks
|
||||
"onyx.background.celery.tasks.shared",
|
||||
"onyx.background.celery.tasks.vespa",
|
||||
"onyx.background.celery.tasks.connector_deletion",
|
||||
"onyx.background.celery.tasks.doc_permission_syncing",
|
||||
"onyx.background.celery.tasks.opensearch_migration",
|
||||
# Docprocessing worker tasks
|
||||
"onyx.background.celery.tasks.docprocessing",
|
||||
# Docfetching worker tasks
|
||||
"onyx.background.celery.tasks.docfetching",
|
||||
# Sandbox cleanup tasks (isolated in build feature)
|
||||
"onyx.server.features.build.sandbox.tasks",
|
||||
]
|
||||
)
|
||||
)
|
||||
23
backend/onyx/background/celery/configs/background.py
Normal file
23
backend/onyx/background/celery/configs/background.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import onyx.background.celery.configs.base as shared_config
|
||||
from onyx.configs.app_configs import CELERY_WORKER_BACKGROUND_CONCURRENCY
|
||||
|
||||
broker_url = shared_config.broker_url
|
||||
broker_connection_retry_on_startup = shared_config.broker_connection_retry_on_startup
|
||||
broker_pool_limit = shared_config.broker_pool_limit
|
||||
broker_transport_options = shared_config.broker_transport_options
|
||||
|
||||
redis_socket_keepalive = shared_config.redis_socket_keepalive
|
||||
redis_retry_on_timeout = shared_config.redis_retry_on_timeout
|
||||
redis_backend_health_check_interval = shared_config.redis_backend_health_check_interval
|
||||
|
||||
result_backend = shared_config.result_backend
|
||||
result_expires = shared_config.result_expires # 86400 seconds is the default
|
||||
|
||||
task_default_priority = shared_config.task_default_priority
|
||||
task_acks_late = shared_config.task_acks_late
|
||||
|
||||
worker_concurrency = CELERY_WORKER_BACKGROUND_CONCURRENCY
|
||||
worker_pool = "threads"
|
||||
# Increased from 1 to 4 to handle fast light worker tasks more efficiently
|
||||
# This allows the worker to prefetch multiple tasks per thread
|
||||
worker_prefetch_multiplier = 4
|
||||
10
backend/onyx/background/celery/versioned_apps/background.py
Normal file
10
backend/onyx/background/celery/versioned_apps/background.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from celery import Celery
|
||||
|
||||
from onyx.utils.variable_functionality import fetch_versioned_implementation
|
||||
from onyx.utils.variable_functionality import set_is_ee_based_on_env_variable
|
||||
|
||||
set_is_ee_based_on_env_variable()
|
||||
app: Celery = fetch_versioned_implementation(
|
||||
"onyx.background.celery.apps.background",
|
||||
"celery_app",
|
||||
)
|
||||
@@ -495,7 +495,14 @@ CELERY_WORKER_PRIMARY_POOL_OVERFLOW = int(
|
||||
os.environ.get("CELERY_WORKER_PRIMARY_POOL_OVERFLOW") or 4
|
||||
)
|
||||
|
||||
# Individual worker concurrency settings
|
||||
# Consolidated background worker (light, docprocessing, docfetching, heavy, monitoring, user_file_processing)
|
||||
# separate workers' defaults: light=24, docprocessing=6, docfetching=1, heavy=4, kg=2, monitoring=1, user_file=2
|
||||
# Total would be 40, but we use a more conservative default of 20 for the consolidated worker
|
||||
CELERY_WORKER_BACKGROUND_CONCURRENCY = int(
|
||||
os.environ.get("CELERY_WORKER_BACKGROUND_CONCURRENCY") or 20
|
||||
)
|
||||
|
||||
# Individual worker concurrency settings (used when USE_LIGHTWEIGHT_BACKGROUND_WORKER is False or on Kuberenetes deployments)
|
||||
CELERY_WORKER_HEAVY_CONCURRENCY = int(
|
||||
os.environ.get("CELERY_WORKER_HEAVY_CONCURRENCY") or 4
|
||||
)
|
||||
|
||||
@@ -84,6 +84,7 @@ POSTGRES_CELERY_WORKER_LIGHT_APP_NAME = "celery_worker_light"
|
||||
POSTGRES_CELERY_WORKER_DOCPROCESSING_APP_NAME = "celery_worker_docprocessing"
|
||||
POSTGRES_CELERY_WORKER_DOCFETCHING_APP_NAME = "celery_worker_docfetching"
|
||||
POSTGRES_CELERY_WORKER_INDEXING_CHILD_APP_NAME = "celery_worker_indexing_child"
|
||||
POSTGRES_CELERY_WORKER_BACKGROUND_APP_NAME = "celery_worker_background"
|
||||
POSTGRES_CELERY_WORKER_HEAVY_APP_NAME = "celery_worker_heavy"
|
||||
POSTGRES_CELERY_WORKER_MONITORING_APP_NAME = "celery_worker_monitoring"
|
||||
POSTGRES_CELERY_WORKER_USER_FILE_PROCESSING_APP_NAME = (
|
||||
|
||||
@@ -13,7 +13,6 @@ from sqlalchemy.orm import aliased
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.configs.app_configs import DISABLE_VECTOR_DB
|
||||
from onyx.db.connector_credential_pair import get_cc_pair_groups_for_ids
|
||||
from onyx.db.connector_credential_pair import get_connector_credential_pairs
|
||||
from onyx.db.enums import AccessType
|
||||
@@ -247,7 +246,6 @@ def insert_document_set(
|
||||
description=document_set_creation_request.description,
|
||||
user_id=user_id,
|
||||
is_public=document_set_creation_request.is_public,
|
||||
is_up_to_date=DISABLE_VECTOR_DB,
|
||||
time_last_modified_by_user=func.now(),
|
||||
)
|
||||
db_session.add(new_document_set_row)
|
||||
@@ -338,8 +336,7 @@ def update_document_set(
|
||||
)
|
||||
|
||||
document_set_row.description = document_set_update_request.description
|
||||
if not DISABLE_VECTOR_DB:
|
||||
document_set_row.is_up_to_date = False
|
||||
document_set_row.is_up_to_date = False
|
||||
document_set_row.is_public = document_set_update_request.is_public
|
||||
document_set_row.time_last_modified_by_user = func.now()
|
||||
versioned_private_doc_set_fn = fetch_versioned_implementation(
|
||||
|
||||
@@ -130,7 +130,7 @@ def format_slack_message(message: str | None) -> str:
|
||||
message = _transform_outside_code_blocks(message, _sanitize_html)
|
||||
message = _convert_slack_links_to_markdown(message)
|
||||
normalized_message = _normalize_link_destinations(message)
|
||||
md = create_markdown(renderer=SlackRenderer(), plugins=["strikethrough", "table"])
|
||||
md = create_markdown(renderer=SlackRenderer(), plugins=["strikethrough"])
|
||||
result = md(normalized_message)
|
||||
# With HTMLRenderer, result is always str (not AST list)
|
||||
assert isinstance(result, str)
|
||||
@@ -146,11 +146,6 @@ class SlackRenderer(HTMLRenderer):
|
||||
|
||||
SPECIALS: dict[str, str] = {"&": "&", "<": "<", ">": ">"}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._table_headers: list[str] = []
|
||||
self._current_row_cells: list[str] = []
|
||||
|
||||
def escape_special(self, text: str) -> str:
|
||||
for special, replacement in self.SPECIALS.items():
|
||||
text = text.replace(special, replacement)
|
||||
@@ -223,48 +218,5 @@ class SlackRenderer(HTMLRenderer):
|
||||
# as literal " text since Slack doesn't recognize that entity.
|
||||
return self.escape_special(text)
|
||||
|
||||
# -- Table rendering (converts markdown tables to vertical cards) --
|
||||
|
||||
def table_cell(
|
||||
self, text: str, align: str | None = None, head: bool = False # noqa: ARG002
|
||||
) -> str:
|
||||
if head:
|
||||
self._table_headers.append(text.strip())
|
||||
else:
|
||||
self._current_row_cells.append(text.strip())
|
||||
return ""
|
||||
|
||||
def table_head(self, text: str) -> str: # noqa: ARG002
|
||||
self._current_row_cells = []
|
||||
return ""
|
||||
|
||||
def table_row(self, text: str) -> str: # noqa: ARG002
|
||||
cells = self._current_row_cells
|
||||
self._current_row_cells = []
|
||||
# First column becomes the bold title, remaining columns are bulleted fields
|
||||
lines: list[str] = []
|
||||
if cells:
|
||||
title = cells[0]
|
||||
if title:
|
||||
# Avoid double-wrapping if cell already contains bold markup
|
||||
if title.startswith("*") and title.endswith("*") and len(title) > 1:
|
||||
lines.append(title)
|
||||
else:
|
||||
lines.append(f"*{title}*")
|
||||
for i, cell in enumerate(cells[1:], start=1):
|
||||
if i < len(self._table_headers):
|
||||
lines.append(f" • {self._table_headers[i]}: {cell}")
|
||||
else:
|
||||
lines.append(f" • {cell}")
|
||||
return "\n".join(lines) + "\n\n"
|
||||
|
||||
def table_body(self, text: str) -> str:
|
||||
return text
|
||||
|
||||
def table(self, text: str) -> str:
|
||||
self._table_headers = []
|
||||
self._current_row_cells = []
|
||||
return text + "\n"
|
||||
|
||||
def paragraph(self, text: str) -> str:
|
||||
return f"{text}\n\n"
|
||||
|
||||
@@ -7424,9 +7424,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.5",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz",
|
||||
"integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==",
|
||||
"version": "4.11.7",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
|
||||
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
|
||||
@@ -11,7 +11,6 @@ from onyx.configs.app_configs import DISABLE_VECTOR_DB
|
||||
from onyx.configs.constants import OnyxCeleryPriority
|
||||
from onyx.configs.constants import OnyxCeleryTask
|
||||
from onyx.db.document_set import check_document_sets_are_public
|
||||
from onyx.db.document_set import delete_document_set as db_delete_document_set
|
||||
from onyx.db.document_set import fetch_all_document_sets_for_user
|
||||
from onyx.db.document_set import get_document_set_by_id
|
||||
from onyx.db.document_set import insert_document_set
|
||||
@@ -143,10 +142,7 @@ def delete_document_set(
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
if DISABLE_VECTOR_DB:
|
||||
db_session.refresh(document_set)
|
||||
db_delete_document_set(document_set, db_session)
|
||||
else:
|
||||
if not DISABLE_VECTOR_DB:
|
||||
client_app.send_task(
|
||||
OnyxCeleryTask.CHECK_FOR_VESPA_SYNC_TASK,
|
||||
kwargs={"tenant_id": tenant_id},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.auth.users import current_admin_user
|
||||
@@ -15,6 +14,8 @@ from onyx.db.llm import remove_llm_provider__no_commit
|
||||
from onyx.db.models import LLMProvider as LLMProviderModel
|
||||
from onyx.db.models import ModelConfiguration
|
||||
from onyx.db.models import User
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.image_gen.exceptions import ImageProviderCredentialsError
|
||||
from onyx.image_gen.factory import get_image_generation_provider
|
||||
from onyx.image_gen.factory import validate_credentials
|
||||
@@ -74,9 +75,9 @@ def _build_llm_provider_request(
|
||||
# Clone mode: Only use API key from source provider
|
||||
source_provider = db_session.get(LLMProviderModel, source_llm_provider_id)
|
||||
if not source_provider:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Source LLM provider with id {source_llm_provider_id} not found",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.NOT_FOUND,
|
||||
f"Source LLM provider with id {source_llm_provider_id} not found",
|
||||
)
|
||||
|
||||
_validate_llm_provider_change(
|
||||
@@ -110,9 +111,9 @@ def _build_llm_provider_request(
|
||||
)
|
||||
|
||||
if not provider:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No provider or source llm provided",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"No provider or source llm provided",
|
||||
)
|
||||
|
||||
credentials = ImageGenerationProviderCredentials(
|
||||
@@ -124,9 +125,9 @@ def _build_llm_provider_request(
|
||||
)
|
||||
|
||||
if not validate_credentials(provider, credentials):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Incorrect credentials for {provider}",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.CREDENTIAL_INVALID,
|
||||
f"Incorrect credentials for {provider}",
|
||||
)
|
||||
|
||||
return LLMProviderUpsertRequest(
|
||||
@@ -215,9 +216,9 @@ def test_image_generation(
|
||||
LLMProviderModel, test_request.source_llm_provider_id
|
||||
)
|
||||
if not source_provider:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Source LLM provider with id {test_request.source_llm_provider_id} not found",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.NOT_FOUND,
|
||||
f"Source LLM provider with id {test_request.source_llm_provider_id} not found",
|
||||
)
|
||||
|
||||
_validate_llm_provider_change(
|
||||
@@ -236,9 +237,9 @@ def test_image_generation(
|
||||
provider = source_provider.provider
|
||||
|
||||
if provider is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No provider or source llm provided",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"No provider or source llm provided",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -257,14 +258,14 @@ def test_image_generation(
|
||||
),
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Invalid image generation provider: {provider}",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.NOT_FOUND,
|
||||
f"Invalid image generation provider: {provider}",
|
||||
)
|
||||
except ImageProviderCredentialsError:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid image generation credentials",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.CREDENTIAL_INVALID,
|
||||
"Invalid image generation credentials",
|
||||
)
|
||||
|
||||
quality = _get_test_quality_for_model(test_request.model_name)
|
||||
@@ -276,15 +277,15 @@ def test_image_generation(
|
||||
n=1,
|
||||
quality=quality,
|
||||
)
|
||||
except HTTPException:
|
||||
except OnyxError:
|
||||
raise
|
||||
except Exception as e:
|
||||
# Log only exception type to avoid exposing sensitive data
|
||||
# (LiteLLM errors may contain URLs with API keys or auth tokens)
|
||||
logger.warning(f"Image generation test failed: {type(e).__name__}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Image generation test failed: {type(e).__name__}",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
f"Image generation test failed: {type(e).__name__}",
|
||||
)
|
||||
|
||||
|
||||
@@ -309,9 +310,9 @@ def create_config(
|
||||
db_session, config_create.image_provider_id
|
||||
)
|
||||
if existing_config:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"ImageGenerationConfig with image_provider_id '{config_create.image_provider_id}' already exists",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.DUPLICATE_RESOURCE,
|
||||
f"ImageGenerationConfig with image_provider_id '{config_create.image_provider_id}' already exists",
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -345,10 +346,10 @@ def create_config(
|
||||
db_session.commit()
|
||||
db_session.refresh(config)
|
||||
return ImageGenerationConfigView.from_model(config)
|
||||
except HTTPException:
|
||||
except OnyxError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
|
||||
|
||||
|
||||
@admin_router.get("/config")
|
||||
@@ -373,9 +374,9 @@ def get_config_credentials(
|
||||
"""
|
||||
config = get_image_generation_config(db_session, image_provider_id)
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.NOT_FOUND,
|
||||
f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
|
||||
)
|
||||
|
||||
return ImageGenerationCredentials.from_model(config)
|
||||
@@ -401,9 +402,9 @@ def update_config(
|
||||
# 1. Get existing config
|
||||
existing_config = get_image_generation_config(db_session, image_provider_id)
|
||||
if not existing_config:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.NOT_FOUND,
|
||||
f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
|
||||
)
|
||||
|
||||
old_llm_provider_id = existing_config.model_configuration.llm_provider_id
|
||||
@@ -472,10 +473,10 @@ def update_config(
|
||||
db_session.refresh(existing_config)
|
||||
return ImageGenerationConfigView.from_model(existing_config)
|
||||
|
||||
except HTTPException:
|
||||
except OnyxError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
|
||||
|
||||
|
||||
@admin_router.delete("/config/{image_provider_id}")
|
||||
@@ -489,9 +490,9 @@ def delete_config(
|
||||
# Get the config first to find the associated LLM provider
|
||||
existing_config = get_image_generation_config(db_session, image_provider_id)
|
||||
if not existing_config:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.NOT_FOUND,
|
||||
f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
|
||||
)
|
||||
|
||||
llm_provider_id = existing_config.model_configuration.llm_provider_id
|
||||
@@ -503,10 +504,10 @@ def delete_config(
|
||||
remove_llm_provider__no_commit(db_session, llm_provider_id)
|
||||
|
||||
db_session.commit()
|
||||
except HTTPException:
|
||||
except OnyxError:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, str(e))
|
||||
|
||||
|
||||
@admin_router.post("/config/{image_provider_id}/default")
|
||||
@@ -519,7 +520,7 @@ def set_config_as_default(
|
||||
try:
|
||||
set_default_image_generation_config(db_session, image_provider_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, str(e))
|
||||
|
||||
|
||||
@admin_router.delete("/config/{image_provider_id}/default")
|
||||
@@ -532,4 +533,4 @@ def unset_config_as_default(
|
||||
try:
|
||||
unset_default_image_generation_config(db_session, image_provider_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.NOT_FOUND, str(e))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.auth.users import current_admin_user
|
||||
@@ -21,6 +19,8 @@ from onyx.db.search_settings import get_secondary_search_settings
|
||||
from onyx.db.search_settings import update_current_search_settings
|
||||
from onyx.db.search_settings import update_search_settings_status
|
||||
from onyx.document_index.factory import get_default_document_index
|
||||
from onyx.error_handling.error_codes import OnyxErrorCode
|
||||
from onyx.error_handling.exceptions import OnyxError
|
||||
from onyx.file_processing.unstructured import delete_unstructured_api_key
|
||||
from onyx.file_processing.unstructured import get_unstructured_api_key
|
||||
from onyx.file_processing.unstructured import update_unstructured_api_key
|
||||
@@ -48,9 +48,9 @@ def set_new_search_settings(
|
||||
# NOTE Enable integration external dependency tests in test_search_settings.py
|
||||
# when this is reenabled. They are currently skipped
|
||||
logger.error("Setting new search settings is temporarily disabled.")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="Setting new search settings is temporarily disabled.",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.NOT_IMPLEMENTED,
|
||||
"Setting new search settings is temporarily disabled.",
|
||||
)
|
||||
# if search_settings_new.index_name:
|
||||
# logger.warning("Index name was specified by request, this is not suggested")
|
||||
@@ -191,7 +191,7 @@ def delete_search_settings_endpoint(
|
||||
search_settings_id=deletion_request.search_settings_id,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
|
||||
|
||||
|
||||
@router.get("/get-current-search-settings")
|
||||
@@ -241,9 +241,9 @@ def update_saved_search_settings(
|
||||
) -> None:
|
||||
# Disallow contextual RAG for cloud deployments
|
||||
if MULTI_TENANT and search_settings.enable_contextual_rag:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Contextual RAG disabled in Onyx Cloud",
|
||||
raise OnyxError(
|
||||
OnyxErrorCode.VALIDATION_ERROR,
|
||||
"Contextual RAG disabled in Onyx Cloud",
|
||||
)
|
||||
|
||||
validate_contextual_rag_model(
|
||||
@@ -297,7 +297,7 @@ def validate_contextual_rag_model(
|
||||
model_name=model_name,
|
||||
db_session=db_session,
|
||||
):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_msg)
|
||||
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, error_msg)
|
||||
|
||||
|
||||
def _validate_contextual_rag_model(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from collections.abc import Generator
|
||||
@@ -86,19 +84,6 @@ class CodeInterpreterClient:
|
||||
raise ValueError("CODE_INTERPRETER_BASE_URL not configured")
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.session = requests.Session()
|
||||
self._closed = False
|
||||
|
||||
def __enter__(self) -> CodeInterpreterClient:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
self.session.close()
|
||||
self._closed = True
|
||||
|
||||
def _build_payload(
|
||||
self,
|
||||
@@ -192,11 +177,8 @@ class CodeInterpreterClient:
|
||||
yield from self._batch_as_stream(code, stdin, timeout_ms, files)
|
||||
return
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
yield from self._parse_sse(response)
|
||||
finally:
|
||||
response.close()
|
||||
response.raise_for_status()
|
||||
yield from self._parse_sse(response)
|
||||
|
||||
def _parse_sse(
|
||||
self, response: requests.Response
|
||||
|
||||
@@ -111,8 +111,8 @@ class PythonTool(Tool[PythonToolOverrideKwargs]):
|
||||
if not server.server_enabled:
|
||||
return False
|
||||
|
||||
with CodeInterpreterClient() as client:
|
||||
return client.health(use_cache=True)
|
||||
client = CodeInterpreterClient()
|
||||
return client.health(use_cache=True)
|
||||
|
||||
def tool_definition(self) -> dict:
|
||||
return {
|
||||
@@ -176,203 +176,196 @@ class PythonTool(Tool[PythonToolOverrideKwargs]):
|
||||
)
|
||||
)
|
||||
|
||||
# Create Code Interpreter client — context manager ensures
|
||||
# session.close() is called on every exit path.
|
||||
with CodeInterpreterClient() as client:
|
||||
# Stage chat files for execution
|
||||
files_to_stage: list[FileInput] = []
|
||||
for ind, chat_file in enumerate(chat_files):
|
||||
file_name = chat_file.filename or f"file_{ind}"
|
||||
try:
|
||||
# Upload to Code Interpreter
|
||||
ci_file_id = client.upload_file(chat_file.content, file_name)
|
||||
|
||||
# Stage for execution
|
||||
files_to_stage.append({"path": file_name, "file_id": ci_file_id})
|
||||
|
||||
logger.info(f"Staged file for Python execution: {file_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to stage file {file_name}: {e}")
|
||||
# Create Code Interpreter client
|
||||
client = CodeInterpreterClient()
|
||||
|
||||
# Stage chat files for execution
|
||||
files_to_stage: list[FileInput] = []
|
||||
for ind, chat_file in enumerate(chat_files):
|
||||
file_name = chat_file.filename or f"file_{ind}"
|
||||
try:
|
||||
logger.debug(f"Executing code: {code}")
|
||||
# Upload to Code Interpreter
|
||||
ci_file_id = client.upload_file(chat_file.content, file_name)
|
||||
|
||||
# Execute code with streaming (falls back to batch if unavailable)
|
||||
stdout_parts: list[str] = []
|
||||
stderr_parts: list[str] = []
|
||||
result_event: StreamResultEvent | None = None
|
||||
# Stage for execution
|
||||
files_to_stage.append({"path": file_name, "file_id": ci_file_id})
|
||||
|
||||
for event in client.execute_streaming(
|
||||
code=code,
|
||||
timeout_ms=CODE_INTERPRETER_DEFAULT_TIMEOUT_MS,
|
||||
files=files_to_stage or None,
|
||||
):
|
||||
if isinstance(event, StreamOutputEvent):
|
||||
if event.stream == "stdout":
|
||||
stdout_parts.append(event.data)
|
||||
else:
|
||||
stderr_parts.append(event.data)
|
||||
# Emit incremental delta to frontend
|
||||
self.emitter.emit(
|
||||
Packet(
|
||||
placement=placement,
|
||||
obj=PythonToolDelta(
|
||||
stdout=(
|
||||
event.data if event.stream == "stdout" else ""
|
||||
),
|
||||
stderr=(
|
||||
event.data if event.stream == "stderr" else ""
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
elif isinstance(event, StreamResultEvent):
|
||||
result_event = event
|
||||
elif isinstance(event, StreamErrorEvent):
|
||||
raise RuntimeError(f"Code interpreter error: {event.message}")
|
||||
logger.info(f"Staged file for Python execution: {file_name}")
|
||||
|
||||
if result_event is None:
|
||||
raise RuntimeError(
|
||||
"Code interpreter stream ended without a result event"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to stage file {file_name}: {e}")
|
||||
|
||||
full_stdout = "".join(stdout_parts)
|
||||
full_stderr = "".join(stderr_parts)
|
||||
try:
|
||||
logger.debug(f"Executing code: {code}")
|
||||
|
||||
# Truncate output for LLM consumption
|
||||
truncated_stdout = _truncate_output(
|
||||
full_stdout, CODE_INTERPRETER_MAX_OUTPUT_LENGTH, "stdout"
|
||||
)
|
||||
truncated_stderr = _truncate_output(
|
||||
full_stderr, CODE_INTERPRETER_MAX_OUTPUT_LENGTH, "stderr"
|
||||
)
|
||||
# Execute code with streaming (falls back to batch if unavailable)
|
||||
stdout_parts: list[str] = []
|
||||
stderr_parts: list[str] = []
|
||||
result_event: StreamResultEvent | None = None
|
||||
|
||||
# Handle generated files
|
||||
generated_files: list[PythonExecutionFile] = []
|
||||
generated_file_ids: list[str] = []
|
||||
file_ids_to_cleanup: list[str] = []
|
||||
file_store = get_default_file_store()
|
||||
|
||||
for workspace_file in result_event.files:
|
||||
if workspace_file.kind != "file" or not workspace_file.file_id:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Download file from Code Interpreter
|
||||
file_content = client.download_file(workspace_file.file_id)
|
||||
|
||||
# Determine MIME type from file extension
|
||||
filename = workspace_file.path.split("/")[-1]
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
# Default to binary if we can't determine the type
|
||||
mime_type = mime_type or "application/octet-stream"
|
||||
|
||||
# Save to Onyx file store
|
||||
onyx_file_id = file_store.save_file(
|
||||
content=BytesIO(file_content),
|
||||
display_name=filename,
|
||||
file_origin=FileOrigin.CHAT_UPLOAD,
|
||||
file_type=mime_type,
|
||||
)
|
||||
|
||||
generated_files.append(
|
||||
PythonExecutionFile(
|
||||
filename=filename,
|
||||
file_link=build_full_frontend_file_url(onyx_file_id),
|
||||
)
|
||||
)
|
||||
generated_file_ids.append(onyx_file_id)
|
||||
|
||||
# Mark for cleanup
|
||||
file_ids_to_cleanup.append(workspace_file.file_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to handle generated file "
|
||||
f"{workspace_file.path}: {e}"
|
||||
)
|
||||
|
||||
# Cleanup Code Interpreter files (generated files)
|
||||
for ci_file_id in file_ids_to_cleanup:
|
||||
try:
|
||||
client.delete_file(ci_file_id)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to delete Code Interpreter generated "
|
||||
f"file {ci_file_id}: {e}"
|
||||
)
|
||||
|
||||
# Cleanup staged input files
|
||||
for file_mapping in files_to_stage:
|
||||
try:
|
||||
client.delete_file(file_mapping["file_id"])
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to delete Code Interpreter staged "
|
||||
f"file {file_mapping['file_id']}: {e}"
|
||||
)
|
||||
|
||||
# Emit file_ids once files are processed
|
||||
if generated_file_ids:
|
||||
for event in client.execute_streaming(
|
||||
code=code,
|
||||
timeout_ms=CODE_INTERPRETER_DEFAULT_TIMEOUT_MS,
|
||||
files=files_to_stage or None,
|
||||
):
|
||||
if isinstance(event, StreamOutputEvent):
|
||||
if event.stream == "stdout":
|
||||
stdout_parts.append(event.data)
|
||||
else:
|
||||
stderr_parts.append(event.data)
|
||||
# Emit incremental delta to frontend
|
||||
self.emitter.emit(
|
||||
Packet(
|
||||
placement=placement,
|
||||
obj=PythonToolDelta(file_ids=generated_file_ids),
|
||||
obj=PythonToolDelta(
|
||||
stdout=event.data if event.stream == "stdout" else "",
|
||||
stderr=event.data if event.stream == "stderr" else "",
|
||||
),
|
||||
)
|
||||
)
|
||||
elif isinstance(event, StreamResultEvent):
|
||||
result_event = event
|
||||
elif isinstance(event, StreamErrorEvent):
|
||||
raise RuntimeError(f"Code interpreter error: {event.message}")
|
||||
|
||||
# Build result
|
||||
result = LlmPythonExecutionResult(
|
||||
stdout=truncated_stdout,
|
||||
stderr=truncated_stderr,
|
||||
exit_code=result_event.exit_code,
|
||||
timed_out=result_event.timed_out,
|
||||
generated_files=generated_files,
|
||||
error=(None if result_event.exit_code == 0 else truncated_stderr),
|
||||
if result_event is None:
|
||||
raise RuntimeError(
|
||||
"Code interpreter stream ended without a result event"
|
||||
)
|
||||
|
||||
# Serialize result for LLM
|
||||
adapter = TypeAdapter(LlmPythonExecutionResult)
|
||||
llm_response = adapter.dump_json(result).decode()
|
||||
full_stdout = "".join(stdout_parts)
|
||||
full_stderr = "".join(stderr_parts)
|
||||
|
||||
return ToolResponse(
|
||||
rich_response=PythonToolRichResponse(
|
||||
generated_files=generated_files,
|
||||
),
|
||||
llm_facing_response=llm_response,
|
||||
)
|
||||
# Truncate output for LLM consumption
|
||||
truncated_stdout = _truncate_output(
|
||||
full_stdout, CODE_INTERPRETER_MAX_OUTPUT_LENGTH, "stdout"
|
||||
)
|
||||
truncated_stderr = _truncate_output(
|
||||
full_stderr, CODE_INTERPRETER_MAX_OUTPUT_LENGTH, "stderr"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Python execution failed: {e}")
|
||||
error_msg = str(e)
|
||||
# Handle generated files
|
||||
generated_files: list[PythonExecutionFile] = []
|
||||
generated_file_ids: list[str] = []
|
||||
file_ids_to_cleanup: list[str] = []
|
||||
file_store = get_default_file_store()
|
||||
|
||||
# Emit error delta
|
||||
for workspace_file in result_event.files:
|
||||
if workspace_file.kind != "file" or not workspace_file.file_id:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Download file from Code Interpreter
|
||||
file_content = client.download_file(workspace_file.file_id)
|
||||
|
||||
# Determine MIME type from file extension
|
||||
filename = workspace_file.path.split("/")[-1]
|
||||
mime_type, _ = mimetypes.guess_type(filename)
|
||||
# Default to binary if we can't determine the type
|
||||
mime_type = mime_type or "application/octet-stream"
|
||||
|
||||
# Save to Onyx file store
|
||||
onyx_file_id = file_store.save_file(
|
||||
content=BytesIO(file_content),
|
||||
display_name=filename,
|
||||
file_origin=FileOrigin.CHAT_UPLOAD,
|
||||
file_type=mime_type,
|
||||
)
|
||||
|
||||
generated_files.append(
|
||||
PythonExecutionFile(
|
||||
filename=filename,
|
||||
file_link=build_full_frontend_file_url(onyx_file_id),
|
||||
)
|
||||
)
|
||||
generated_file_ids.append(onyx_file_id)
|
||||
|
||||
# Mark for cleanup
|
||||
file_ids_to_cleanup.append(workspace_file.file_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to handle generated file {workspace_file.path}: {e}"
|
||||
)
|
||||
|
||||
# Cleanup Code Interpreter files (generated files)
|
||||
for ci_file_id in file_ids_to_cleanup:
|
||||
try:
|
||||
client.delete_file(ci_file_id)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to delete Code Interpreter generated file {ci_file_id}: {e}"
|
||||
)
|
||||
|
||||
# Cleanup staged input files
|
||||
for file_mapping in files_to_stage:
|
||||
try:
|
||||
client.delete_file(file_mapping["file_id"])
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to delete Code Interpreter staged file {file_mapping['file_id']}: {e}"
|
||||
)
|
||||
|
||||
# Emit file_ids once files are processed
|
||||
if generated_file_ids:
|
||||
self.emitter.emit(
|
||||
Packet(
|
||||
placement=placement,
|
||||
obj=PythonToolDelta(
|
||||
stdout="",
|
||||
stderr=error_msg,
|
||||
file_ids=[],
|
||||
),
|
||||
obj=PythonToolDelta(file_ids=generated_file_ids),
|
||||
)
|
||||
)
|
||||
|
||||
# Return error result
|
||||
result = LlmPythonExecutionResult(
|
||||
stdout="",
|
||||
stderr=error_msg,
|
||||
exit_code=-1,
|
||||
timed_out=False,
|
||||
generated_files=[],
|
||||
error=error_msg,
|
||||
)
|
||||
# Build result
|
||||
result = LlmPythonExecutionResult(
|
||||
stdout=truncated_stdout,
|
||||
stderr=truncated_stderr,
|
||||
exit_code=result_event.exit_code,
|
||||
timed_out=result_event.timed_out,
|
||||
generated_files=generated_files,
|
||||
error=None if result_event.exit_code == 0 else truncated_stderr,
|
||||
)
|
||||
|
||||
adapter = TypeAdapter(LlmPythonExecutionResult)
|
||||
llm_response = adapter.dump_json(result).decode()
|
||||
# Serialize result for LLM
|
||||
adapter = TypeAdapter(LlmPythonExecutionResult)
|
||||
llm_response = adapter.dump_json(result).decode()
|
||||
|
||||
return ToolResponse(
|
||||
rich_response=None,
|
||||
llm_facing_response=llm_response,
|
||||
return ToolResponse(
|
||||
rich_response=PythonToolRichResponse(
|
||||
generated_files=generated_files,
|
||||
),
|
||||
llm_facing_response=llm_response,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Python execution failed: {e}")
|
||||
error_msg = str(e)
|
||||
|
||||
# Emit error delta
|
||||
self.emitter.emit(
|
||||
Packet(
|
||||
placement=placement,
|
||||
obj=PythonToolDelta(
|
||||
stdout="",
|
||||
stderr=error_msg,
|
||||
file_ids=[],
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Return error result
|
||||
result = LlmPythonExecutionResult(
|
||||
stdout="",
|
||||
stderr=error_msg,
|
||||
exit_code=-1,
|
||||
timed_out=False,
|
||||
generated_files=[],
|
||||
error=error_msg,
|
||||
)
|
||||
|
||||
adapter = TypeAdapter(LlmPythonExecutionResult)
|
||||
llm_response = adapter.dump_json(result).decode()
|
||||
|
||||
return ToolResponse(
|
||||
rich_response=None,
|
||||
llm_facing_response=llm_response,
|
||||
)
|
||||
|
||||
@@ -596,7 +596,7 @@ mypy-extensions==1.0.0
|
||||
# typing-inspect
|
||||
nest-asyncio==1.6.0
|
||||
# via onyx
|
||||
nltk==3.9.3
|
||||
nltk==3.9.1
|
||||
# via unstructured
|
||||
numpy==2.4.1
|
||||
# via
|
||||
|
||||
@@ -16,6 +16,10 @@ def monitor_process(process_name: str, process: subprocess.Popen) -> None:
|
||||
|
||||
|
||||
def run_jobs() -> None:
|
||||
# Check if we should use lightweight mode, defaults to True, change to False to use separate background workers
|
||||
use_lightweight = True
|
||||
|
||||
# command setup
|
||||
cmd_worker_primary = [
|
||||
"celery",
|
||||
"-A",
|
||||
@@ -70,48 +74,6 @@ def run_jobs() -> None:
|
||||
"--queues=connector_doc_fetching",
|
||||
]
|
||||
|
||||
cmd_worker_heavy = [
|
||||
"celery",
|
||||
"-A",
|
||||
"onyx.background.celery.versioned_apps.heavy",
|
||||
"worker",
|
||||
"--pool=threads",
|
||||
"--concurrency=4",
|
||||
"--prefetch-multiplier=1",
|
||||
"--loglevel=INFO",
|
||||
"--hostname=heavy@%n",
|
||||
"-Q",
|
||||
"connector_pruning,connector_doc_permissions_sync,connector_external_group_sync,csv_generation,sandbox",
|
||||
]
|
||||
|
||||
cmd_worker_monitoring = [
|
||||
"celery",
|
||||
"-A",
|
||||
"onyx.background.celery.versioned_apps.monitoring",
|
||||
"worker",
|
||||
"--pool=threads",
|
||||
"--concurrency=1",
|
||||
"--prefetch-multiplier=1",
|
||||
"--loglevel=INFO",
|
||||
"--hostname=monitoring@%n",
|
||||
"-Q",
|
||||
"monitoring",
|
||||
]
|
||||
|
||||
cmd_worker_user_file_processing = [
|
||||
"celery",
|
||||
"-A",
|
||||
"onyx.background.celery.versioned_apps.user_file_processing",
|
||||
"worker",
|
||||
"--pool=threads",
|
||||
"--concurrency=2",
|
||||
"--prefetch-multiplier=1",
|
||||
"--loglevel=INFO",
|
||||
"--hostname=user_file_processing@%n",
|
||||
"-Q",
|
||||
"user_file_processing,user_file_project_sync,user_file_delete",
|
||||
]
|
||||
|
||||
cmd_beat = [
|
||||
"celery",
|
||||
"-A",
|
||||
@@ -120,31 +82,144 @@ def run_jobs() -> None:
|
||||
"--loglevel=INFO",
|
||||
]
|
||||
|
||||
all_workers = [
|
||||
("PRIMARY", cmd_worker_primary),
|
||||
("LIGHT", cmd_worker_light),
|
||||
("DOCPROCESSING", cmd_worker_docprocessing),
|
||||
("DOCFETCHING", cmd_worker_docfetching),
|
||||
("HEAVY", cmd_worker_heavy),
|
||||
("MONITORING", cmd_worker_monitoring),
|
||||
("USER_FILE_PROCESSING", cmd_worker_user_file_processing),
|
||||
("BEAT", cmd_beat),
|
||||
]
|
||||
# Prepare background worker commands based on mode
|
||||
if use_lightweight:
|
||||
print("Starting workers in LIGHTWEIGHT mode (single background worker)")
|
||||
cmd_worker_background = [
|
||||
"celery",
|
||||
"-A",
|
||||
"onyx.background.celery.versioned_apps.background",
|
||||
"worker",
|
||||
"--pool=threads",
|
||||
"--concurrency=6",
|
||||
"--prefetch-multiplier=1",
|
||||
"--loglevel=INFO",
|
||||
"--hostname=background@%n",
|
||||
"-Q",
|
||||
"connector_pruning,connector_doc_permissions_sync,connector_external_group_sync,csv_generation,monitoring,user_file_processing,user_file_project_sync,user_file_delete,opensearch_migration",
|
||||
]
|
||||
background_workers = [("BACKGROUND", cmd_worker_background)]
|
||||
else:
|
||||
print("Starting workers in STANDARD mode (separate background workers)")
|
||||
cmd_worker_heavy = [
|
||||
"celery",
|
||||
"-A",
|
||||
"onyx.background.celery.versioned_apps.heavy",
|
||||
"worker",
|
||||
"--pool=threads",
|
||||
"--concurrency=4",
|
||||
"--prefetch-multiplier=1",
|
||||
"--loglevel=INFO",
|
||||
"--hostname=heavy@%n",
|
||||
"-Q",
|
||||
"connector_pruning,sandbox",
|
||||
]
|
||||
cmd_worker_monitoring = [
|
||||
"celery",
|
||||
"-A",
|
||||
"onyx.background.celery.versioned_apps.monitoring",
|
||||
"worker",
|
||||
"--pool=threads",
|
||||
"--concurrency=1",
|
||||
"--prefetch-multiplier=1",
|
||||
"--loglevel=INFO",
|
||||
"--hostname=monitoring@%n",
|
||||
"-Q",
|
||||
"monitoring",
|
||||
]
|
||||
cmd_worker_user_file_processing = [
|
||||
"celery",
|
||||
"-A",
|
||||
"onyx.background.celery.versioned_apps.user_file_processing",
|
||||
"worker",
|
||||
"--pool=threads",
|
||||
"--concurrency=2",
|
||||
"--prefetch-multiplier=1",
|
||||
"--loglevel=INFO",
|
||||
"--hostname=user_file_processing@%n",
|
||||
"-Q",
|
||||
"user_file_processing,user_file_project_sync,connector_doc_permissions_sync,connector_external_group_sync,csv_generation,user_file_delete",
|
||||
]
|
||||
background_workers = [
|
||||
("HEAVY", cmd_worker_heavy),
|
||||
("MONITORING", cmd_worker_monitoring),
|
||||
("USER_FILE_PROCESSING", cmd_worker_user_file_processing),
|
||||
]
|
||||
|
||||
processes = []
|
||||
for name, cmd in all_workers:
|
||||
# spawn processes
|
||||
worker_primary_process = subprocess.Popen(
|
||||
cmd_worker_primary, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
||||
)
|
||||
|
||||
worker_light_process = subprocess.Popen(
|
||||
cmd_worker_light, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
||||
)
|
||||
|
||||
worker_docprocessing_process = subprocess.Popen(
|
||||
cmd_worker_docprocessing,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
worker_docfetching_process = subprocess.Popen(
|
||||
cmd_worker_docfetching,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
beat_process = subprocess.Popen(
|
||||
cmd_beat, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
||||
)
|
||||
|
||||
# Spawn background worker processes based on mode
|
||||
background_processes = []
|
||||
for name, cmd in background_workers:
|
||||
process = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
||||
)
|
||||
processes.append((name, process))
|
||||
background_processes.append((name, process))
|
||||
|
||||
threads = []
|
||||
for name, process in processes:
|
||||
# monitor threads
|
||||
worker_primary_thread = threading.Thread(
|
||||
target=monitor_process, args=("PRIMARY", worker_primary_process)
|
||||
)
|
||||
worker_light_thread = threading.Thread(
|
||||
target=monitor_process, args=("LIGHT", worker_light_process)
|
||||
)
|
||||
worker_docprocessing_thread = threading.Thread(
|
||||
target=monitor_process, args=("DOCPROCESSING", worker_docprocessing_process)
|
||||
)
|
||||
worker_docfetching_thread = threading.Thread(
|
||||
target=monitor_process, args=("DOCFETCHING", worker_docfetching_process)
|
||||
)
|
||||
beat_thread = threading.Thread(target=monitor_process, args=("BEAT", beat_process))
|
||||
|
||||
# Create monitor threads for background workers
|
||||
background_threads = []
|
||||
for name, process in background_processes:
|
||||
thread = threading.Thread(target=monitor_process, args=(name, process))
|
||||
threads.append(thread)
|
||||
background_threads.append(thread)
|
||||
|
||||
# Start all threads
|
||||
worker_primary_thread.start()
|
||||
worker_light_thread.start()
|
||||
worker_docprocessing_thread.start()
|
||||
worker_docfetching_thread.start()
|
||||
beat_thread.start()
|
||||
|
||||
for thread in background_threads:
|
||||
thread.start()
|
||||
|
||||
for thread in threads:
|
||||
# Wait for all threads
|
||||
worker_primary_thread.join()
|
||||
worker_light_thread.join()
|
||||
worker_docprocessing_thread.join()
|
||||
worker_docfetching_thread.join()
|
||||
beat_thread.join()
|
||||
|
||||
for thread in background_threads:
|
||||
thread.join()
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
#!/bin/sh
|
||||
# Entrypoint script for supervisord
|
||||
# Entrypoint script for supervisord that sets environment variables
|
||||
# for controlling which celery workers to start
|
||||
|
||||
# Default to lightweight mode if not set
|
||||
if [ -z "$USE_LIGHTWEIGHT_BACKGROUND_WORKER" ]; then
|
||||
export USE_LIGHTWEIGHT_BACKGROUND_WORKER="true"
|
||||
fi
|
||||
|
||||
# Set the complementary variable for supervisord
|
||||
# because it doesn't support %(not ENV_USE_LIGHTWEIGHT_BACKGROUND_WORKER) syntax
|
||||
if [ "$USE_LIGHTWEIGHT_BACKGROUND_WORKER" = "true" ]; then
|
||||
export USE_SEPARATE_BACKGROUND_WORKERS="false"
|
||||
else
|
||||
export USE_SEPARATE_BACKGROUND_WORKERS="true"
|
||||
fi
|
||||
|
||||
echo "Worker mode configuration:"
|
||||
echo " USE_LIGHTWEIGHT_BACKGROUND_WORKER=$USE_LIGHTWEIGHT_BACKGROUND_WORKER"
|
||||
echo " USE_SEPARATE_BACKGROUND_WORKERS=$USE_SEPARATE_BACKGROUND_WORKERS"
|
||||
|
||||
# Launch supervisord with environment variables available
|
||||
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
@@ -39,6 +39,7 @@ autorestart=true
|
||||
startsecs=10
|
||||
stopasgroup=true
|
||||
|
||||
# Standard mode: Light worker for fast operations
|
||||
# NOTE: only allowing configuration here and not in the other celery workers,
|
||||
# since this is often the bottleneck for "sync" jobs (e.g. document set syncing,
|
||||
# user group syncing, deletion, etc.)
|
||||
@@ -53,7 +54,26 @@ redirect_stderr=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopasgroup=true
|
||||
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
|
||||
|
||||
# Lightweight mode: single consolidated background worker
|
||||
# Used when USE_LIGHTWEIGHT_BACKGROUND_WORKER=true (default)
|
||||
# Consolidates: light, docprocessing, docfetching, heavy, monitoring, user_file_processing
|
||||
[program:celery_worker_background]
|
||||
command=celery -A onyx.background.celery.versioned_apps.background worker
|
||||
--loglevel=INFO
|
||||
--hostname=background@%%n
|
||||
-Q vespa_metadata_sync,connector_deletion,doc_permissions_upsert,checkpoint_cleanup,index_attempt_cleanup,sandbox,docprocessing,connector_doc_fetching,connector_pruning,connector_doc_permissions_sync,connector_external_group_sync,csv_generation,monitoring,user_file_processing,user_file_project_sync,opensearch_migration
|
||||
stdout_logfile=/var/log/celery_worker_background.log
|
||||
stdout_logfile_maxbytes=16MB
|
||||
redirect_stderr=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopasgroup=true
|
||||
autostart=%(ENV_USE_LIGHTWEIGHT_BACKGROUND_WORKER)s
|
||||
|
||||
# Standard mode: separate workers for different background tasks
|
||||
# Used when USE_LIGHTWEIGHT_BACKGROUND_WORKER=false
|
||||
[program:celery_worker_heavy]
|
||||
command=celery -A onyx.background.celery.versioned_apps.heavy worker
|
||||
--loglevel=INFO
|
||||
@@ -65,7 +85,9 @@ redirect_stderr=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopasgroup=true
|
||||
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
|
||||
|
||||
# Standard mode: Document processing worker
|
||||
[program:celery_worker_docprocessing]
|
||||
command=celery -A onyx.background.celery.versioned_apps.docprocessing worker
|
||||
--loglevel=INFO
|
||||
@@ -77,6 +99,7 @@ redirect_stderr=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopasgroup=true
|
||||
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
|
||||
|
||||
[program:celery_worker_user_file_processing]
|
||||
command=celery -A onyx.background.celery.versioned_apps.user_file_processing worker
|
||||
@@ -89,7 +112,9 @@ redirect_stderr=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopasgroup=true
|
||||
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
|
||||
|
||||
# Standard mode: Document fetching worker
|
||||
[program:celery_worker_docfetching]
|
||||
command=celery -A onyx.background.celery.versioned_apps.docfetching worker
|
||||
--loglevel=INFO
|
||||
@@ -101,6 +126,7 @@ redirect_stderr=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopasgroup=true
|
||||
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
|
||||
|
||||
[program:celery_worker_monitoring]
|
||||
command=celery -A onyx.background.celery.versioned_apps.monitoring worker
|
||||
@@ -113,6 +139,7 @@ redirect_stderr=true
|
||||
autorestart=true
|
||||
startsecs=10
|
||||
stopasgroup=true
|
||||
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
|
||||
|
||||
|
||||
# Job scheduler for periodic tasks
|
||||
@@ -170,6 +197,7 @@ command=tail -qF
|
||||
/var/log/celery_beat.log
|
||||
/var/log/celery_worker_primary.log
|
||||
/var/log/celery_worker_light.log
|
||||
/var/log/celery_worker_background.log
|
||||
/var/log/celery_worker_heavy.log
|
||||
/var/log/celery_worker_docprocessing.log
|
||||
/var/log/celery_worker_monitoring.log
|
||||
|
||||
@@ -114,8 +114,8 @@ def test_create_duplicate_config_fails(
|
||||
headers=admin_user.headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "already exists" in response.json()["detail"]
|
||||
assert response.status_code == 409
|
||||
assert "already exists" in response.json()["message"]
|
||||
|
||||
|
||||
def test_get_all_configs(
|
||||
@@ -292,7 +292,7 @@ def test_update_config_source_provider_not_found(
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "not found" in response.json()["detail"]
|
||||
assert "not found" in response.json()["message"]
|
||||
|
||||
|
||||
def test_delete_config(
|
||||
@@ -468,7 +468,7 @@ def test_create_config_missing_credentials(
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "No provider or source llm provided" in response.json()["detail"]
|
||||
assert "No provider or source llm provided" in response.json()["message"]
|
||||
|
||||
|
||||
def test_create_config_source_provider_not_found(
|
||||
@@ -488,4 +488,4 @@ def test_create_config_source_provider_not_found(
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "not found" in response.json()["detail"]
|
||||
assert "not found" in response.json()["message"]
|
||||
|
||||
@@ -300,7 +300,7 @@ def test_update_contextual_rag_nonexistent_provider(
|
||||
headers=admin_user.headers,
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "Provider nonexistent-provider not found" in response.json()["detail"]
|
||||
assert "Provider nonexistent-provider not found" in response.json()["message"]
|
||||
|
||||
|
||||
def test_update_contextual_rag_nonexistent_model(
|
||||
@@ -322,7 +322,7 @@ def test_update_contextual_rag_nonexistent_model(
|
||||
assert response.status_code == 400
|
||||
assert (
|
||||
f"Model nonexistent-model not found in provider {llm_provider.name}"
|
||||
in response.json()["detail"]
|
||||
in response.json()["message"]
|
||||
)
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ def test_update_contextual_rag_missing_provider_name(
|
||||
headers=admin_user.headers,
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "Provider name and model name are required" in response.json()["detail"]
|
||||
assert "Provider name and model name are required" in response.json()["message"]
|
||||
|
||||
|
||||
def test_update_contextual_rag_missing_model_name(
|
||||
@@ -362,7 +362,7 @@ def test_update_contextual_rag_missing_model_name(
|
||||
headers=admin_user.headers,
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "Provider name and model name are required" in response.json()["detail"]
|
||||
assert "Provider name and model name are required" in response.json()["message"]
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Set new search settings is temporarily disabled.")
|
||||
|
||||
@@ -104,102 +104,3 @@ def test_format_slack_message_ampersand_not_double_escaped() -> None:
|
||||
|
||||
assert "&" in formatted
|
||||
assert """ not in formatted
|
||||
|
||||
|
||||
# -- Table rendering tests --
|
||||
|
||||
|
||||
def test_table_renders_as_vertical_cards() -> None:
|
||||
message = (
|
||||
"| Feature | Status | Owner |\n"
|
||||
"|---------|--------|-------|\n"
|
||||
"| Auth | Done | Alice |\n"
|
||||
"| Search | In Progress | Bob |\n"
|
||||
)
|
||||
|
||||
formatted = format_slack_message(message)
|
||||
|
||||
assert "*Auth*\n • Status: Done\n • Owner: Alice" in formatted
|
||||
assert "*Search*\n • Status: In Progress\n • Owner: Bob" in formatted
|
||||
# Cards separated by blank line
|
||||
assert "Owner: Alice\n\n*Search*" in formatted
|
||||
# No raw pipe-and-dash table syntax
|
||||
assert "---|" not in formatted
|
||||
|
||||
|
||||
def test_table_single_column() -> None:
|
||||
message = "| Name |\n|------|\n| Alice |\n| Bob |\n"
|
||||
|
||||
formatted = format_slack_message(message)
|
||||
|
||||
assert "*Alice*" in formatted
|
||||
assert "*Bob*" in formatted
|
||||
|
||||
|
||||
def test_table_embedded_in_text() -> None:
|
||||
message = (
|
||||
"Here are the results:\n\n"
|
||||
"| Item | Count |\n"
|
||||
"|------|-------|\n"
|
||||
"| Apples | 5 |\n"
|
||||
"\n"
|
||||
"That's all."
|
||||
)
|
||||
|
||||
formatted = format_slack_message(message)
|
||||
|
||||
assert "Here are the results:" in formatted
|
||||
assert "*Apples*\n • Count: 5" in formatted
|
||||
assert "That's all." in formatted
|
||||
|
||||
|
||||
def test_table_with_formatted_cells() -> None:
|
||||
message = (
|
||||
"| Name | Link |\n"
|
||||
"|------|------|\n"
|
||||
"| **Alice** | [profile](https://example.com) |\n"
|
||||
)
|
||||
|
||||
formatted = format_slack_message(message)
|
||||
|
||||
# Bold cell should not double-wrap: *Alice* not **Alice**
|
||||
assert "*Alice*" in formatted
|
||||
assert "**Alice**" not in formatted
|
||||
assert "<https://example.com|profile>" in formatted
|
||||
|
||||
|
||||
def test_table_with_alignment_specifiers() -> None:
|
||||
message = (
|
||||
"| Left | Center | Right |\n" "|:-----|:------:|------:|\n" "| a | b | c |\n"
|
||||
)
|
||||
|
||||
formatted = format_slack_message(message)
|
||||
|
||||
assert "*a*\n • Center: b\n • Right: c" in formatted
|
||||
|
||||
|
||||
def test_two_tables_in_same_message_use_independent_headers() -> None:
|
||||
message = (
|
||||
"| A | B |\n"
|
||||
"|---|---|\n"
|
||||
"| 1 | 2 |\n"
|
||||
"\n"
|
||||
"| X | Y | Z |\n"
|
||||
"|---|---|---|\n"
|
||||
"| p | q | r |\n"
|
||||
)
|
||||
|
||||
formatted = format_slack_message(message)
|
||||
|
||||
assert "*1*\n • B: 2" in formatted
|
||||
assert "*p*\n • Y: q\n • Z: r" in formatted
|
||||
|
||||
|
||||
def test_table_empty_first_column_no_bare_asterisks() -> None:
|
||||
message = "| Name | Status |\n" "|------|--------|\n" "| | Done |\n"
|
||||
|
||||
formatted = format_slack_message(message)
|
||||
|
||||
# Empty title should not produce "**" (bare asterisks)
|
||||
assert "**" not in formatted
|
||||
assert " • Status: Done" in formatted
|
||||
|
||||
@@ -87,8 +87,7 @@ def test_python_tool_available_when_health_check_passes(
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.health.return_value = True
|
||||
mock_client_cls.return_value.__enter__ = MagicMock(return_value=mock_client)
|
||||
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
db_session = MagicMock(spec=Session)
|
||||
assert PythonTool.is_available(db_session) is True
|
||||
@@ -110,8 +109,7 @@ def test_python_tool_unavailable_when_health_check_fails(
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.health.return_value = False
|
||||
mock_client_cls.return_value.__enter__ = MagicMock(return_value=mock_client)
|
||||
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
db_session = MagicMock(spec=Session)
|
||||
assert PythonTool.is_available(db_session) is False
|
||||
|
||||
@@ -138,6 +138,7 @@ services:
|
||||
- indexing_model_server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
|
||||
- ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=true
|
||||
- MULTI_TENANT=true
|
||||
- LOG_LEVEL=DEBUG
|
||||
|
||||
@@ -52,6 +52,7 @@ services:
|
||||
- indexing_model_server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
|
||||
- AUTH_TYPE=${AUTH_TYPE:-oidc}
|
||||
- POSTGRES_HOST=relational_db
|
||||
- VESPA_HOST=index
|
||||
|
||||
@@ -65,6 +65,7 @@ services:
|
||||
- indexing_model_server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
|
||||
- AUTH_TYPE=${AUTH_TYPE:-oidc}
|
||||
- POSTGRES_HOST=relational_db
|
||||
- VESPA_HOST=index
|
||||
|
||||
@@ -70,6 +70,7 @@ services:
|
||||
- indexing_model_server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
|
||||
- AUTH_TYPE=${AUTH_TYPE:-oidc}
|
||||
- POSTGRES_HOST=relational_db
|
||||
- VESPA_HOST=index
|
||||
|
||||
@@ -58,6 +58,7 @@ services:
|
||||
env_file:
|
||||
- .env_eval
|
||||
environment:
|
||||
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
|
||||
- AUTH_TYPE=disabled
|
||||
- POSTGRES_HOST=relational_db
|
||||
- VESPA_HOST=index
|
||||
|
||||
@@ -146,6 +146,7 @@ services:
|
||||
- indexing_model_server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
|
||||
- FILE_STORE_BACKEND=${FILE_STORE_BACKEND:-s3}
|
||||
- POSTGRES_HOST=${POSTGRES_HOST:-relational_db}
|
||||
- VESPA_HOST=${VESPA_HOST:-index}
|
||||
|
||||
@@ -14,32 +14,30 @@ Built with [Tauri](https://tauri.app) for minimal bundle size (~10MB vs Electron
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
| -------- | ---------------- |
|
||||
| `⌘ N` | New Chat |
|
||||
| `⌘ ⇧ N` | New Window |
|
||||
| `⌘ R` | Reload |
|
||||
| `⌘ [` | Go Back |
|
||||
| `⌘ ]` | Go Forward |
|
||||
| `⌘ ,` | Open Config File |
|
||||
| `⌘ W` | Close Window |
|
||||
| `⌘ Q` | Quit |
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `⌘ N` | New Chat |
|
||||
| `⌘ ⇧ N` | New Window |
|
||||
| `⌘ R` | Reload |
|
||||
| `⌘ [` | Go Back |
|
||||
| `⌘ ]` | Go Forward |
|
||||
| `⌘ ,` | Open Config File |
|
||||
| `⌘ W` | Close Window |
|
||||
| `⌘ Q` | Quit |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Rust** (latest stable)
|
||||
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
```
|
||||
|
||||
2. **Node.js** (18+)
|
||||
|
||||
```bash
|
||||
# Using homebrew
|
||||
brew install node
|
||||
|
||||
|
||||
# Or using nvm
|
||||
nvm install 18
|
||||
```
|
||||
@@ -57,21 +55,16 @@ npm install
|
||||
|
||||
# Run in development mode
|
||||
npm run dev
|
||||
|
||||
# Run in debug mode
|
||||
npm run debug
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Build for current architecture
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Build Universal Binary (Intel + Apple Silicon)
|
||||
|
||||
```bash
|
||||
# First, add the targets
|
||||
rustup target add x86_64-apple-darwin
|
||||
@@ -110,7 +103,6 @@ Before building, add your app icons to `src-tauri/icons/`:
|
||||
- `icon.ico` (Windows, optional)
|
||||
|
||||
You can generate these from a 1024x1024 source image using:
|
||||
|
||||
```bash
|
||||
# Using tauri's icon generator
|
||||
npm run tauri icon path/to/your-icon.png
|
||||
@@ -123,7 +115,6 @@ npm run tauri icon path/to/your-icon.png
|
||||
The app defaults to `https://cloud.onyx.app` but supports any Onyx instance.
|
||||
|
||||
**Config file location:**
|
||||
|
||||
- macOS: `~/Library/Application Support/app.onyx.desktop/config.json`
|
||||
- Linux: `~/.config/app.onyx.desktop/config.json`
|
||||
- Windows: `%APPDATA%/app.onyx.desktop/config.json`
|
||||
@@ -144,7 +135,6 @@ The app defaults to `https://cloud.onyx.app` but supports any Onyx instance.
|
||||
4. Restart the app
|
||||
|
||||
**Quick edit via terminal:**
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
open -t ~/Library/Application\ Support/app.onyx.desktop/config.json
|
||||
@@ -156,7 +146,6 @@ code ~/Library/Application\ Support/app.onyx.desktop/config.json
|
||||
### Change the default URL in build
|
||||
|
||||
Edit `src-tauri/tauri.conf.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"app": {
|
||||
@@ -176,7 +165,6 @@ Edit `src-tauri/src/main.rs` in the `setup_shortcuts` function.
|
||||
### Window appearance
|
||||
|
||||
Modify the window configuration in `src-tauri/tauri.conf.json`:
|
||||
|
||||
- `titleBarStyle`: `"Overlay"` (macOS native) or `"Visible"`
|
||||
- `decorations`: Window chrome
|
||||
- `transparent`: For custom backgrounds
|
||||
@@ -184,20 +172,16 @@ Modify the window configuration in `src-tauri/tauri.conf.json`:
|
||||
## Troubleshooting
|
||||
|
||||
### "Unable to resolve host"
|
||||
|
||||
Make sure you have an internet connection. The app loads content from `cloud.onyx.app`.
|
||||
|
||||
### Build fails on M1/M2 Mac
|
||||
|
||||
```bash
|
||||
# Ensure you have the right target
|
||||
rustup target add aarch64-apple-darwin
|
||||
```
|
||||
|
||||
### Code signing for distribution
|
||||
|
||||
For distributing outside the App Store, you'll need to:
|
||||
|
||||
1. Get an Apple Developer certificate
|
||||
2. Sign the app: `codesign --deep --force --sign "Developer ID" target/release/bundle/macos/Onyx.app`
|
||||
3. Notarize with Apple
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"description": "Lightweight desktop app for Onyx Cloud",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"debug": "tauri dev -- -- --debug",
|
||||
"build": "tauri build",
|
||||
"build:dmg": "tauri build --target universal-apple-darwin",
|
||||
"build:linux": "tauri build --bundles deb,rpm"
|
||||
|
||||
@@ -23,4 +23,3 @@ url = "2.5"
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
devtools = ["tauri/devtools"]
|
||||
|
||||
@@ -6,9 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::{Mutex, RwLock};
|
||||
use std::io::Write as IoWrite;
|
||||
use std::time::SystemTime;
|
||||
use std::sync::RwLock;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::time::Duration;
|
||||
use tauri::image::Image;
|
||||
@@ -232,63 +230,6 @@ const MENU_KEY_HANDLER_SCRIPT: &str = r#"
|
||||
})();
|
||||
"#;
|
||||
|
||||
const CONSOLE_CAPTURE_SCRIPT: &str = r#"
|
||||
(() => {
|
||||
if (window.__ONYX_CONSOLE_CAPTURE__) return;
|
||||
window.__ONYX_CONSOLE_CAPTURE__ = true;
|
||||
|
||||
const levels = ['log', 'warn', 'error', 'info', 'debug'];
|
||||
const originals = {};
|
||||
|
||||
levels.forEach(level => {
|
||||
originals[level] = console[level];
|
||||
console[level] = function(...args) {
|
||||
originals[level].apply(console, args);
|
||||
try {
|
||||
const invoke =
|
||||
window.__TAURI__?.core?.invoke || window.__TAURI_INTERNALS__?.invoke;
|
||||
if (typeof invoke === 'function') {
|
||||
const message = args.map(a => {
|
||||
try { return typeof a === 'string' ? a : JSON.stringify(a); }
|
||||
catch { return String(a); }
|
||||
}).join(' ');
|
||||
invoke('log_from_frontend', { level, message });
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
});
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
try {
|
||||
const invoke =
|
||||
window.__TAURI__?.core?.invoke || window.__TAURI_INTERNALS__?.invoke;
|
||||
if (typeof invoke === 'function') {
|
||||
invoke('log_from_frontend', {
|
||||
level: 'error',
|
||||
message: `[uncaught] ${event.message} at ${event.filename}:${event.lineno}:${event.colno}`
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
try {
|
||||
const invoke =
|
||||
window.__TAURI__?.core?.invoke || window.__TAURI_INTERNALS__?.invoke;
|
||||
if (typeof invoke === 'function') {
|
||||
invoke('log_from_frontend', {
|
||||
level: 'error',
|
||||
message: `[unhandled rejection] ${event.reason}`
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
})();
|
||||
"#;
|
||||
|
||||
const MENU_TOGGLE_DEVTOOLS_ID: &str = "toggle_devtools";
|
||||
const MENU_OPEN_DEBUG_LOG_ID: &str = "open_debug_log";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppConfig {
|
||||
pub server_url: String,
|
||||
@@ -370,87 +311,12 @@ fn save_config(config: &AppConfig) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Debug Mode
|
||||
// ============================================================================
|
||||
|
||||
fn is_debug_mode() -> bool {
|
||||
std::env::args().any(|arg| arg == "--debug") || std::env::var("ONYX_DEBUG").is_ok()
|
||||
}
|
||||
|
||||
fn get_debug_log_path() -> Option<PathBuf> {
|
||||
get_config_dir().map(|dir| dir.join("frontend_debug.log"))
|
||||
}
|
||||
|
||||
fn init_debug_log_file() -> Option<fs::File> {
|
||||
let log_path = get_debug_log_path()?;
|
||||
if let Some(parent) = log_path.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_path)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn format_utc_timestamp() -> String {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
let total_secs = now.as_secs();
|
||||
let millis = now.subsec_millis();
|
||||
|
||||
let days = total_secs / 86400;
|
||||
let secs_of_day = total_secs % 86400;
|
||||
let hours = secs_of_day / 3600;
|
||||
let mins = (secs_of_day % 3600) / 60;
|
||||
let secs = secs_of_day % 60;
|
||||
|
||||
// Days since Unix epoch -> Y/M/D via civil calendar arithmetic
|
||||
let z = days as i64 + 719468;
|
||||
let era = z / 146097;
|
||||
let doe = z - era * 146097;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
let y = yoe + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = if m <= 2 { y + 1 } else { y };
|
||||
|
||||
format!(
|
||||
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
|
||||
y, m, d, hours, mins, secs, millis
|
||||
)
|
||||
}
|
||||
|
||||
fn inject_console_capture(webview: &Webview) {
|
||||
let _ = webview.eval(CONSOLE_CAPTURE_SCRIPT);
|
||||
}
|
||||
|
||||
fn maybe_open_devtools(app: &AppHandle, window: &tauri::WebviewWindow) {
|
||||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
{
|
||||
let state = app.state::<ConfigState>();
|
||||
if state.debug_mode {
|
||||
window.open_devtools();
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(debug_assertions, feature = "devtools")))]
|
||||
{
|
||||
let _ = (app, window);
|
||||
}
|
||||
}
|
||||
|
||||
// Global config state
|
||||
struct ConfigState {
|
||||
config: RwLock<AppConfig>,
|
||||
config_initialized: RwLock<bool>,
|
||||
app_base_url: RwLock<Option<Url>>,
|
||||
menu_temporarily_visible: RwLock<bool>,
|
||||
debug_mode: bool,
|
||||
debug_log_file: Mutex<Option<fs::File>>,
|
||||
}
|
||||
|
||||
fn focus_main_window(app: &AppHandle) {
|
||||
@@ -506,7 +372,6 @@ fn trigger_new_window(app: &AppHandle) {
|
||||
}
|
||||
|
||||
apply_settings_to_window(&handle, &window);
|
||||
maybe_open_devtools(&handle, &window);
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
});
|
||||
@@ -602,65 +467,10 @@ fn inject_chat_link_intercept(webview: &Webview) {
|
||||
let _ = webview.eval(CHAT_LINK_INTERCEPT_SCRIPT);
|
||||
}
|
||||
|
||||
fn handle_toggle_devtools(app: &AppHandle) {
|
||||
#[cfg(any(debug_assertions, feature = "devtools"))]
|
||||
{
|
||||
let windows: Vec<_> = app.webview_windows().into_values().collect();
|
||||
let any_open = windows.iter().any(|w| w.is_devtools_open());
|
||||
for window in &windows {
|
||||
if any_open {
|
||||
window.close_devtools();
|
||||
} else {
|
||||
window.open_devtools();
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(debug_assertions, feature = "devtools")))]
|
||||
{
|
||||
let _ = app;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_open_debug_log() {
|
||||
let log_path = match get_debug_log_path() {
|
||||
Some(p) => p,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if !log_path.exists() {
|
||||
eprintln!("[ONYX DEBUG] Log file does not exist yet: {:?}", log_path);
|
||||
return;
|
||||
}
|
||||
|
||||
let url_path = log_path.to_string_lossy().replace('\\', "/");
|
||||
let _ = open_in_default_browser(&format!(
|
||||
"file:///{}",
|
||||
url_path.trim_start_matches('/')
|
||||
));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tauri Commands
|
||||
// ============================================================================
|
||||
|
||||
#[tauri::command]
|
||||
fn log_from_frontend(level: String, message: String, state: tauri::State<ConfigState>) {
|
||||
if !state.debug_mode {
|
||||
return;
|
||||
}
|
||||
let timestamp = format_utc_timestamp();
|
||||
let log_line = format!("[{}] [{}] {}", timestamp, level.to_uppercase(), message);
|
||||
|
||||
eprintln!("{}", log_line);
|
||||
|
||||
if let Ok(mut guard) = state.debug_log_file.lock() {
|
||||
if let Some(ref mut file) = *guard {
|
||||
let _ = writeln!(file, "{}", log_line);
|
||||
let _ = file.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current server URL
|
||||
#[tauri::command]
|
||||
fn get_server_url(state: tauri::State<ConfigState>) -> String {
|
||||
@@ -847,7 +657,6 @@ async fn new_window(app: AppHandle, state: tauri::State<'_, ConfigState>) -> Res
|
||||
}
|
||||
|
||||
apply_settings_to_window(&app, &window);
|
||||
maybe_open_devtools(&app, &window);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1127,30 +936,6 @@ fn setup_app_menu(app: &AppHandle) -> tauri::Result<()> {
|
||||
menu.append(&help_menu)?;
|
||||
}
|
||||
|
||||
let state = app.state::<ConfigState>();
|
||||
if state.debug_mode {
|
||||
let toggle_devtools_item = MenuItem::with_id(
|
||||
app,
|
||||
MENU_TOGGLE_DEVTOOLS_ID,
|
||||
"Toggle DevTools",
|
||||
true,
|
||||
Some("F12"),
|
||||
)?;
|
||||
let open_log_item = MenuItem::with_id(
|
||||
app,
|
||||
MENU_OPEN_DEBUG_LOG_ID,
|
||||
"Open Debug Log",
|
||||
true,
|
||||
None::<&str>,
|
||||
)?;
|
||||
|
||||
let debug_menu = SubmenuBuilder::new(app, "Debug")
|
||||
.item(&toggle_devtools_item)
|
||||
.item(&open_log_item)
|
||||
.build()?;
|
||||
menu.append(&debug_menu)?;
|
||||
}
|
||||
|
||||
app.set_menu(menu)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1242,20 +1027,8 @@ fn setup_tray_icon(app: &AppHandle) -> tauri::Result<()> {
|
||||
// ============================================================================
|
||||
|
||||
fn main() {
|
||||
// Load config at startup
|
||||
let (config, config_initialized) = load_config();
|
||||
let debug_mode = is_debug_mode();
|
||||
|
||||
let debug_log_file = if debug_mode {
|
||||
eprintln!("[ONYX DEBUG] Debug mode enabled");
|
||||
if let Some(path) = get_debug_log_path() {
|
||||
eprintln!("[ONYX DEBUG] Frontend logs: {}", path.display());
|
||||
}
|
||||
eprintln!("[ONYX DEBUG] DevTools will open automatically");
|
||||
eprintln!("[ONYX DEBUG] Capturing console.log/warn/error/info/debug from webview");
|
||||
init_debug_log_file()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
@@ -1286,8 +1059,6 @@ fn main() {
|
||||
config_initialized: RwLock::new(config_initialized),
|
||||
app_base_url: RwLock::new(None),
|
||||
menu_temporarily_visible: RwLock::new(false),
|
||||
debug_mode,
|
||||
debug_log_file: Mutex::new(debug_log_file),
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_server_url,
|
||||
@@ -1306,8 +1077,7 @@ fn main() {
|
||||
start_drag_window,
|
||||
toggle_menu_bar,
|
||||
show_menu_bar_temporarily,
|
||||
hide_menu_bar_temporary,
|
||||
log_from_frontend
|
||||
hide_menu_bar_temporary
|
||||
])
|
||||
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||
"open_docs" => open_docs(),
|
||||
@@ -1316,8 +1086,6 @@ fn main() {
|
||||
"open_settings" => open_settings(app),
|
||||
"show_menu_bar" => handle_menu_bar_toggle(app),
|
||||
"hide_window_decorations" => handle_decorations_toggle(app),
|
||||
MENU_TOGGLE_DEVTOOLS_ID => handle_toggle_devtools(app),
|
||||
MENU_OPEN_DEBUG_LOG_ID => handle_open_debug_log(),
|
||||
_ => {}
|
||||
})
|
||||
.setup(move |app| {
|
||||
@@ -1351,7 +1119,6 @@ fn main() {
|
||||
inject_titlebar(window.clone());
|
||||
|
||||
apply_settings_to_window(&app_handle, &window);
|
||||
maybe_open_devtools(&app_handle, &window);
|
||||
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
@@ -1361,14 +1128,6 @@ fn main() {
|
||||
.on_page_load(|webview: &Webview, _payload: &PageLoadPayload| {
|
||||
inject_chat_link_intercept(webview);
|
||||
|
||||
{
|
||||
let app = webview.app_handle();
|
||||
let state = app.state::<ConfigState>();
|
||||
if state.debug_mode {
|
||||
inject_console_capture(webview);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = webview.eval(MENU_KEY_HANDLER_SCRIPT);
|
||||
|
||||
6
uv.lock
generated
6
uv.lock
generated
@@ -4106,7 +4106,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "nltk"
|
||||
version = "3.9.3"
|
||||
version = "3.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
@@ -4114,9 +4114,9 @@ dependencies = [
|
||||
{ name = "regex" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/8f/915e1c12df07c70ed779d18ab83d065718a926e70d3ea33eb0cd66ffb7c0/nltk-3.9.3.tar.gz", hash = "sha256:cb5945d6424a98d694c2b9a0264519fab4363711065a46aa0ae7a2195b92e71f", size = 2923673, upload-time = "2026-02-24T12:05:53.833Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691, upload-time = "2024-08-18T19:48:37.769Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/7e/9af5a710a1236e4772de8dfcc6af942a561327bb9f42b5b4a24d0cf100fd/nltk-3.9.3-py3-none-any.whl", hash = "sha256:60b3db6e9995b3dd976b1f0fa7dec22069b2677e759c28eb69b62ddd44870522", size = 1525385, upload-time = "2026-02-24T12:05:46.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442, upload-time = "2024-08-18T19:48:21.909Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { IconProps } from "@opal/types";
|
||||
|
||||
const SvgColumn = ({ size, ...props }: IconProps) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M6 14H3.33333C2.59695 14 2 13.403 2 12.6667V3.33333C2 2.59695 2.59695 2 3.33333 2H6M6 14V2M6 14H10M6 2H10M10 2H12.6667C13.403 2 14 2.59695 14 3.33333V12.6667C14 13.403 13.403 14 12.6667 14H10M10 2V14"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SvgColumn;
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { IconProps } from "@opal/types";
|
||||
|
||||
const SvgHandle = ({ size = 16, ...props }: IconProps) => (
|
||||
<svg
|
||||
width={Math.round((size * 3) / 17)}
|
||||
height={size}
|
||||
viewBox="0 0 3 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M0.5 0.5V16.5M2.5 0.5V16.5"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SvgHandle;
|
||||
@@ -49,7 +49,6 @@ export { default as SvgClock } from "@opal/icons/clock";
|
||||
export { default as SvgClockHandsSmall } from "@opal/icons/clock-hands-small";
|
||||
export { default as SvgCloud } from "@opal/icons/cloud";
|
||||
export { default as SvgCode } from "@opal/icons/code";
|
||||
export { default as SvgColumn } from "@opal/icons/column";
|
||||
export { default as SvgCopy } from "@opal/icons/copy";
|
||||
export { default as SvgCornerRightUpDot } from "@opal/icons/corner-right-up-dot";
|
||||
export { default as SvgCpu } from "@opal/icons/cpu";
|
||||
@@ -80,7 +79,6 @@ export { default as SvgFolderPartialOpen } from "@opal/icons/folder-partial-open
|
||||
export { default as SvgFolderPlus } from "@opal/icons/folder-plus";
|
||||
export { default as SvgGemini } from "@opal/icons/gemini";
|
||||
export { default as SvgGlobe } from "@opal/icons/globe";
|
||||
export { default as SvgHandle } from "@opal/icons/handle";
|
||||
export { default as SvgHardDrive } from "@opal/icons/hard-drive";
|
||||
export { default as SvgHashSmall } from "@opal/icons/hash-small";
|
||||
export { default as SvgHash } from "@opal/icons/hash";
|
||||
@@ -148,8 +146,6 @@ export { default as SvgSlack } from "@opal/icons/slack";
|
||||
export { default as SvgSlash } from "@opal/icons/slash";
|
||||
export { default as SvgSliders } from "@opal/icons/sliders";
|
||||
export { default as SvgSlidersSmall } from "@opal/icons/sliders-small";
|
||||
export { default as SvgSort } from "@opal/icons/sort";
|
||||
export { default as SvgSortOrder } from "@opal/icons/sort-order";
|
||||
export { default as SvgSparkle } from "@opal/icons/sparkle";
|
||||
export { default as SvgStar } from "@opal/icons/star";
|
||||
export { default as SvgStep1 } from "@opal/icons/step1";
|
||||
@@ -173,7 +169,6 @@ export { default as SvgUploadCloud } from "@opal/icons/upload-cloud";
|
||||
export { default as SvgUser } from "@opal/icons/user";
|
||||
export { default as SvgUserManage } from "@opal/icons/user-manage";
|
||||
export { default as SvgUserPlus } from "@opal/icons/user-plus";
|
||||
export { default as SvgUserSync } from "@opal/icons/user-sync";
|
||||
export { default as SvgUsers } from "@opal/icons/users";
|
||||
export { default as SvgWallet } from "@opal/icons/wallet";
|
||||
export { default as SvgWorkflow } from "@opal/icons/workflow";
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { IconProps } from "@opal/types";
|
||||
|
||||
const SvgSortOrder = ({ size, ...props }: IconProps) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M2.66675 12L7.67009 12.0001M2.66675 8H10.5001M2.66675 4H13.3334"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default SvgSortOrder;
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { IconProps } from "@opal/types";
|
||||
|
||||
const SvgSort = ({ size, ...props }: IconProps) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M2 4.5H10M2 8H7M2 11.5H5"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 5V12M12 12L14 10M12 12L10 10"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default SvgSort;
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { IconProps } from "@opal/types";
|
||||
|
||||
const SvgUserSync = ({ size, ...props }: IconProps) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M1 14C1 13.6667 1 13.3333 1 13C1 11.3431 2.34316 10 4.00002 10H7M11 8.5L9.5 10L14.5 9.99985M13 14L14.5 12.5L9.5 12.5M8.75 4.75C8.75 6.26878 7.51878 7.5 6 7.5C4.48122 7.5 3.25 6.26878 3.25 4.75C3.25 3.23122 4.48122 2 6 2C7.51878 2 8.75 3.23122 8.75 4.75Z"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SvgUserSync;
|
||||
@@ -41,7 +41,7 @@ export default defineConfig({
|
||||
viewport: { width: 1280, height: 720 },
|
||||
storageState: "admin_auth.json",
|
||||
},
|
||||
grepInvert: [/@exclusive/, /@lite/],
|
||||
grepInvert: /@exclusive/,
|
||||
},
|
||||
{
|
||||
// this suite runs independently and serially + slower
|
||||
@@ -55,15 +55,5 @@ export default defineConfig({
|
||||
grep: /@exclusive/,
|
||||
workers: 1,
|
||||
},
|
||||
{
|
||||
// runs against the Onyx Lite stack (DISABLE_VECTOR_DB=true, no Vespa/Redis)
|
||||
name: "lite",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
viewport: { width: 1280, height: 720 },
|
||||
storageState: "admin_auth.json",
|
||||
},
|
||||
grep: /@lite/,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import { SourceCategory, SourceMetadata } from "@/lib/search/interfaces";
|
||||
import { listSourceMetadata } from "@/lib/sources";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import {
|
||||
useCallback,
|
||||
useContext,
|
||||
@@ -254,7 +254,9 @@ export default function Page() {
|
||||
icon={route.icon}
|
||||
title={route.title}
|
||||
rightChildren={
|
||||
<Button href="/admin/indexing/status">See Connectors</Button>
|
||||
<Button href="/admin/indexing/status" primary>
|
||||
See Connectors
|
||||
</Button>
|
||||
}
|
||||
separator
|
||||
/>
|
||||
|
||||
@@ -16,8 +16,9 @@ import {
|
||||
} from "./lib";
|
||||
import { FiEdit2 } from "react-icons/fi";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import { Button } from "@opal/components";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import ConfirmationModalLayout from "@/refresh-components/layouts/ConfirmationModalLayout";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { SvgAlertCircle, SvgTrash } from "@opal/icons";
|
||||
import type { Route } from "next";
|
||||
|
||||
@@ -299,7 +300,7 @@ export function PersonasTable({
|
||||
<div key="edit" className="flex">
|
||||
<div className="mr-auto my-auto">
|
||||
{!persona.builtin_persona && isEditable ? (
|
||||
<Button
|
||||
<OpalButton
|
||||
icon={SvgTrash}
|
||||
prominence="tertiary"
|
||||
onClick={() => openDeleteModal(persona)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Form, Formik } from "formik";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { createApiKey, updateApiKey } from "./lib";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import InputComboBox from "@/refresh-components/inputs/InputComboBox";
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
DISCORD_SERVICE_API_KEY_NAME,
|
||||
} from "@/app/admin/api-key/types";
|
||||
import CreateButton from "@/refresh-components/buttons/CreateButton";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { SvgEdit, SvgKey, SvgRefreshCw } from "@opal/icons";
|
||||
@@ -161,11 +161,11 @@ function Main() {
|
||||
<TableRow key={apiKey.api_key_id}>
|
||||
<TableCell>
|
||||
<Button
|
||||
prominence="internal"
|
||||
internal
|
||||
onClick={() => handleEdit(apiKey)}
|
||||
icon={SvgEdit}
|
||||
leftIcon={SvgEdit}
|
||||
>
|
||||
{apiKey.api_key_name || "null"}
|
||||
{apiKey.api_key_name || <i>null</i>}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-64">
|
||||
@@ -176,8 +176,8 @@ function Main() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
prominence="internal"
|
||||
icon={SvgRefreshCw}
|
||||
internal
|
||||
leftIcon={SvgRefreshCw}
|
||||
onClick={async () => {
|
||||
setKeyIsGenerating(true);
|
||||
const response = await regenerateApiKey(apiKey);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Content } from "@opal/layouts";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import Message from "@/refresh-components/messages/Message";
|
||||
import InfoBlock from "@/refresh-components/messages/InfoBlock";
|
||||
@@ -245,20 +244,25 @@ function SubscriptionCard({
|
||||
to make changes.
|
||||
</Text>
|
||||
) : disabled ? (
|
||||
<OpalButton
|
||||
prominence="secondary"
|
||||
<Button
|
||||
main
|
||||
secondary
|
||||
onClick={handleReconnect}
|
||||
rightIcon={SvgArrowRight}
|
||||
disabled={isReconnecting}
|
||||
>
|
||||
{isReconnecting ? "Connecting..." : "Connect to Stripe"}
|
||||
</OpalButton>
|
||||
</Button>
|
||||
) : (
|
||||
<OpalButton onClick={handleManagePlan} rightIcon={SvgExternalLink}>
|
||||
<Button
|
||||
main
|
||||
primary
|
||||
onClick={handleManagePlan}
|
||||
rightIcon={SvgExternalLink}
|
||||
>
|
||||
Manage Plan
|
||||
</OpalButton>
|
||||
</Button>
|
||||
)}
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button tertiary onClick={onViewPlans} className="billing-text-link">
|
||||
<Text secondaryBody text03>
|
||||
View Plan Details
|
||||
@@ -375,13 +379,9 @@ function SeatsCard({
|
||||
sizePreset="main-content"
|
||||
variant="section"
|
||||
/>
|
||||
<OpalButton
|
||||
prominence="secondary"
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Button main secondary onClick={handleCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</OpalButton>
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
<div className="billing-content-area">
|
||||
@@ -463,14 +463,16 @@ function SeatsCard({
|
||||
No changes to your billing.
|
||||
</Text>
|
||||
)}
|
||||
<OpalButton
|
||||
<Button
|
||||
main
|
||||
primary
|
||||
onClick={handleConfirm}
|
||||
disabled={
|
||||
isSubmitting || newSeatCount === totalSeats || isBelowMinimum
|
||||
}
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Confirm Change"}
|
||||
</OpalButton>
|
||||
</Button>
|
||||
</Section>
|
||||
</Card>
|
||||
);
|
||||
@@ -500,22 +502,19 @@ function SeatsCard({
|
||||
height="auto"
|
||||
width="auto"
|
||||
>
|
||||
<OpalButton
|
||||
prominence="tertiary"
|
||||
href="/admin/users"
|
||||
icon={SvgExternalLink}
|
||||
>
|
||||
<Button main tertiary href="/admin/users" leftIcon={SvgExternalLink}>
|
||||
View Users
|
||||
</OpalButton>
|
||||
</Button>
|
||||
{!hideUpdateSeats && (
|
||||
<OpalButton
|
||||
prominence="secondary"
|
||||
<Button
|
||||
main
|
||||
secondary
|
||||
onClick={handleStartEdit}
|
||||
icon={SvgPlus}
|
||||
leftIcon={SvgPlus}
|
||||
disabled={isLoadingUsers || disabled || !billing}
|
||||
>
|
||||
Update Seats
|
||||
</OpalButton>
|
||||
</Button>
|
||||
)}
|
||||
</Section>
|
||||
</Section>
|
||||
@@ -567,13 +566,14 @@ function PaymentSection({ billing }: { billing: BillingInformation }) {
|
||||
title="Visa ending in 1234"
|
||||
description="Payment method"
|
||||
/>
|
||||
<OpalButton
|
||||
prominence="tertiary"
|
||||
<Button
|
||||
main
|
||||
tertiary
|
||||
onClick={handleOpenPortal}
|
||||
rightIcon={SvgExternalLink}
|
||||
>
|
||||
Update
|
||||
</OpalButton>
|
||||
</Button>
|
||||
</Section>
|
||||
</Card>
|
||||
{lastPaymentDate && (
|
||||
@@ -589,13 +589,14 @@ function PaymentSection({ billing }: { billing: BillingInformation }) {
|
||||
title={lastPaymentDate}
|
||||
description="Last payment"
|
||||
/>
|
||||
<OpalButton
|
||||
prominence="tertiary"
|
||||
<Button
|
||||
main
|
||||
tertiary
|
||||
onClick={handleOpenPortal}
|
||||
rightIcon={SvgExternalLink}
|
||||
>
|
||||
View Invoice
|
||||
</OpalButton>
|
||||
</Button>
|
||||
</Section>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
@@ -176,7 +176,7 @@ export default function CheckoutView({ onAdjustPlan }: CheckoutViewProps) {
|
||||
Business
|
||||
</Text>
|
||||
</Section>
|
||||
<Button prominence="secondary" onClick={onAdjustPlan}>
|
||||
<Button secondary onClick={onAdjustPlan}>
|
||||
Adjust Plan
|
||||
</Button>
|
||||
</Section>
|
||||
@@ -262,7 +262,7 @@ export default function CheckoutView({ onAdjustPlan }: CheckoutViewProps) {
|
||||
// Empty div to maintain space-between alignment
|
||||
<div></div>
|
||||
)}
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
||||
<Button main primary onClick={handleSubmit} disabled={isSubmitting}>
|
||||
{isSubmitting ? "Loading..." : "Continue to Payment"}
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import InputFile from "@/refresh-components/inputs/InputFile";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
@@ -119,11 +119,11 @@ export default function LicenseActivationCard({
|
||||
</Text>
|
||||
</Section>
|
||||
<Section flexDirection="row" gap={0.5} height="auto" width="auto">
|
||||
<Button prominence="secondary" onClick={() => setShowInput(true)}>
|
||||
<Button main secondary onClick={() => setShowInput(true)}>
|
||||
Update Key
|
||||
</Button>
|
||||
{!hideClose && (
|
||||
<Button prominence="tertiary" onClick={handleClose}>
|
||||
<Button main tertiary onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
)}
|
||||
@@ -146,11 +146,7 @@ export default function LicenseActivationCard({
|
||||
<Text headingH3>
|
||||
{hasLicense ? "Update License Key" : "Activate License Key"}
|
||||
</Text>
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={handleClose}
|
||||
disabled={isActivating}
|
||||
>
|
||||
<Button secondary onClick={handleClose} disabled={isActivating}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Section>
|
||||
@@ -223,6 +219,8 @@ export default function LicenseActivationCard({
|
||||
{/* Footer */}
|
||||
<Section flexDirection="row" justifyContent="end" padding={1}>
|
||||
<Button
|
||||
main
|
||||
primary
|
||||
onClick={handleActivate}
|
||||
disabled={isActivating || !licenseKey.trim() || success}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,6 @@ import "@/app/admin/billing/billing.css";
|
||||
import type { IconProps } from "@opal/types";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
|
||||
@@ -147,27 +146,26 @@ function PlanCard({
|
||||
{/* Button */}
|
||||
<div className="plan-card-button">
|
||||
{isCurrentPlan ? (
|
||||
// TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved
|
||||
<Button tertiary transient className="pointer-events-none">
|
||||
<Text mainUiAction text03>
|
||||
Your Current Plan
|
||||
</Text>
|
||||
</Button>
|
||||
) : href ? (
|
||||
<OpalButton
|
||||
prominence="secondary"
|
||||
<Button
|
||||
main
|
||||
secondary
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{buttonLabel}
|
||||
</OpalButton>
|
||||
</Button>
|
||||
) : onClick ? (
|
||||
<OpalButton onClick={onClick} icon={ButtonIcon}>
|
||||
<Button main primary onClick={onClick} leftIcon={ButtonIcon}>
|
||||
{buttonLabel}
|
||||
</OpalButton>
|
||||
</Button>
|
||||
) : (
|
||||
// TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved
|
||||
<Button tertiary transient className="pointer-events-none">
|
||||
<Text mainUiAction text03>
|
||||
Included in your plan
|
||||
|
||||
@@ -66,7 +66,6 @@ function FooterLinks({
|
||||
<Text secondaryBody text03>
|
||||
Have a license key?
|
||||
</Text>
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button action tertiary onClick={onActivateLicense}>
|
||||
<Text secondaryBody text05 className="underline">
|
||||
{licenseText}
|
||||
@@ -74,7 +73,6 @@ function FooterLinks({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button
|
||||
action
|
||||
tertiary
|
||||
|
||||
@@ -10,7 +10,7 @@ import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { EditableStringFieldDisplay } from "@/components/EditableStringFieldDisplay";
|
||||
import { deleteSlackBot } from "./new/lib";
|
||||
import GenericConfirmModal from "@/components/modals/GenericConfirmModal";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SvgChevronDownSmall, SvgTrash } from "@opal/icons";
|
||||
|
||||
@@ -106,20 +106,20 @@ export const ExistingSlackBotForm = ({
|
||||
<div className="flex flex-col" ref={dropdownRef}>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
prominence="secondary"
|
||||
icon={({ className }) => (
|
||||
leftIcon={({ className }) => (
|
||||
<SvgChevronDownSmall
|
||||
className={cn(className, !isExpanded && "-rotate-90")}
|
||||
/>
|
||||
)}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
secondary
|
||||
>
|
||||
Update Tokens
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
danger
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
icon={SvgTrash}
|
||||
leftIcon={SvgTrash}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TextFormField } from "@/components/Field";
|
||||
import { Form, Formik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { createSlackBot, updateSlackBot } from "./new/lib";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
import { useEffect } from "react";
|
||||
import { DOCS_ADMINS_PATH } from "@/lib/constants";
|
||||
|
||||
@@ -17,8 +17,9 @@ import type { Route } from "next";
|
||||
import { useState } from "react";
|
||||
import { deleteSlackChannelConfig, isPersonaASlackBotPersona } from "./lib";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import CreateButton from "@/refresh-components/buttons/CreateButton";
|
||||
import { Button } from "@opal/components";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import { SvgSettings, SvgTrash } from "@opal/icons";
|
||||
const numToDisplay = 50;
|
||||
|
||||
@@ -44,11 +45,11 @@ export default function SlackChannelConfigsTable({
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={() => {
|
||||
window.location.href = `/admin/bots/${slackBotId}/channels/${defaultConfig?.id}`;
|
||||
}}
|
||||
icon={SvgSettings}
|
||||
secondary
|
||||
leftIcon={SvgSettings}
|
||||
>
|
||||
Edit Default Configuration
|
||||
</Button>
|
||||
@@ -120,7 +121,7 @@ export default function SlackChannelConfigsTable({
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
<OpalButton
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
const response = await deleteSlackChannelConfig(
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
TextArrayField,
|
||||
TextFormField,
|
||||
} from "@/components/Field";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { MinimalPersonaSnapshot } from "@/app/admin/agents/interfaces";
|
||||
import DocumentSetCard from "@/sections/cards/DocumentSetCard";
|
||||
import CollapsibleSection from "@/app/admin/agents/CollapsibleSection";
|
||||
@@ -598,7 +598,7 @@ export function SlackChannelConfigFormFields({
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Button type="submit">{isUpdate ? "Update" : "Create"}</Button>
|
||||
<Button prominence="secondary" onClick={() => router.back()}>
|
||||
<Button secondary onClick={() => router.back()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import useSWR from "swr";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
@@ -129,7 +129,7 @@ function Main() {
|
||||
<div className="flex flex-col gap-2 desktop:flex-row desktop:items-center desktop:gap-2">
|
||||
{isApiKeySet ? (
|
||||
<>
|
||||
<Button variant="danger" onClick={handleDelete}>
|
||||
<Button onClick={handleDelete} danger>
|
||||
Delete API Key
|
||||
</Button>
|
||||
<Text as="p" mainContentBody text04 className="desktop:mt-0">
|
||||
@@ -137,7 +137,7 @@ function Main() {
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="action" onClick={handleSave}>
|
||||
<Button onClick={handleSave} action>
|
||||
Save API Key
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import Text from "@/components/ui/text";
|
||||
import Title from "@/components/ui/title";
|
||||
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 { ReindexingProgressTable } from "../../../../components/embedding/ReindexingProgressTable";
|
||||
@@ -24,7 +23,6 @@ import { FailedReIndexAttempts } from "@/components/embedding/FailedReIndexAttem
|
||||
import { useConnectorIndexingStatusWithPagination } from "@/lib/hooks";
|
||||
import { SvgX } from "@opal/icons";
|
||||
import { ConnectorCredentialPairStatus } from "@/app/admin/connector/[ccPairId]/types";
|
||||
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
|
||||
|
||||
export default function UpgradingPage({
|
||||
futureEmbeddingModel,
|
||||
@@ -32,12 +30,11 @@ export default function UpgradingPage({
|
||||
futureEmbeddingModel: CloudEmbeddingModel | HostedEmbeddingModel;
|
||||
}) {
|
||||
const [isCancelling, setIsCancelling] = useState<boolean>(false);
|
||||
const vectorDbEnabled = useVectorDbEnabled();
|
||||
|
||||
const { data: connectors, isLoading: isLoadingConnectors } = useSWR<
|
||||
Connector<any>[]
|
||||
>(vectorDbEnabled ? "/api/manage/connector" : null, errorHandlingFetcher, {
|
||||
refreshInterval: 5000,
|
||||
>("/api/manage/connector", errorHandlingFetcher, {
|
||||
refreshInterval: 5000, // 5 seconds
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -45,8 +42,7 @@ export default function UpgradingPage({
|
||||
isLoading: isLoadingOngoingReIndexingStatus,
|
||||
} = useConnectorIndexingStatusWithPagination(
|
||||
{ secondary_index: true, get_all_connectors: true },
|
||||
5000,
|
||||
vectorDbEnabled
|
||||
5000
|
||||
) as {
|
||||
data: ConnectorIndexingStatusLiteResponse[];
|
||||
isLoading: boolean;
|
||||
@@ -55,11 +51,9 @@ export default function UpgradingPage({
|
||||
const { data: failedIndexingStatus } = useSWR<
|
||||
FailedConnectorIndexingStatus[]
|
||||
>(
|
||||
vectorDbEnabled
|
||||
? "/api/manage/admin/connector/failed-indexing-status?secondary_index=true"
|
||||
: null,
|
||||
"/api/manage/admin/connector/failed-indexing-status?secondary_index=true",
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 }
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
|
||||
const onCancel = async () => {
|
||||
@@ -146,13 +140,10 @@ export default function UpgradingPage({
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<OpalButton onClick={onCancel}>Confirm</OpalButton>
|
||||
<OpalButton
|
||||
prominence="secondary"
|
||||
onClick={() => setIsCancelling(false)}
|
||||
>
|
||||
<Button onClick={onCancel}>Confirm</Button>
|
||||
<Button onClick={() => setIsCancelling(false)} secondary>
|
||||
Cancel
|
||||
</OpalButton>
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
@@ -167,7 +158,6 @@ export default function UpgradingPage({
|
||||
{futureEmbeddingModel.model_name}
|
||||
</div>
|
||||
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button
|
||||
danger
|
||||
className="mt-4"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import * as SettingsLayouts from "@/layouts/settings-layouts";
|
||||
import Text from "@/components/ui/text";
|
||||
import Title from "@/components/ui/title";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import useSWR from "swr";
|
||||
import { ModelPreview } from "@/components/embedding/ModelSelector";
|
||||
import {
|
||||
@@ -130,7 +130,7 @@ function Main() {
|
||||
</CardSection>
|
||||
|
||||
<div className="mt-4">
|
||||
<Button variant="action" href="/admin/embeddings">
|
||||
<Button action href="/admin/embeddings">
|
||||
Update Search Settings
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FormField } from "@/refresh-components/form/FormField";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import PasswordInputTypeIn from "@/refresh-components/inputs/PasswordInputTypeIn";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
|
||||
import { SvgArrowExchange, SvgOnyxLogo } from "@opal/icons";
|
||||
import type { IconProps } from "@opal/types";
|
||||
@@ -244,11 +244,13 @@ export const WebProviderSetupModal = memo(
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button prominence="secondary" type="button" onClick={onClose}>
|
||||
<Button type="button" main secondary onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
main
|
||||
primary
|
||||
disabled={!canConnect || isProcessing}
|
||||
onClick={onConnect}
|
||||
>
|
||||
|
||||
@@ -91,7 +91,6 @@ function HoverIconButton({
|
||||
}: HoverIconButtonProps) {
|
||||
return (
|
||||
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
{/* TODO(@raunakab): migrate to opal Button once HoverIconButtonProps typing is resolved */}
|
||||
<Button {...buttonProps} rightIcon={isHovered ? SvgX : SvgCheckSquare}>
|
||||
{children}
|
||||
</Button>
|
||||
@@ -1011,8 +1010,9 @@ export default function Page() {
|
||||
{buttonState.label}
|
||||
</HoverIconButton>
|
||||
) : (
|
||||
<OpalButton
|
||||
prominence="tertiary"
|
||||
<Button
|
||||
action={false}
|
||||
tertiary
|
||||
disabled={
|
||||
buttonState.disabled || !buttonState.onClick
|
||||
}
|
||||
@@ -1029,7 +1029,7 @@ export default function Page() {
|
||||
}
|
||||
>
|
||||
{buttonState.label}
|
||||
</OpalButton>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1202,8 +1202,9 @@ export default function Page() {
|
||||
{buttonState.label}
|
||||
</HoverIconButton>
|
||||
) : (
|
||||
<OpalButton
|
||||
prominence="tertiary"
|
||||
<Button
|
||||
action={false}
|
||||
tertiary
|
||||
disabled={
|
||||
buttonState.disabled || !buttonState.onClick
|
||||
}
|
||||
@@ -1220,7 +1221,7 @@ export default function Page() {
|
||||
}
|
||||
>
|
||||
{buttonState.label}
|
||||
</OpalButton>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useState } from "react";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Button } from "@opal/components";
|
||||
import IconButton from "@/refresh-components/buttons/IconButton";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import Separator from "@/refresh-components/Separator";
|
||||
import { SvgChevronUp, SvgChevronDown, SvgEdit } from "@opal/icons";
|
||||
import Truncated from "@/refresh-components/texts/Truncated";
|
||||
@@ -118,21 +119,16 @@ function ConfigItem({ label, value, onEdit }: ConfigItemProps) {
|
||||
|
||||
{isExpandable && (
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
tertiary
|
||||
size="md"
|
||||
icon={isExpanded ? SvgChevronUp : SvgChevronDown}
|
||||
leftIcon={isExpanded ? SvgChevronUp : SvgChevronDown}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? "Show less" : `Show all (${value.length} items)`}
|
||||
</Button>
|
||||
)}
|
||||
{onEdit && (
|
||||
<Button
|
||||
prominence="tertiary"
|
||||
icon={SvgEdit}
|
||||
onClick={onEdit}
|
||||
tooltip="Edit"
|
||||
/>
|
||||
<IconButton icon={SvgEdit} tertiary onClick={onEdit} tooltip="Edit" />
|
||||
)}
|
||||
</Section>
|
||||
</Section>
|
||||
|
||||
@@ -229,7 +229,6 @@ export default function IndexAttemptErrorsModal({
|
||||
<div className="flex w-full">
|
||||
<div className="flex gap-2 ml-auto">
|
||||
{hasUnresolvedErrors && !isResolvingErrors && (
|
||||
// TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved
|
||||
<Button
|
||||
onClick={onResolveAll}
|
||||
className="ml-4 whitespace-nowrap"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -175,25 +176,26 @@ export default function InlineFileManagement({
|
||||
<div className="flex gap-2">
|
||||
{!isEditing ? (
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={() => setIsEditing(true)}
|
||||
icon={SvgEdit}
|
||||
secondary
|
||||
leftIcon={SvgEdit}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={handleCancel}
|
||||
icon={SvgX}
|
||||
secondary
|
||||
leftIcon={SvgX}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveClick}
|
||||
icon={SvgCheck}
|
||||
primary
|
||||
leftIcon={SvgCheck}
|
||||
disabled={
|
||||
isSaving ||
|
||||
(selectedFilesToRemove.size === 0 && filesToAdd.length === 0)
|
||||
@@ -293,7 +295,7 @@ export default function InlineFileManagement({
|
||||
>
|
||||
{isEditing && (
|
||||
<TableCell>
|
||||
<Button
|
||||
<OpalButton
|
||||
icon={SvgX}
|
||||
variant="danger"
|
||||
prominence="tertiary"
|
||||
@@ -333,9 +335,9 @@ export default function InlineFileManagement({
|
||||
id={`file-upload-${connectorId}`}
|
||||
/>
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
icon={SvgPlusCircle}
|
||||
secondary
|
||||
leftIcon={SvgPlusCircle}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Add Files
|
||||
@@ -396,8 +398,8 @@ export default function InlineFileManagement({
|
||||
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={() => setShowSaveConfirm(false)}
|
||||
secondary
|
||||
disabled={isSaving}
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { useState } from "react";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import { triggerIndexing } from "@/app/admin/connector/[ccPairId]/lib";
|
||||
|
||||
@@ -62,7 +62,7 @@ import { DropdownMenuItemWithTooltip } from "@/components/ui/dropdown-menu-with-
|
||||
import { timeAgo } from "@/lib/time";
|
||||
import { useStatusChange } from "./useStatusChange";
|
||||
import { useReIndexModal } from "./ReIndexModal";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { SvgSettings } from "@opal/icons";
|
||||
import { UserRole } from "@/lib/types";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
@@ -456,7 +456,7 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
{ccPair.is_editable_for_current_user && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button prominence="secondary" icon={SvgSettings}>
|
||||
<Button leftIcon={SvgSettings} secondary>
|
||||
Manage
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -55,7 +55,7 @@ import {
|
||||
} from "@/lib/connectors/oauth";
|
||||
import { CreateStdOAuthCredential } from "@/components/credentials/actions/CreateStdOAuthCredential";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { deleteConnector } from "@/lib/connector";
|
||||
import ConnectorDocsLink from "@/components/admin/connectors/ConnectorDocsLink";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
@@ -580,7 +580,7 @@ export default function AddConnector({
|
||||
{oauthSupportedSources.includes(connector) &&
|
||||
(NEXT_PUBLIC_CLOUD_ENABLED || NEXT_PUBLIC_TEST_ENV) && (
|
||||
<Button
|
||||
variant="action"
|
||||
action
|
||||
onClick={handleAuthorize}
|
||||
disabled={isAuthorizing}
|
||||
hidden={!isAuthorizeVisible}
|
||||
|
||||
@@ -48,7 +48,6 @@ export default function ConnectorWrapper({
|
||||
<HeaderTitle>
|
||||
<p>‘{connector}’ is not a valid Connector Type!</p>
|
||||
</HeaderTitle>
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button
|
||||
onClick={() => window.open("/admin/indexing/status", "_self")}
|
||||
className="mr-auto"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useFormContext } from "@/components/context/FormContext";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { SvgArrowLeft, SvgArrowRight, SvgPlusCircle } from "@opal/icons";
|
||||
|
||||
const NavigationRow = ({
|
||||
@@ -22,11 +22,7 @@ const NavigationRow = ({
|
||||
<div>
|
||||
{((formStep > 0 && !noCredentials) ||
|
||||
(formStep > 1 && !noAdvanced)) && (
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={prevFormStep}
|
||||
icon={SvgArrowLeft}
|
||||
>
|
||||
<Button secondary onClick={prevFormStep} leftIcon={SvgArrowLeft}>
|
||||
Previous
|
||||
</Button>
|
||||
)}
|
||||
@@ -45,7 +41,7 @@ const NavigationRow = ({
|
||||
<div className="flex justify-end">
|
||||
{formStep === 0 && (
|
||||
<Button
|
||||
variant="action"
|
||||
action
|
||||
disabled={!activatedCredential}
|
||||
rightIcon={SvgArrowRight}
|
||||
onClick={() => nextFormStep()}
|
||||
@@ -55,7 +51,7 @@ const NavigationRow = ({
|
||||
)}
|
||||
{!noAdvanced && formStep === 1 && (
|
||||
<Button
|
||||
prominence="secondary"
|
||||
secondary
|
||||
disabled={!isValid}
|
||||
rightIcon={SvgArrowRight}
|
||||
onClick={() => nextFormStep()}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { getSourceMetadata, isValidSource } from "@/lib/sources";
|
||||
import { ConfluenceAccessibleResource, ValidSources } from "@/lib/types";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import NumberInput from "./ConnectorInput/NumberInput";
|
||||
import { TextFormField } from "@/components/Field";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { SvgTrash } from "@opal/icons";
|
||||
export default function AdvancedFormPage() {
|
||||
return (
|
||||
@@ -35,7 +35,7 @@ export default function AdvancedFormPage() {
|
||||
name="indexingStart"
|
||||
/>
|
||||
<div className="mt-4 flex w-full mx-auto max-w-2xl justify-start">
|
||||
<Button variant="danger" icon={SvgTrash} type="submit">
|
||||
<Button leftIcon={SvgTrash} danger type="submit">
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DOCS_ADMINS_PATH } from "@/lib/constants";
|
||||
import { TextFormField, SectionHeader } from "@/components/Field";
|
||||
import { Form, Formik } from "formik";
|
||||
import { User } from "@/lib/types";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import {
|
||||
Credential,
|
||||
GoogleDriveCredentialJson,
|
||||
@@ -314,7 +314,7 @@ export const DriveJsonUploadSection = ({
|
||||
{isAdmin && !existingAuthCredential && (
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
variant="danger"
|
||||
danger
|
||||
onClick={async () => {
|
||||
const endpoint =
|
||||
localServiceAccountData?.service_account_email
|
||||
@@ -467,7 +467,7 @@ export const DriveAuthSection = ({
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="danger"
|
||||
danger
|
||||
onClick={async () => {
|
||||
handleRevokeAccess(
|
||||
connectorAssociated,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -314,7 +314,7 @@ export const GmailJsonUploadSection = ({
|
||||
{isAdmin && !existingAuthCredential && (
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
variant="danger"
|
||||
danger
|
||||
onClick={async () => {
|
||||
const endpoint =
|
||||
localServiceAccountData?.service_account_email
|
||||
@@ -470,7 +470,7 @@ export const GmailAuthSection = ({
|
||||
</div>
|
||||
<Section flexDirection="row" justifyContent="between" height="fit">
|
||||
<Button
|
||||
variant="danger"
|
||||
danger
|
||||
onClick={async () => {
|
||||
handleRevokeAccess(
|
||||
connectorExists,
|
||||
@@ -482,7 +482,10 @@ export const GmailAuthSection = ({
|
||||
Revoke Access
|
||||
</Button>
|
||||
{buildMode && onCredentialCreated && (
|
||||
<Button onClick={() => onCredentialCreated(existingCredential)}>
|
||||
<Button
|
||||
primary
|
||||
onClick={() => onCredentialCreated(existingCredential)}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import Text from "@/components/ui/text";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
@@ -99,9 +99,9 @@ function Main() {
|
||||
<TableCell className="font-medium">{category}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={() => handleDownload(category)}
|
||||
icon={SvgDownloadCloud}
|
||||
secondary
|
||||
leftIcon={SvgDownloadCloud}
|
||||
>
|
||||
Download Logs
|
||||
</Button>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
||||
import { Section } from "@/layouts/general-layouts";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import PasswordInputTypeIn from "@/refresh-components/inputs/PasswordInputTypeIn";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
@@ -126,9 +126,9 @@ export function BotConfigCard() {
|
||||
disabled={!hasServerConfigs}
|
||||
>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={isSubmitting || hasServerConfigs}
|
||||
danger
|
||||
>
|
||||
Delete Discord Token
|
||||
</Button>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DeleteButton } from "@/components/DeleteButton";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import Switch from "@/refresh-components/inputs/Switch";
|
||||
import { SvgEdit, SvgServer } from "@opal/icons";
|
||||
import EmptyMessage from "@/refresh-components/EmptyMessage";
|
||||
@@ -116,10 +116,10 @@ export function DiscordGuildsTable({ guilds, onRefresh }: Props) {
|
||||
<TableRow key={guild.id}>
|
||||
<TableCell>
|
||||
<Button
|
||||
prominence="internal"
|
||||
internal
|
||||
disabled={!guild.guild_id}
|
||||
onClick={() => router.push(`/admin/discord-bot/${guild.id}`)}
|
||||
icon={SvgEdit}
|
||||
leftIcon={SvgEdit}
|
||||
>
|
||||
{guild.guild_name || `Server #${guild.id}`}
|
||||
</Button>
|
||||
|
||||
@@ -12,7 +12,7 @@ import Text from "@/refresh-components/texts/Text";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
import Message from "@/refresh-components/messages/Message";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { SvgServer } from "@opal/icons";
|
||||
import InputSelect from "@/refresh-components/inputs/InputSelect";
|
||||
import {
|
||||
@@ -104,17 +104,13 @@ function GuildDetailContent({
|
||||
width="fit"
|
||||
gap={0.5}
|
||||
>
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={handleEnableAll}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button onClick={handleEnableAll} disabled={disabled} secondary>
|
||||
Enable All
|
||||
</Button>
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={handleDisableAll}
|
||||
disabled={disabled}
|
||||
secondary
|
||||
>
|
||||
Disable All
|
||||
</Button>
|
||||
|
||||
@@ -200,7 +200,6 @@ function RetrievalSourceSection() {
|
||||
</InputSelect>
|
||||
|
||||
{hasChanges && (
|
||||
// TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved
|
||||
<Button
|
||||
className="self-center"
|
||||
onClick={handleUpdate}
|
||||
|
||||
@@ -257,7 +257,6 @@ export const DocumentSetCreationForm = ({
|
||||
</div>
|
||||
|
||||
<div className="flex mt-6 pt-4 border-t border-border-02">
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={props.isSubmitting}
|
||||
|
||||
@@ -10,11 +10,9 @@ import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { DocumentSetCreationForm } from "../DocumentSetCreationForm";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
|
||||
|
||||
function Main({ documentSetId }: { documentSetId: number }) {
|
||||
const router = useRouter();
|
||||
const vectorDbEnabled = useVectorDbEnabled();
|
||||
|
||||
const {
|
||||
data: documentSets,
|
||||
@@ -26,16 +24,12 @@ function Main({ documentSetId }: { documentSetId: number }) {
|
||||
data: ccPairs,
|
||||
isLoading: isCCPairsLoading,
|
||||
error: ccPairsError,
|
||||
} = useConnectorStatus(30000, vectorDbEnabled);
|
||||
} = useConnectorStatus();
|
||||
|
||||
// EE only
|
||||
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
|
||||
|
||||
if (
|
||||
isDocumentSetsLoading ||
|
||||
(vectorDbEnabled && isCCPairsLoading) ||
|
||||
userGroupsIsLoading
|
||||
) {
|
||||
if (isDocumentSetsLoading || isCCPairsLoading || userGroupsIsLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<ThreeDotsLoader />
|
||||
@@ -52,7 +46,7 @@ function Main({ documentSetId }: { documentSetId: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
if (vectorDbEnabled && (ccPairsError || !ccPairs)) {
|
||||
if (ccPairsError || !ccPairs) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Failed to fetch Connectors"
|
||||
@@ -76,7 +70,7 @@ function Main({ documentSetId }: { documentSetId: number }) {
|
||||
return (
|
||||
<CardSection>
|
||||
<DocumentSetCreationForm
|
||||
ccPairs={ccPairs ?? []}
|
||||
ccPairs={ccPairs}
|
||||
userGroups={userGroups}
|
||||
onClose={() => {
|
||||
refreshDocumentSets();
|
||||
|
||||
@@ -9,22 +9,20 @@ import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { refreshDocumentSets } from "../hooks";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
|
||||
|
||||
function Main() {
|
||||
const router = useRouter();
|
||||
const vectorDbEnabled = useVectorDbEnabled();
|
||||
|
||||
const {
|
||||
data: ccPairs,
|
||||
isLoading: isCCPairsLoading,
|
||||
error: ccPairsError,
|
||||
} = useConnectorStatus(30000, vectorDbEnabled);
|
||||
} = useConnectorStatus();
|
||||
|
||||
// EE only
|
||||
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
|
||||
|
||||
if ((vectorDbEnabled && isCCPairsLoading) || userGroupsIsLoading) {
|
||||
if (isCCPairsLoading || userGroupsIsLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[400px]">
|
||||
<ThreeDotsLoader />
|
||||
@@ -32,7 +30,7 @@ function Main() {
|
||||
);
|
||||
}
|
||||
|
||||
if (vectorDbEnabled && (ccPairsError || !ccPairs)) {
|
||||
if (ccPairsError || !ccPairs) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Failed to fetch Connectors"
|
||||
@@ -45,7 +43,7 @@ function Main() {
|
||||
<>
|
||||
<CardSection>
|
||||
<DocumentSetCreationForm
|
||||
ccPairs={ccPairs ?? []}
|
||||
ccPairs={ccPairs}
|
||||
userGroups={userGroups}
|
||||
onClose={() => {
|
||||
refreshDocumentSets();
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
EMBEDDING_PROVIDERS_ADMIN_URL,
|
||||
} from "@/lib/llmConfig/constants";
|
||||
import { AdvancedSearchConfiguration } from "@/app/admin/embeddings/interfaces";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
|
||||
export interface EmbeddingDetails {
|
||||
api_key?: string;
|
||||
@@ -279,7 +279,7 @@ export default function EmbeddingModelSelection({
|
||||
{currentEmbeddingModel?.provider_type && (
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
prominence="secondary"
|
||||
secondary
|
||||
onClick={() => {
|
||||
const allProviders = [
|
||||
...AVAILABLE_CLOUD_PROVIDERS,
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
MixedBreadIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { TextFormField } from "@/components/Field";
|
||||
import { SettingsContext } from "@/providers/SettingsProvider";
|
||||
import { SvgAlertTriangle, SvgKey } from "@opal/icons";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { CloudEmbeddingModel } from "../../../../components/embedding/interfaces";
|
||||
import { SvgCheck } from "@opal/icons";
|
||||
|
||||
|
||||
@@ -268,7 +268,6 @@ export default function ChangeCredentialsModal({
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button
|
||||
className="mr-auto mt-4"
|
||||
onClick={() => handleSubmit()}
|
||||
@@ -290,7 +289,6 @@ export default function ChangeCredentialsModal({
|
||||
embedding type!
|
||||
</Text>
|
||||
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button className="mr-auto" onClick={handleDelete} danger>
|
||||
Delete Configuration
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
import {
|
||||
CloudEmbeddingProvider,
|
||||
@@ -38,10 +38,10 @@ export default function DeleteCredentialsModal({
|
||||
<Callout type="danger" title="Point of No Return" />
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button prominence="secondary" onClick={onCancel}>
|
||||
<Button secondary onClick={onCancel}>
|
||||
Keep Credentials
|
||||
</Button>
|
||||
<Button variant="danger" onClick={onConfirm}>
|
||||
<Button danger onClick={onConfirm}>
|
||||
Delete Credentials
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { SvgAlertTriangle } from "@opal/icons";
|
||||
export interface InstantSwitchConfirmModalProps {
|
||||
@@ -31,7 +31,7 @@ export default function InstantSwitchConfirmModal({
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={onConfirm}>Confirm</Button>
|
||||
<Button prominence="secondary" onClick={onClose}>
|
||||
<Button secondary onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { HostedEmbeddingModel } from "@/components/embedding/interfaces";
|
||||
import { SvgServer } from "@opal/icons";
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function ModelSelectionConfirmationModal({
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={onConfirm}>Confirm</Button>
|
||||
<Button prominence="secondary" onClick={onCancel}>
|
||||
<Button secondary onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Callout } from "@/components/ui/callout";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { Label, TextFormField } from "@/components/Field";
|
||||
import { LoadingAnimation } from "@/components/Loading";
|
||||
import {
|
||||
CloudEmbeddingProvider,
|
||||
EmbeddingProvider,
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
import { EMBEDDING_PROVIDERS_ADMIN_URL } from "@/lib/llmConfig/constants";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { SvgSettings } from "@opal/icons";
|
||||
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
|
||||
export interface ProviderCreationModalProps {
|
||||
updateCurrentModel: (
|
||||
newModel: string,
|
||||
@@ -39,6 +39,7 @@ export default function ProviderCreationModal({
|
||||
const useFileUpload =
|
||||
selectedProvider.provider_type == EmbeddingProvider.GOOGLE;
|
||||
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState<string>("");
|
||||
const [fileName, setFileName] = useState<string>("");
|
||||
|
||||
@@ -109,6 +110,7 @@ export default function ProviderCreationModal({
|
||||
values: any,
|
||||
{ setSubmitting }: { setSubmitting: (isSubmitting: boolean) => void }
|
||||
) => {
|
||||
setIsProcessing(true);
|
||||
setErrorMsg("");
|
||||
try {
|
||||
const customConfig = Object.fromEntries(values.custom_config);
|
||||
@@ -139,6 +141,7 @@ export default function ProviderCreationModal({
|
||||
if (!initialResponse.ok) {
|
||||
const errorMsg = (await initialResponse.json()).detail;
|
||||
setErrorMsg(errorMsg);
|
||||
setIsProcessing(false);
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
@@ -176,6 +179,7 @@ export default function ProviderCreationModal({
|
||||
setErrorMsg("An unknown error occurred");
|
||||
}
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
@@ -298,15 +302,16 @@ export default function ProviderCreationModal({
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
width="full"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
icon={isSubmitting ? SimpleLoader : undefined}
|
||||
>
|
||||
{isSubmitting
|
||||
? "Submitting"
|
||||
: existingProvider
|
||||
? "Update"
|
||||
: "Create"}
|
||||
{isProcessing ? (
|
||||
<LoadingAnimation />
|
||||
) : existingProvider ? (
|
||||
"Update"
|
||||
) : (
|
||||
"Create"
|
||||
)}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { CloudEmbeddingModel } from "@/components/embedding/interfaces";
|
||||
import { SvgServer } from "@opal/icons";
|
||||
@@ -32,7 +32,7 @@ export default function SelectModelModal({
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={onConfirm}>Confirm</Button>
|
||||
<Button prominence="secondary" onClick={onCancel}>
|
||||
<Button secondary onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
||||
@@ -6,7 +6,6 @@ import EmbeddingModelSelection from "../EmbeddingModelSelectionForm";
|
||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import { WarningCircle, Warning, CaretDownIcon } from "@phosphor-icons/react";
|
||||
import {
|
||||
CloudEmbeddingModel,
|
||||
@@ -248,7 +247,6 @@ export default function EmbeddingForm() {
|
||||
return needsReIndex ? (
|
||||
<div className="flex mx-auto gap-x-1 ml-auto items-center">
|
||||
<div className="flex items-center h-fit">
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (switchoverType == SwitchoverType.INSTANT) {
|
||||
@@ -270,7 +268,6 @@ export default function EmbeddingForm() {
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
|
||||
<Button
|
||||
disabled={!isOverallFormValid}
|
||||
action
|
||||
@@ -376,7 +373,7 @@ export default function EmbeddingForm() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex mx-auto gap-x-1 ml-auto items-center">
|
||||
<OpalButton
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateSearch();
|
||||
navigateToEmbeddingPage("search settings");
|
||||
@@ -384,7 +381,7 @@ export default function EmbeddingForm() {
|
||||
disabled={!isOverallFormValid}
|
||||
>
|
||||
Update Search
|
||||
</OpalButton>
|
||||
</Button>
|
||||
{!isOverallFormValid &&
|
||||
Object.keys(combinedFormErrors).length > 0 && (
|
||||
<div className="relative group">
|
||||
@@ -510,8 +507,7 @@ export default function EmbeddingForm() {
|
||||
/>
|
||||
</CardSection>
|
||||
<div className="mt-4 flex w-full justify-end">
|
||||
<OpalButton
|
||||
variant="action"
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (
|
||||
selectedProvider.model_name.includes("e5") &&
|
||||
@@ -526,9 +522,10 @@ export default function EmbeddingForm() {
|
||||
}
|
||||
}}
|
||||
rightIcon={SvgArrowRight}
|
||||
action
|
||||
>
|
||||
Continue
|
||||
</OpalButton>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -560,13 +557,10 @@ export default function EmbeddingForm() {
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<OpalButton
|
||||
prominence="secondary"
|
||||
onClick={() => setShowPoorModel(false)}
|
||||
>
|
||||
<Button secondary onClick={() => setShowPoorModel(false)}>
|
||||
Cancel update
|
||||
</OpalButton>
|
||||
<OpalButton
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowPoorModel(false);
|
||||
// Skip reranking step (step 1), go directly to advanced settings (step 2)
|
||||
@@ -575,7 +569,7 @@ export default function EmbeddingForm() {
|
||||
}}
|
||||
>
|
||||
{`Continue with ${selectedProvider.model_name}`}
|
||||
</OpalButton>
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
@@ -621,26 +615,26 @@ export default function EmbeddingForm() {
|
||||
</CardSection>
|
||||
|
||||
<div className={`mt-4 w-full grid grid-cols-3`}>
|
||||
<OpalButton
|
||||
prominence="secondary"
|
||||
icon={SvgArrowLeft}
|
||||
<Button
|
||||
leftIcon={SvgArrowLeft}
|
||||
onClick={() => prevFormStep()}
|
||||
secondary
|
||||
>
|
||||
Previous
|
||||
</OpalButton>
|
||||
</Button>
|
||||
|
||||
<ReIndexingButton needsReIndex={needsReIndex} />
|
||||
|
||||
<div className="flex w-full justify-end">
|
||||
<OpalButton
|
||||
prominence="secondary"
|
||||
<Button
|
||||
onClick={() => {
|
||||
nextFormStep();
|
||||
}}
|
||||
rightIcon={SvgArrowRight}
|
||||
secondary
|
||||
>
|
||||
Advanced
|
||||
</OpalButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -666,17 +660,17 @@ export default function EmbeddingForm() {
|
||||
</CardSection>
|
||||
|
||||
<div className={`mt-4 grid grid-cols-3 w-full `}>
|
||||
<OpalButton
|
||||
prominence="secondary"
|
||||
<Button
|
||||
onClick={() => {
|
||||
// Skip reranking step (step 1), go back to embedding model (step 0)
|
||||
prevFormStep();
|
||||
prevFormStep();
|
||||
}}
|
||||
icon={SvgArrowLeft}
|
||||
leftIcon={SvgArrowLeft}
|
||||
secondary
|
||||
>
|
||||
Previous
|
||||
</OpalButton>
|
||||
</Button>
|
||||
|
||||
<ReIndexingButton needsReIndex={needsReIndex} />
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,6 @@ export default function OpenEmbeddingPage({
|
||||
Onyx team.
|
||||
</Text>
|
||||
{!configureModel && (
|
||||
// TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved
|
||||
<Button
|
||||
onClick={() => setConfigureModel(true)}
|
||||
className="mt-4"
|
||||
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { AccessType, ValidStatuses } from "@/lib/types";
|
||||
import { Button } from "@opal/components";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import { SvgFilter } from "@opal/icons";
|
||||
export interface FilterOptions {
|
||||
accessType: AccessType[] | null;
|
||||
@@ -127,7 +128,11 @@ export const FilterComponent = forwardRef<
|
||||
<div className="relative">
|
||||
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button icon={SvgFilter} prominence="secondary" transient={isOpen} />
|
||||
<OpalButton
|
||||
icon={SvgFilter}
|
||||
prominence="secondary"
|
||||
transient={isOpen}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
@@ -237,7 +242,7 @@ export const FilterComponent = forwardRef<
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
prominence={docsOperator !== ">" ? "secondary" : "primary"}
|
||||
secondary={docsOperator !== ">"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -248,7 +253,7 @@ export const FilterComponent = forwardRef<
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
prominence={docsOperator !== "<" ? "secondary" : "primary"}
|
||||
secondary={docsOperator !== "<"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -259,7 +264,7 @@ export const FilterComponent = forwardRef<
|
||||
<
|
||||
</Button>
|
||||
<Button
|
||||
prominence={docsOperator !== "=" ? "secondary" : "primary"}
|
||||
secondary={docsOperator !== "="}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -281,7 +286,7 @@ export const FilterComponent = forwardRef<
|
||||
</div>
|
||||
<div className="px-2 py-1.5">
|
||||
<Button
|
||||
width="full"
|
||||
className="w-full"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@opal/components";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { FilterComponent, FilterOptions } from "./FilterComponent";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
|
||||
@@ -8,8 +8,7 @@ import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
|
||||
import Text from "@/components/ui/text";
|
||||
import { useConnectorIndexingStatusWithPagination } from "@/lib/hooks";
|
||||
import { useToastFromQuery } from "@/hooks/useToast";
|
||||
import { Button } from "@opal/components";
|
||||
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { useState, useRef, useMemo, RefObject } from "react";
|
||||
import { FilterOptions } from "./FilterComponent";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
@@ -19,8 +18,6 @@ import { ConnectorStaggeredSkeleton } from "./ConnectorRowSkeleton";
|
||||
import { IndexingStatusRequest } from "@/lib/types";
|
||||
|
||||
function Main() {
|
||||
const vectorDbEnabled = useVectorDbEnabled();
|
||||
|
||||
// State for filter management
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
accessType: null,
|
||||
@@ -68,7 +65,7 @@ function Main() {
|
||||
sourcePages,
|
||||
sourceLoadingStates,
|
||||
resetPagination,
|
||||
} = useConnectorIndexingStatusWithPagination(request, 30000, vectorDbEnabled);
|
||||
} = useConnectorIndexingStatusWithPagination(request, 30000);
|
||||
|
||||
// Check if filters are active
|
||||
const hasActiveFilters = useMemo(() => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user