mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-18 08:15:48 +00:00
Compare commits
15 Commits
jamison/he
...
thread_sen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5848975679 | ||
|
|
dcc330010e | ||
|
|
d0f5f1f5ae | ||
|
|
3e475993ff | ||
|
|
7c2b5fa822 | ||
|
|
409cfdc788 | ||
|
|
7a9a132739 | ||
|
|
33bad8c37b | ||
|
|
9241ff7a75 | ||
|
|
0a25bc30ec | ||
|
|
e359732f4c | ||
|
|
be47866a4d | ||
|
|
8a20540559 | ||
|
|
e6e1f2860a | ||
|
|
fc3f433df7 |
@@ -12,6 +12,7 @@ from celery import Celery
|
||||
from celery import shared_task
|
||||
from celery import Task
|
||||
from celery.exceptions import SoftTimeLimitExceeded
|
||||
from fastapi import HTTPException
|
||||
from pydantic import BaseModel
|
||||
from redis import Redis
|
||||
from redis.lock import Lock as RedisLock
|
||||
@@ -40,9 +41,11 @@ from onyx.background.indexing.checkpointing_utils import (
|
||||
)
|
||||
from onyx.background.indexing.index_attempt_utils import cleanup_index_attempts
|
||||
from onyx.background.indexing.index_attempt_utils import get_old_index_attempts
|
||||
from onyx.configs.app_configs import AUTH_TYPE
|
||||
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 AuthType
|
||||
from onyx.configs.constants import CELERY_GENERIC_BEAT_LOCK_TIMEOUT
|
||||
from onyx.configs.constants import CELERY_INDEXING_LOCK_TIMEOUT
|
||||
from onyx.configs.constants import MilestoneRecordType
|
||||
@@ -61,6 +64,7 @@ from onyx.db.connector_credential_pair import (
|
||||
)
|
||||
from onyx.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
from onyx.db.connector_credential_pair import set_cc_pair_repeated_error_state
|
||||
from onyx.db.connector_credential_pair import update_connector_credential_pair_from_id
|
||||
from onyx.db.engine.sql_engine import get_session_with_current_tenant
|
||||
from onyx.db.engine.time_utils import get_db_current_time
|
||||
from onyx.db.enums import ConnectorCredentialPairStatus
|
||||
@@ -83,7 +87,6 @@ from onyx.db.models import SearchSettings
|
||||
from onyx.db.search_settings import get_current_search_settings
|
||||
from onyx.db.search_settings import get_secondary_search_settings
|
||||
from onyx.db.swap_index import check_and_perform_index_swap
|
||||
from onyx.db.usage import UsageLimitExceededError
|
||||
from onyx.document_index.factory import get_default_document_index
|
||||
from onyx.file_store.document_batch_storage import DocumentBatchStorage
|
||||
from onyx.file_store.document_batch_storage import get_document_batch_storage
|
||||
@@ -828,16 +831,39 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
|
||||
for cc_pair_id in primary_cc_pair_ids:
|
||||
lock_beat.reacquire()
|
||||
|
||||
if is_in_repeated_error_state(
|
||||
cc_pair_id=cc_pair_id,
|
||||
search_settings_id=current_search_settings.id,
|
||||
cc_pair = get_connector_credential_pair_from_id(
|
||||
db_session=db_session,
|
||||
cc_pair_id=cc_pair_id,
|
||||
)
|
||||
|
||||
# if already in repeated error state, don't do anything
|
||||
# this is important so that we don't keep pausing the connector
|
||||
# immediately upon a user un-pausing it to manually re-trigger and
|
||||
# recover.
|
||||
if (
|
||||
cc_pair
|
||||
and not cc_pair.in_repeated_error_state
|
||||
and is_in_repeated_error_state(
|
||||
cc_pair=cc_pair,
|
||||
search_settings_id=current_search_settings.id,
|
||||
db_session=db_session,
|
||||
)
|
||||
):
|
||||
set_cc_pair_repeated_error_state(
|
||||
db_session=db_session,
|
||||
cc_pair_id=cc_pair_id,
|
||||
in_repeated_error_state=True,
|
||||
)
|
||||
# When entering repeated error state, also pause the connector
|
||||
# to prevent continued indexing retry attempts burning through embedding credits.
|
||||
# NOTE: only for Cloud, since most self-hosted users use self-hosted embedding
|
||||
# models. Also, they are more prone to repeated failures -> eventual success.
|
||||
if AUTH_TYPE == AuthType.CLOUD:
|
||||
update_connector_credential_pair_from_id(
|
||||
db_session=db_session,
|
||||
cc_pair_id=cc_pair.id,
|
||||
status=ConnectorCredentialPairStatus.PAUSED,
|
||||
)
|
||||
|
||||
# NOTE: At this point, we haven't done heavy checks on whether or not the CC pairs should actually be indexed
|
||||
# Heavy check, should_index(), is called in _kickoff_indexing_tasks
|
||||
@@ -1270,19 +1296,14 @@ def _check_chunk_usage_limit(tenant_id: str) -> None:
|
||||
if not USAGE_LIMITS_ENABLED:
|
||||
return
|
||||
|
||||
from onyx.db.usage import check_usage_limit
|
||||
from onyx.db.usage import UsageType
|
||||
from onyx.server.usage_limits import get_limit_for_usage_type
|
||||
from onyx.server.usage_limits import is_tenant_on_trial_fn
|
||||
|
||||
is_trial = is_tenant_on_trial_fn(tenant_id)
|
||||
limit = get_limit_for_usage_type(UsageType.CHUNKS_INDEXED, is_trial, tenant_id)
|
||||
from onyx.server.usage_limits import check_usage_and_raise
|
||||
|
||||
with get_session_with_current_tenant() as db_session:
|
||||
check_usage_limit(
|
||||
check_usage_and_raise(
|
||||
db_session=db_session,
|
||||
usage_type=UsageType.CHUNKS_INDEXED,
|
||||
limit=limit,
|
||||
tenant_id=tenant_id,
|
||||
pending_amount=0, # Just check current usage
|
||||
)
|
||||
|
||||
@@ -1302,7 +1323,7 @@ def _docprocessing_task(
|
||||
if USAGE_LIMITS_ENABLED:
|
||||
try:
|
||||
_check_chunk_usage_limit(tenant_id)
|
||||
except UsageLimitExceededError as e:
|
||||
except HTTPException as e:
|
||||
# Log the error and fail the indexing attempt
|
||||
task_logger.error(
|
||||
f"Chunk indexing usage limit exceeded for tenant {tenant_id}: {e}"
|
||||
|
||||
@@ -10,7 +10,6 @@ from sqlalchemy.orm import Session
|
||||
from onyx.configs.app_configs import DISABLE_INDEX_UPDATE_ON_SWAP
|
||||
from onyx.configs.constants import CELERY_GENERIC_BEAT_LOCK_TIMEOUT
|
||||
from onyx.configs.constants import DocumentSource
|
||||
from onyx.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
from onyx.db.engine.time_utils import get_db_current_time
|
||||
from onyx.db.enums import ConnectorCredentialPairStatus
|
||||
from onyx.db.enums import IndexingStatus
|
||||
@@ -126,18 +125,9 @@ class IndexingCallback(IndexingHeartbeatInterface):
|
||||
|
||||
|
||||
def is_in_repeated_error_state(
|
||||
cc_pair_id: int, search_settings_id: int, db_session: Session
|
||||
cc_pair: ConnectorCredentialPair, search_settings_id: int, db_session: Session
|
||||
) -> bool:
|
||||
"""Checks if the cc pair / search setting combination is in a repeated error state."""
|
||||
cc_pair = get_connector_credential_pair_from_id(
|
||||
db_session=db_session,
|
||||
cc_pair_id=cc_pair_id,
|
||||
)
|
||||
if not cc_pair:
|
||||
raise RuntimeError(
|
||||
f"is_in_repeated_error_state - could not find cc_pair with id={cc_pair_id}"
|
||||
)
|
||||
|
||||
# if the connector doesn't have a refresh_freq, a single failed attempt is enough
|
||||
number_of_failed_attempts_in_a_row_needed = (
|
||||
NUM_REPEAT_ERRORS_BEFORE_REPEATED_ERROR_STATE
|
||||
@@ -146,7 +136,7 @@ def is_in_repeated_error_state(
|
||||
)
|
||||
|
||||
most_recent_index_attempts = get_recent_attempts_for_cc_pair(
|
||||
cc_pair_id=cc_pair_id,
|
||||
cc_pair_id=cc_pair.id,
|
||||
search_settings_id=search_settings_id,
|
||||
limit=number_of_failed_attempts_in_a_row_needed,
|
||||
db_session=db_session,
|
||||
@@ -180,7 +170,7 @@ def should_index(
|
||||
db_session=db_session,
|
||||
)
|
||||
all_recent_errored = is_in_repeated_error_state(
|
||||
cc_pair_id=cc_pair.id,
|
||||
cc_pair=cc_pair,
|
||||
search_settings_id=search_settings_instance.id,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
@@ -961,14 +961,20 @@ def get_user_email_from_username__server(
|
||||
try:
|
||||
response = confluence_client.get_mobile_parameters(user_name)
|
||||
email = response.get("email")
|
||||
except Exception:
|
||||
logger.warning(f"failed to get confluence email for {user_name}")
|
||||
except HTTPError as e:
|
||||
status_code = e.response.status_code if e.response is not None else "N/A"
|
||||
logger.warning(
|
||||
f"Failed to get confluence email for {user_name}: "
|
||||
f"HTTP {status_code} - {e}"
|
||||
)
|
||||
# For now, we'll just return None and log a warning. This means
|
||||
# we will keep retrying to get the email every group sync.
|
||||
email = None
|
||||
# We may want to just return a string that indicates failure so we dont
|
||||
# keep retrying
|
||||
# email = f"FAILED TO GET CONFLUENCE EMAIL FOR {user_name}"
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to get confluence email for {user_name}: {type(e).__name__} - {e}"
|
||||
)
|
||||
email = None
|
||||
_USER_EMAIL_CACHE[user_name] = email
|
||||
return _USER_EMAIL_CACHE[user_name]
|
||||
|
||||
|
||||
@@ -14,9 +14,7 @@ from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from onyx.configs.app_configs import AUTH_TYPE
|
||||
from onyx.configs.app_configs import DISABLE_AUTH
|
||||
from onyx.configs.constants import AuthType
|
||||
from onyx.configs.constants import DocumentSource
|
||||
from onyx.db.connector import fetch_connector_by_id
|
||||
from onyx.db.credentials import fetch_credential_by_id
|
||||
@@ -428,27 +426,10 @@ def set_cc_pair_repeated_error_state(
|
||||
cc_pair_id: int,
|
||||
in_repeated_error_state: bool,
|
||||
) -> None:
|
||||
values: dict = {"in_repeated_error_state": in_repeated_error_state}
|
||||
|
||||
# When entering repeated error state, also pause the connector
|
||||
# to prevent continued indexing retry attempts burning through embedding credits.
|
||||
# However, don't pause if there's an active manual indexing trigger,
|
||||
# which indicates the user wants to retry immediately.
|
||||
# NOTE: only for Cloud, since most self-hosted users use self-hosted embedding
|
||||
# models. Also, they are more prone to repeated failures -> eventual success.
|
||||
if in_repeated_error_state and AUTH_TYPE == AuthType.CLOUD:
|
||||
cc_pair = get_connector_credential_pair_from_id(
|
||||
db_session=db_session,
|
||||
cc_pair_id=cc_pair_id,
|
||||
)
|
||||
# Only pause if there's no manual indexing trigger active
|
||||
if cc_pair and cc_pair.indexing_trigger is None:
|
||||
values["status"] = ConnectorCredentialPairStatus.PAUSED
|
||||
|
||||
stmt = (
|
||||
update(ConnectorCredentialPair)
|
||||
.where(ConnectorCredentialPair.id == cc_pair_id)
|
||||
.values(**values)
|
||||
.values(in_repeated_error_state=in_repeated_error_state)
|
||||
)
|
||||
db_session.execute(stmt)
|
||||
db_session.commit()
|
||||
|
||||
@@ -52,7 +52,7 @@ def create_or_add_document_tag(
|
||||
is_list=False,
|
||||
)
|
||||
insert_stmt = insert_stmt.on_conflict_do_nothing(
|
||||
index_elements=["tag_key", "tag_value", "source", "is_list"]
|
||||
constraint="_tag_key_value_source_list_uc"
|
||||
)
|
||||
db_session.execute(insert_stmt)
|
||||
|
||||
@@ -98,7 +98,7 @@ def create_or_add_document_tag_list(
|
||||
is_list=True,
|
||||
)
|
||||
insert_stmt = insert_stmt.on_conflict_do_nothing(
|
||||
index_elements=["tag_key", "tag_value", "source", "is_list"]
|
||||
constraint="_tag_key_value_source_list_uc"
|
||||
)
|
||||
db_session.execute(insert_stmt)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
from collections.abc import AsyncGenerator
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
from uuid import UUID
|
||||
@@ -103,6 +105,7 @@ from onyx.server.utils import PUBLIC_API_TAGS
|
||||
from onyx.utils.headers import get_custom_tool_additional_request_headers
|
||||
from onyx.utils.logger import setup_logger
|
||||
from onyx.utils.telemetry import mt_cloud_telemetry
|
||||
from onyx.utils.threadpool_concurrency import run_in_background
|
||||
from shared_configs.contextvars import get_current_tenant_id
|
||||
|
||||
logger = setup_logger()
|
||||
@@ -507,7 +510,7 @@ def handle_new_chat_message(
|
||||
|
||||
|
||||
@router.post("/send-chat-message", response_model=None, tags=PUBLIC_API_TAGS)
|
||||
def handle_send_chat_message(
|
||||
async def handle_send_chat_message(
|
||||
chat_message_req: SendMessageRequest,
|
||||
request: Request,
|
||||
user: User | None = Depends(current_chat_accessible_user),
|
||||
@@ -572,34 +575,63 @@ def handle_send_chat_message(
|
||||
# Note: LLM cost tracking is now handled in multi_llm.py
|
||||
return result
|
||||
|
||||
# Streaming path, normal Onyx UI behavior
|
||||
def stream_generator() -> Generator[str, None, None]:
|
||||
# Use prod-cons pattern to continue processing even if request stops yielding
|
||||
buffer: asyncio.Queue[str | None] = asyncio.Queue()
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Capture headers before spawning thread
|
||||
litellm_headers = extract_headers(request.headers, LITELLM_PASS_THROUGH_HEADERS)
|
||||
custom_tool_headers = get_custom_tool_additional_request_headers(request.headers)
|
||||
|
||||
def producer() -> None:
|
||||
"""
|
||||
Producer function that runs handle_stream_message_objects in a loop
|
||||
and writes results to the buffer.
|
||||
"""
|
||||
state_container = ChatStateContainer()
|
||||
try:
|
||||
logger.debug("Producer started")
|
||||
with get_session_with_current_tenant() as db_session:
|
||||
for obj in handle_stream_message_objects(
|
||||
new_msg_req=chat_message_req,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
litellm_additional_headers=extract_headers(
|
||||
request.headers, LITELLM_PASS_THROUGH_HEADERS
|
||||
),
|
||||
custom_tool_additional_headers=get_custom_tool_additional_request_headers(
|
||||
request.headers
|
||||
),
|
||||
litellm_additional_headers=litellm_headers,
|
||||
custom_tool_additional_headers=custom_tool_headers,
|
||||
external_state_container=state_container,
|
||||
):
|
||||
yield get_json_line(obj.model_dump())
|
||||
# Thread-safe put into the asyncio queue
|
||||
loop.call_soon_threadsafe(
|
||||
buffer.put_nowait, get_json_line(obj.model_dump())
|
||||
)
|
||||
# Note: LLM cost tracking is now handled in multi_llm.py
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Error in chat message streaming")
|
||||
yield json.dumps({"error": str(e)})
|
||||
|
||||
loop.call_soon_threadsafe(buffer.put_nowait, json.dumps({"error": str(e)}))
|
||||
finally:
|
||||
logger.debug("Stream generator finished")
|
||||
# Signal end of stream
|
||||
loop.call_soon_threadsafe(buffer.put_nowait, None)
|
||||
logger.debug("Producer finished")
|
||||
|
||||
return StreamingResponse(stream_generator(), media_type="text/event-stream")
|
||||
async def stream_from_buffer() -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
Async generator that reads from the buffer and yields to the client.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
item = await buffer.get()
|
||||
if item is None:
|
||||
# End of stream signal
|
||||
break
|
||||
yield item
|
||||
except asyncio.CancelledError:
|
||||
logger.warning("Stream cancelled (Consumer disconnected)")
|
||||
finally:
|
||||
logger.debug("Stream consumer finished")
|
||||
|
||||
run_in_background(producer)
|
||||
|
||||
return StreamingResponse(stream_from_buffer(), media_type="text/event-stream")
|
||||
|
||||
|
||||
@router.put("/set-message-as-latest")
|
||||
|
||||
@@ -54,7 +54,7 @@ attrs==25.4.0
|
||||
# jsonschema
|
||||
# referencing
|
||||
# zeep
|
||||
authlib==1.6.5
|
||||
authlib==1.6.6
|
||||
# via fastmcp
|
||||
babel==2.17.0
|
||||
# via courlan
|
||||
@@ -1248,7 +1248,7 @@ websockets==15.0.1
|
||||
# via
|
||||
# fastmcp
|
||||
# google-genai
|
||||
werkzeug==3.1.4
|
||||
werkzeug==3.1.5
|
||||
# via sendgrid
|
||||
wrapt==1.17.3
|
||||
# via
|
||||
|
||||
@@ -1,142 +1,70 @@
|
||||
# Helm Chart for Onyx
|
||||
# Dependency updates (when subchart versions are bumped)
|
||||
* If updating subcharts, you need to run this before committing!
|
||||
* cd charts/onyx
|
||||
* helm dependency update .
|
||||
|
||||
## Updating Dependencies
|
||||
# Local testing
|
||||
|
||||
When subchart versions are bumped, rebuild the dependency lock file before committing:
|
||||
## One time setup
|
||||
* brew install kind
|
||||
* Ensure you have no config at ~/.kube/config
|
||||
* kind create cluster
|
||||
* mv ~/.kube/config ~/.kube/kind-config
|
||||
|
||||
```bash
|
||||
cd deployment/helm/charts/onyx
|
||||
helm dependency update .
|
||||
```
|
||||
## Automated install and test with ct
|
||||
* export KUBECONFIG=~/.kube/kind-config
|
||||
* kubectl config use-context kind-kind
|
||||
* from source root run the following. This does a very basic test against the web server
|
||||
* ct install --all --helm-extra-set-args="--set=nginx.enabled=false" --debug --config ct.yaml
|
||||
|
||||
---
|
||||
## Output template to file and inspect
|
||||
* cd charts/onyx
|
||||
* helm template test-output . > test-output.yaml
|
||||
|
||||
# Local Testing with Kind
|
||||
## Test the entire cluster manually
|
||||
* cd charts/onyx
|
||||
* helm install onyx . -n onyx --set postgresql.primary.persistence.enabled=false
|
||||
* the postgres flag is to keep the storage ephemeral for testing. You probably don't want to set that in prod.
|
||||
* no flag for ephemeral vespa storage yet, might be good for testing
|
||||
* kubectl -n onyx port-forward service/onyx-nginx 8080:80
|
||||
* this will forward the local port 8080 to the installed chart for you to run tests, etc.
|
||||
* When you are finished
|
||||
* helm uninstall onyx -n onyx
|
||||
* Vespa leaves behind a PVC. Delete it if you are completely done.
|
||||
* k -n onyx get pvc
|
||||
* k -n onyx delete pvc vespa-storage-da-vespa-0
|
||||
* If you didn't disable Postgres persistence earlier, you may want to delete that PVC too.
|
||||
|
||||
## One-Time Setup
|
||||
## Run as non-root user
|
||||
By default, some onyx containers run as root. If you'd like to explicitly run the onyx containers as a non-root user, update the values.yaml file for the following components:
|
||||
* `celery_shared`, `api`, `webserver`, `indexCapability`, `inferenceCapability`
|
||||
```yaml
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
```
|
||||
* `vespa`
|
||||
```yaml
|
||||
podSecurityContext:
|
||||
fsGroup: 1000
|
||||
securityContext:
|
||||
privileged: false
|
||||
runAsUser: 1000
|
||||
```
|
||||
|
||||
Install [kind](https://kind.sigs.k8s.io/) (Kubernetes in Docker) and create a local cluster:
|
||||
## Resourcing
|
||||
In the helm charts, we have resource suggestions for all Onyx-owned components.
|
||||
These are simply initial suggestions, and may need to be tuned for your specific use case.
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install kind
|
||||
Please talk to us in Slack if you have any questions!
|
||||
|
||||
# Linux (amd64) - see https://kind.sigs.k8s.io/docs/user/quick-start for other architectures
|
||||
curl -Lo ./kind https://kind.sigs.k8s.io/releases/latest/download/kind-linux-amd64
|
||||
chmod +x ./kind
|
||||
sudo mv ./kind /usr/local/bin/kind
|
||||
```
|
||||
## Autoscaling options
|
||||
The chart renders Kubernetes HorizontalPodAutoscalers by default. To keep this behavior, leave
|
||||
`autoscaling.engine` as `hpa` and adjust the per-component `autoscaling.*` values as needed.
|
||||
|
||||
Create a cluster:
|
||||
If you would like to use KEDA ScaledObjects instead:
|
||||
|
||||
```bash
|
||||
kind create cluster --name onyx
|
||||
```
|
||||
1. Install and manage the KEDA operator in your cluster yourself (for example via the official KEDA Helm chart). KEDA is no longer packaged as a dependency of the Onyx chart.
|
||||
2. Set `autoscaling.engine: keda` in your `values.yaml` and enable autoscaling for the components you want to scale.
|
||||
|
||||
## Automated Testing with chart-testing (ct)
|
||||
|
||||
From the repo root, run the chart-testing tool:
|
||||
|
||||
```bash
|
||||
ct install --all --helm-extra-set-args="--set=nginx.enabled=false" --debug --config ct.yaml
|
||||
```
|
||||
|
||||
> **Note:** nginx is disabled because kind lacks LoadBalancer support.
|
||||
|
||||
## Render Templates Locally
|
||||
|
||||
Preview the rendered Kubernetes manifests without installing:
|
||||
|
||||
```bash
|
||||
cd deployment/helm/charts/onyx
|
||||
helm template test-output . > test-output.yaml
|
||||
```
|
||||
|
||||
## Manual Cluster Testing
|
||||
|
||||
Install the chart into your kind cluster:
|
||||
|
||||
```bash
|
||||
cd deployment/helm/charts/onyx
|
||||
helm install onyx . \
|
||||
--namespace onyx \
|
||||
--create-namespace \
|
||||
--set=postgresql.nameOverride=cloudnative-pg
|
||||
```
|
||||
|
||||
Forward the nginx service to access the UI locally:
|
||||
|
||||
```bash
|
||||
kubectl -n onyx port-forward service/onyx-nginx 8080:80
|
||||
```
|
||||
|
||||
Then open http://localhost:8080 in your browser.
|
||||
|
||||
### Cleanup
|
||||
|
||||
Uninstall the release:
|
||||
|
||||
```bash
|
||||
helm uninstall onyx -n onyx
|
||||
```
|
||||
|
||||
PVCs are not automatically deleted. Remove them if you're done testing:
|
||||
|
||||
```bash
|
||||
kubectl -n onyx delete pvc --all
|
||||
```
|
||||
|
||||
Or delete the entire namespace:
|
||||
|
||||
```bash
|
||||
kubectl delete namespace onyx
|
||||
```
|
||||
|
||||
To tear down the kind cluster entirely:
|
||||
|
||||
```bash
|
||||
kind delete cluster --name onyx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
## Running as Non-Root
|
||||
|
||||
By default, some containers run as root. To run as a non-root user, add to your `values.yaml`:
|
||||
|
||||
For `api`, `webserver`, `indexCapability`, `inferenceCapability`, and `celery_shared`:
|
||||
|
||||
```yaml
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1001
|
||||
```
|
||||
|
||||
For `vespa`:
|
||||
|
||||
```yaml
|
||||
podSecurityContext:
|
||||
fsGroup: 1000
|
||||
securityContext:
|
||||
privileged: false
|
||||
runAsUser: 1000
|
||||
```
|
||||
|
||||
## Resource Tuning
|
||||
|
||||
The chart includes resource requests/limits for all Onyx components. These are starting points—tune them based on your workload and cluster capacity.
|
||||
|
||||
## Autoscaling
|
||||
|
||||
The chart renders **HorizontalPodAutoscalers** by default (`autoscaling.engine: hpa`).
|
||||
|
||||
To use **KEDA ScaledObjects** instead:
|
||||
|
||||
1. Install the [KEDA operator](https://keda.sh/) separately (it's not bundled with this chart)
|
||||
2. Set in your `values.yaml`:
|
||||
```yaml
|
||||
autoscaling:
|
||||
engine: keda
|
||||
```
|
||||
When `autoscaling.engine` is set to `keda`, the chart will render the existing ScaledObject templates; otherwise HPAs will be rendered.
|
||||
|
||||
12
uv.lock
generated
12
uv.lock
generated
@@ -347,14 +347,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.6.5"
|
||||
version = "1.6.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6702,14 +6702,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.1.4"
|
||||
version = "3.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
20
web/lib/opal/src/icons/empty.tsx
Normal file
20
web/lib/opal/src/icons/empty.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { IconProps } from "@opal/types";
|
||||
const SvgEmpty = ({ 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="M14 10V12.6667C14 13.3929 13.3929 14 12.6667 14H3.33333C2.60711 14 2 13.3929 2 12.6667V10M8 2V5M13.5 4.5L11.5 6.5M2.5 4.5L4.5 6.5"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default SvgEmpty;
|
||||
@@ -6,33 +6,36 @@ export { default as SvgAlertCircle } from "@opal/icons/alert-circle";
|
||||
export { default as SvgAlertTriangle } from "@opal/icons/alert-triangle";
|
||||
export { default as SvgArrowDownDot } from "@opal/icons/arrow-down-dot";
|
||||
export { default as SvgArrowExchange } from "@opal/icons/arrow-exchange";
|
||||
export { default as SvgArrowLeftDot } from "@opal/icons/arrow-left-dot";
|
||||
export { default as SvgArrowLeft } from "@opal/icons/arrow-left";
|
||||
export { default as SvgArrowLeftDot } from "@opal/icons/arrow-left-dot";
|
||||
export { default as SvgArrowRight } from "@opal/icons/arrow-right";
|
||||
export { default as SvgArrowRightCircle } from "@opal/icons/arrow-right-circle";
|
||||
export { default as SvgArrowRightDot } from "@opal/icons/arrow-right-dot";
|
||||
export { default as SvgArrowRight } from "@opal/icons/arrow-right";
|
||||
export { default as SvgArrowUp } from "@opal/icons/arrow-up";
|
||||
export { default as SvgArrowUpDot } from "@opal/icons/arrow-up-dot";
|
||||
export { default as SvgArrowUpRight } from "@opal/icons/arrow-up-right";
|
||||
export { default as SvgArrowUp } from "@opal/icons/arrow-up";
|
||||
export { default as SvgArrowWallRight } from "@opal/icons/arrow-wall-right";
|
||||
export { default as SvgAudioEqSmall } from "@opal/icons/audio-eq-small";
|
||||
export { default as SvgAws } from "@opal/icons/aws";
|
||||
export { default as SvgBarChart } from "@opal/icons/bar-chart";
|
||||
export { default as SvgBarChartSmall } from "@opal/icons/bar-chart-small";
|
||||
export { default as SvgBell } from "@opal/icons/bell";
|
||||
export { default as SvgBookOpen } from "@opal/icons/book-open";
|
||||
export { default as SvgBooksLineSmall } from "@opal/icons/books-line-small";
|
||||
export { default as SvgBooksStackSmall } from "@opal/icons/books-stack-small";
|
||||
export { default as SvgBracketCurly } from "@opal/icons/bracket-curly";
|
||||
export { default as SvgBubbleText } from "@opal/icons/bubble-text";
|
||||
export { default as SvgCalendar } from "@opal/icons/calendar";
|
||||
export { default as SvgCheckCircle } from "@opal/icons/check-circle";
|
||||
export { default as SvgCheckSquare } from "@opal/icons/check-square";
|
||||
export { default as SvgCheck } from "@opal/icons/check";
|
||||
export { default as SvgCheckCircle } from "@opal/icons/check-circle";
|
||||
export { default as SvgCheckSmall } from "@opal/icons/check-small";
|
||||
export { default as SvgChevronDownSmall } from "@opal/icons/chevron-down-small";
|
||||
export { default as SvgCheckSquare } from "@opal/icons/check-square";
|
||||
export { default as SvgChevronDown } from "@opal/icons/chevron-down";
|
||||
export { default as SvgChevronDownSmall } from "@opal/icons/chevron-down-small";
|
||||
export { default as SvgChevronLeft } from "@opal/icons/chevron-left";
|
||||
export { default as SvgChevronRight } from "@opal/icons/chevron-right";
|
||||
export { default as SvgChevronUpSmall } from "@opal/icons/chevron-up-small";
|
||||
export { default as SvgChevronUp } from "@opal/icons/chevron-up";
|
||||
export { default as SvgChevronUpSmall } from "@opal/icons/chevron-up-small";
|
||||
export { default as SvgClaude } from "@opal/icons/claude";
|
||||
export { default as SvgClipboard } from "@opal/icons/clipboard";
|
||||
export { default as SvgClock } from "@opal/icons/clock";
|
||||
@@ -44,28 +47,26 @@ export { default as SvgCornerRightUpDot } from "@opal/icons/corner-right-up-dot"
|
||||
export { default as SvgCpu } from "@opal/icons/cpu";
|
||||
export { default as SvgDevKit } from "@opal/icons/dev-kit";
|
||||
export { default as SvgDownloadCloud } from "@opal/icons/download-cloud";
|
||||
export { default as SvgEditBig } from "@opal/icons/edit-big";
|
||||
export { default as SvgExpand } from "@opal/icons/expand";
|
||||
export { default as SvgEdit } from "@opal/icons/edit";
|
||||
export { default as SvgEditBig } from "@opal/icons/edit-big";
|
||||
export { default as SvgEmpty } from "@opal/icons/empty";
|
||||
export { default as SvgExpand } from "@opal/icons/expand";
|
||||
export { default as SvgExternalLink } from "@opal/icons/external-link";
|
||||
export { default as SvgEyeClosed } from "@opal/icons/eye-closed";
|
||||
export { default as SvgEye } from "@opal/icons/eye";
|
||||
export { default as SvgEyeClosed } from "@opal/icons/eye-closed";
|
||||
export { default as SvgFiles } from "@opal/icons/files";
|
||||
export { default as SvgFileSmall } from "@opal/icons/file-small";
|
||||
export { default as SvgFileText } from "@opal/icons/file-text";
|
||||
export { default as SvgFiles } from "@opal/icons/files";
|
||||
export { default as SvgFilter } from "@opal/icons/filter";
|
||||
export { default as SvgFold } from "@opal/icons/fold";
|
||||
export { default as SvgFolder } from "@opal/icons/folder";
|
||||
export { default as SvgFolderIn } from "@opal/icons/folder-in";
|
||||
export { default as SvgFolderOpen } from "@opal/icons/folder-open";
|
||||
export { default as SvgFolderPartialOpen } from "@opal/icons/folder-partial-open";
|
||||
export { default as SvgFolderPlus } from "@opal/icons/folder-plus";
|
||||
export { default as SvgFolder } from "@opal/icons/folder";
|
||||
export { default as SvgGlobe } from "@opal/icons/globe";
|
||||
export { default as SvgBookOpen } from "@opal/icons/book-open";
|
||||
export { default as SvgBooksLineSmall } from "@opal/icons/books-line-small";
|
||||
export { default as SvgBooksStackSmall } from "@opal/icons/books-stack-small";
|
||||
export { default as SvgHashSmall } from "@opal/icons/hash-small";
|
||||
export { default as SvgHardDrive } from "@opal/icons/hard-drive";
|
||||
export { default as SvgHashSmall } from "@opal/icons/hash-small";
|
||||
export { default as SvgHeadsetMic } from "@opal/icons/headset-mic";
|
||||
export { default as SvgHourglass } from "@opal/icons/hourglass";
|
||||
export { default as SvgImage } from "@opal/icons/image";
|
||||
@@ -81,18 +82,21 @@ export { default as SvgLoader } from "@opal/icons/loader";
|
||||
export { default as SvgLock } from "@opal/icons/lock";
|
||||
export { default as SvgLogOut } from "@opal/icons/log-out";
|
||||
export { default as SvgMaximize2 } from "@opal/icons/maximize-2";
|
||||
export { default as SvgMcp } from "@opal/icons/mcp";
|
||||
export { default as SvgMenu } from "@opal/icons/menu";
|
||||
export { default as SvgMinusCircle } from "@opal/icons/minus-circle";
|
||||
export { default as SvgMinus } from "@opal/icons/minus";
|
||||
export { default as SvgMinusCircle } from "@opal/icons/minus-circle";
|
||||
export { default as SvgMoon } from "@opal/icons/moon";
|
||||
export { default as SvgMoreHorizontal } from "@opal/icons/more-horizontal";
|
||||
export { default as SvgMusicSmall } from "@opal/icons/music-small";
|
||||
export { default as SvgNotificationBubble } from "@opal/icons/notification-bubble";
|
||||
export { default as SvgOllama } from "@opal/icons/ollama";
|
||||
export { default as SvgOnyxLogo } from "@opal/icons/onyx-logo";
|
||||
export { default as SvgOnyxOctagon } from "@opal/icons/onyx-octagon";
|
||||
export { default as SvgOpenai } from "@opal/icons/openai";
|
||||
export { default as SvgOpenrouter } from "@opal/icons/openrouter";
|
||||
export { default as SvgOrganization } from "@opal/icons/organization";
|
||||
export { default as SvgPaintBrush } from "@opal/icons/paint-brush";
|
||||
export { default as SvgPaperclip } from "@opal/icons/paperclip";
|
||||
export { default as SvgPauseCircle } from "@opal/icons/pause-circle";
|
||||
export { default as SvgPenSmall } from "@opal/icons/pen-small";
|
||||
@@ -101,14 +105,15 @@ export { default as SvgPin } from "@opal/icons/pin";
|
||||
export { default as SvgPinned } from "@opal/icons/pinned";
|
||||
export { default as SvgPlayCircle } from "@opal/icons/play-circle";
|
||||
export { default as SvgPlug } from "@opal/icons/plug";
|
||||
export { default as SvgPlusCircle } from "@opal/icons/plus-circle";
|
||||
export { default as SvgPlus } from "@opal/icons/plus";
|
||||
export { default as SvgPlusCircle } from "@opal/icons/plus-circle";
|
||||
export { default as SvgQuestionMarkSmall } from "@opal/icons/question-mark-small";
|
||||
export { default as SvgQuoteEnd } from "@opal/icons/quote-end";
|
||||
export { default as SvgQuoteStart } from "@opal/icons/quote-start";
|
||||
export { default as SvgRefreshCw } from "@opal/icons/refresh-cw";
|
||||
export { default as SvgSearchMenu } from "@opal/icons/search-menu";
|
||||
export { default as SvgRevert } from "@opal/icons/revert";
|
||||
export { default as SvgSearch } from "@opal/icons/search";
|
||||
export { default as SvgSearchMenu } from "@opal/icons/search-menu";
|
||||
export { default as SvgSearchSmall } from "@opal/icons/search-small";
|
||||
export { default as SvgServer } from "@opal/icons/server";
|
||||
export { default as SvgSettings } from "@opal/icons/settings";
|
||||
@@ -122,10 +127,10 @@ export { default as SvgSlidersSmall } from "@opal/icons/sliders-small";
|
||||
export { default as SvgSparkle } from "@opal/icons/sparkle";
|
||||
export { default as SvgStep1 } from "@opal/icons/step1";
|
||||
export { default as SvgStep2 } from "@opal/icons/step2";
|
||||
export { default as SvgStep3End } from "@opal/icons/step3-end";
|
||||
export { default as SvgStep3 } from "@opal/icons/step3";
|
||||
export { default as SvgStopCircle } from "@opal/icons/stop-circle";
|
||||
export { default as SvgStep3End } from "@opal/icons/step3-end";
|
||||
export { default as SvgStop } from "@opal/icons/stop";
|
||||
export { default as SvgStopCircle } from "@opal/icons/stop-circle";
|
||||
export { default as SvgSun } from "@opal/icons/sun";
|
||||
export { default as SvgTerminalSmall } from "@opal/icons/terminal-small";
|
||||
export { default as SvgTextLinesSmall } from "@opal/icons/text-lines-small";
|
||||
@@ -135,17 +140,13 @@ export { default as SvgTrash } from "@opal/icons/trash";
|
||||
export { default as SvgTwoLineSmall } from "@opal/icons/two-line-small";
|
||||
export { default as SvgUnplug } from "@opal/icons/unplug";
|
||||
export { default as SvgUploadCloud } from "@opal/icons/upload-cloud";
|
||||
export { default as SvgUserPlus } from "@opal/icons/user-plus";
|
||||
export { default as SvgUser } from "@opal/icons/user";
|
||||
export { default as SvgUserPlus } from "@opal/icons/user-plus";
|
||||
export { default as SvgUsers } from "@opal/icons/users";
|
||||
export { default as SvgWallet } from "@opal/icons/wallet";
|
||||
export { default as SvgWorkflow } from "@opal/icons/workflow";
|
||||
export { default as SvgX } from "@opal/icons/x";
|
||||
export { default as SvgXCircle } from "@opal/icons/x-circle";
|
||||
export { default as SvgXOctagon } from "@opal/icons/x-octagon";
|
||||
export { default as SvgX } from "@opal/icons/x";
|
||||
export { default as SvgZoomIn } from "@opal/icons/zoom-in";
|
||||
export { default as SvgZoomOut } from "@opal/icons/zoom-out";
|
||||
export { default as SvgMcp } from "@opal/icons/mcp";
|
||||
export { default as SvgPaintBrush } from "@opal/icons/paint-brush";
|
||||
export { default as SvgRevert } from "@opal/icons/revert";
|
||||
export { default as SvgNotificationBubble } from "@opal/icons/notification-bubble";
|
||||
|
||||
@@ -19,7 +19,7 @@ import ChatInputBar, {
|
||||
ChatInputBarHandle,
|
||||
} from "@/app/chat/components/input/ChatInputBar";
|
||||
import useChatSessions from "@/hooks/useChatSessions";
|
||||
import { useCCPairs } from "@/lib/hooks/useCCPairs";
|
||||
import useCCPairs from "@/hooks/useCCPairs";
|
||||
import { useTags } from "@/lib/hooks/useTags";
|
||||
import { useDocumentSets } from "@/lib/hooks/useDocumentSets";
|
||||
import { useAgents } from "@/hooks/useAgents";
|
||||
@@ -209,7 +209,7 @@ export default function ChatPage({ firstMessage }: ChatPageProps) {
|
||||
const noAssistants = liveAssistant === null || liveAssistant === undefined;
|
||||
|
||||
const availableSources: ValidSources[] = useMemo(() => {
|
||||
return (ccPairs ?? []).map((ccPair) => ccPair.source);
|
||||
return ccPairs.map((ccPair) => ccPair.source);
|
||||
}, [ccPairs]);
|
||||
|
||||
const sources: SourceMetadata[] = useMemo(() => {
|
||||
|
||||
@@ -11,8 +11,8 @@ import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
|
||||
import LLMPopover from "@/refresh-components/popovers/LLMPopover";
|
||||
import { InputPrompt } from "@/app/chat/interfaces";
|
||||
import { FilterManager, LlmManager, useFederatedConnectors } from "@/lib/hooks";
|
||||
import { useInputPrompts } from "@/lib/hooks/useInputPrompts";
|
||||
import { useCCPairs } from "@/lib/hooks/useCCPairs";
|
||||
import usePromptShortcuts from "@/hooks/usePromptShortcuts";
|
||||
import useCCPairs from "@/hooks/useCCPairs";
|
||||
import { DocumentIcon2, FileIcon } from "@/components/icons/icons";
|
||||
import { OnyxDocument, MinimalOnyxDocument } from "@/lib/search/interfaces";
|
||||
import { ChatState } from "@/app/chat/interfaces";
|
||||
@@ -256,7 +256,7 @@ const ChatInputBar = React.memo(
|
||||
[setCurrentMessageFiles]
|
||||
);
|
||||
|
||||
const { inputPrompts } = useInputPrompts();
|
||||
const { promptShortcuts } = usePromptShortcuts();
|
||||
const { ccPairs, isLoading: ccPairsLoading } = useCCPairs();
|
||||
const { data: federatedConnectorsData, isLoading: federatedLoading } =
|
||||
useFederatedConnectors();
|
||||
@@ -272,7 +272,7 @@ const ChatInputBar = React.memo(
|
||||
// Memoize availableSources to prevent unnecessary re-renders
|
||||
const memoizedAvailableSources = useMemo(
|
||||
() => [
|
||||
...(ccPairs ?? []).map((ccPair) => ccPair.source),
|
||||
...ccPairs.map((ccPair) => ccPair.source),
|
||||
...(federatedConnectorsData?.map((connector) => connector.source) ||
|
||||
[]),
|
||||
],
|
||||
@@ -332,12 +332,12 @@ const ChatInputBar = React.memo(
|
||||
|
||||
const filteredPrompts = useMemo(
|
||||
() =>
|
||||
inputPrompts.filter(
|
||||
promptShortcuts.filter(
|
||||
(prompt) =>
|
||||
prompt.active &&
|
||||
prompt.prompt.toLowerCase().startsWith(startFilterSlash)
|
||||
),
|
||||
[inputPrompts, startFilterSlash]
|
||||
[promptShortcuts, startFilterSlash]
|
||||
);
|
||||
|
||||
// Determine if we should hide processing state based on context limits
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useNightTime } from "@/lib/dateUtils";
|
||||
import { useFilters } from "@/lib/hooks";
|
||||
import { uploadFilesForChat } from "../services/lib";
|
||||
import { ChatFileType, FileDescriptor } from "../interfaces";
|
||||
import { useCCPairs } from "@/lib/hooks/useCCPairs";
|
||||
import useCCPairs from "@/hooks/useCCPairs";
|
||||
import { useDocumentSets } from "@/lib/hooks/useDocumentSets";
|
||||
import { useTags } from "@/lib/hooks/useTags";
|
||||
import { useLLMProviders } from "@/lib/hooks/useLLMProviders";
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { getSourceMetadata } from "@/lib/sources";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { Route } from "next";
|
||||
import { useFederatedOAuthStatus } from "@/lib/hooks/useFederatedOAuthStatus";
|
||||
import useFederatedOAuthStatus from "@/hooks/useFederatedOAuthStatus";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { SvgLink } from "@opal/icons";
|
||||
export interface FederatedConnectorOAuthStatus {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { SEARCH_PARAM_NAMES } from "@/app/chat/services/searchParams";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import type { Route } from "next";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { buildApiPath } from "@/lib/urlBuilder";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import useSWR from "swr";
|
||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import useSWR from "swr";
|
||||
import { ToolSnapshot } from "@/lib/tools/interfaces";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useEffect, useCallback, useState } from "react";
|
||||
|
||||
export function useBoundingBox() {
|
||||
|
||||
81
web/src/hooks/useCCPairs.ts
Normal file
81
web/src/hooks/useCCPairs.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import useSWR from "swr";
|
||||
import { CCPairBasicInfo } from "@/lib/types";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
|
||||
/**
|
||||
* Hook for fetching connector-credential pairs (CC Pairs).
|
||||
*
|
||||
* Retrieves all connector-credential pairs configured in the system. CC Pairs
|
||||
* represent connections between data sources (connectors) and their authentication
|
||||
* credentials, used for indexing content from various sources like Confluence,
|
||||
* Slack, Google Drive, etc. Uses SWR for caching and automatic revalidation.
|
||||
*
|
||||
* @returns Object containing:
|
||||
* - ccPairs: Array of CCPairBasicInfo objects
|
||||
* - isLoading: Boolean indicating if data is being fetched
|
||||
* - error: Error object if the fetch failed
|
||||
* - refetch: Function to manually reload CC pairs
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Display list of connected data sources
|
||||
* const ConnectorList = () => {
|
||||
* const { ccPairs, isLoading, error } = useCCPairs();
|
||||
*
|
||||
* if (isLoading) return <Spinner />;
|
||||
* if (error) return <Error message="Failed to load connectors" />;
|
||||
*
|
||||
* return (
|
||||
* <ul>
|
||||
* {ccPairs.map(pair => (
|
||||
* <li key={pair.id}>
|
||||
* {pair.name} - {pair.source}
|
||||
* </li>
|
||||
* ))}
|
||||
* </ul>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Filter connectors by source type
|
||||
* const SlackConnectors = () => {
|
||||
* const { ccPairs } = useCCPairs();
|
||||
*
|
||||
* const slackPairs = ccPairs.filter(pair => pair.source === 'slack');
|
||||
*
|
||||
* return <ConnectorGrid connectors={slackPairs} />;
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Refresh list after connecting a new source
|
||||
* const ConnectSourceButton = () => {
|
||||
* const { refetch } = useCCPairs();
|
||||
*
|
||||
* const handleConnect = async () => {
|
||||
* await connectNewSource();
|
||||
* refetch(); // Refresh the list
|
||||
* };
|
||||
*
|
||||
* return <Button onClick={handleConnect}>Connect Source</Button>;
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export default function useCCPairs() {
|
||||
const { data, error, isLoading, mutate } = useSWR<CCPairBasicInfo[]>(
|
||||
"/api/manage/connector-status",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
return {
|
||||
ccPairs: data ?? [],
|
||||
isLoading,
|
||||
error,
|
||||
refetch: mutate,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, RefObject } from "react";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
|
||||
interface ContentSize {
|
||||
|
||||
100
web/src/hooks/useFederatedOAuthStatus.ts
Normal file
100
web/src/hooks/useFederatedOAuthStatus.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import { FederatedConnectorOAuthStatus } from "@/components/chat/FederatedOAuthModal";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
|
||||
/**
|
||||
* Hook for fetching federated OAuth connector authentication status.
|
||||
*
|
||||
* Retrieves the authentication status for all federated connectors (e.g., Gmail,
|
||||
* Google Drive, Slack) and provides utilities to identify which connectors need
|
||||
* OAuth authentication. Uses SWR for caching and automatic revalidation.
|
||||
*
|
||||
* @returns Object containing:
|
||||
* - connectors: Array of all federated connector statuses
|
||||
* - needsAuth: Array of connectors that lack OAuth tokens
|
||||
* - hasUnauthenticatedConnectors: Boolean indicating if any connectors need auth
|
||||
* - isLoading: Boolean indicating if data is being fetched
|
||||
* - error: Error object if the fetch failed
|
||||
* - refetch: Function to manually reload connector statuses
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Display connectors requiring authentication
|
||||
* const OAuthPrompt = () => {
|
||||
* const { needsAuth, isLoading } = useFederatedOAuthStatus();
|
||||
*
|
||||
* if (isLoading) return <Spinner />;
|
||||
* if (needsAuth.length === 0) return null;
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <h3>Connect your accounts:</h3>
|
||||
* {needsAuth.map(connector => (
|
||||
* <ConnectButton key={connector.source} connector={connector} />
|
||||
* ))}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Show warning banner if any connectors need authentication
|
||||
* const AuthWarningBanner = () => {
|
||||
* const { hasUnauthenticatedConnectors } = useFederatedOAuthStatus();
|
||||
*
|
||||
* if (!hasUnauthenticatedConnectors) return null;
|
||||
*
|
||||
* return (
|
||||
* <Banner variant="warning">
|
||||
* Some connectors need authentication to access your data.
|
||||
* </Banner>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // List all connectors with their auth status
|
||||
* const ConnectorList = () => {
|
||||
* const { connectors, refetch } = useFederatedOAuthStatus();
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* {connectors.map(connector => (
|
||||
* <ConnectorRow
|
||||
* key={connector.source}
|
||||
* connector={connector}
|
||||
* authenticated={connector.has_oauth_token}
|
||||
* onReconnect={refetch}
|
||||
* />
|
||||
* ))}
|
||||
* </div>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export default function useFederatedOAuthStatus() {
|
||||
const { data, error, isLoading, mutate } = useSWR<
|
||||
FederatedConnectorOAuthStatus[]
|
||||
>("/api/federated/oauth-status", errorHandlingFetcher);
|
||||
|
||||
const connectors = data ?? [];
|
||||
const needsAuth = useMemo(
|
||||
() => (data ?? []).filter((c) => !c.has_oauth_token),
|
||||
[data]
|
||||
);
|
||||
const hasUnauthenticatedConnectors = needsAuth.length > 0;
|
||||
|
||||
return {
|
||||
connectors,
|
||||
needsAuth,
|
||||
hasUnauthenticatedConnectors,
|
||||
isLoading,
|
||||
error,
|
||||
refetch: mutate,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useDropzone, DropzoneOptions, FileRejection } from "react-dropzone";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import type { Route } from "next";
|
||||
|
||||
86
web/src/hooks/usePromptShortcuts.ts
Normal file
86
web/src/hooks/usePromptShortcuts.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import useSWR from "swr";
|
||||
import { InputPrompt } from "@/app/chat/interfaces";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
|
||||
/**
|
||||
* Hook for fetching user-created prompt shortcuts.
|
||||
*
|
||||
* Retrieves prompt shortcuts that can be used to quickly insert common prompts
|
||||
* in chat. Automatically filters out public/system prompts, returning only
|
||||
* prompts created by the current user. Uses SWR for caching and automatic
|
||||
* revalidation.
|
||||
*
|
||||
* @returns Object containing:
|
||||
* - promptShortcuts: Array of InputPrompt objects (user's shortcuts only)
|
||||
* - isLoading: Boolean indicating if data is being fetched
|
||||
* - error: Error object if the fetch failed
|
||||
* - refetch: Function to manually reload the prompts
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Basic usage with loading state
|
||||
* const MyComponent = () => {
|
||||
* const { promptShortcuts, isLoading, error } = usePromptShortcuts();
|
||||
*
|
||||
* if (isLoading) return <Spinner />;
|
||||
* if (error) return <Error />;
|
||||
*
|
||||
* return (
|
||||
* <ul>
|
||||
* {promptShortcuts.map(shortcut => (
|
||||
* <li key={shortcut.id}>{shortcut.prompt}</li>
|
||||
* ))}
|
||||
* </ul>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // With refetch after creating a new shortcut
|
||||
* const ShortcutManager = () => {
|
||||
* const { promptShortcuts, refetch } = usePromptShortcuts();
|
||||
*
|
||||
* const handleCreate = async (newShortcut) => {
|
||||
* await createShortcut(newShortcut);
|
||||
* refetch(); // Refresh the list
|
||||
* };
|
||||
*
|
||||
* return <ShortcutsList shortcuts={promptShortcuts} onCreate={handleCreate} />;
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Filtering active shortcuts for slash command menu
|
||||
* const SlashCommandMenu = ({ searchTerm }) => {
|
||||
* const { promptShortcuts } = usePromptShortcuts();
|
||||
*
|
||||
* const activeShortcuts = promptShortcuts.filter(
|
||||
* (shortcut) =>
|
||||
* shortcut.active &&
|
||||
* shortcut.prompt.toLowerCase().startsWith(searchTerm)
|
||||
* );
|
||||
*
|
||||
* return <CommandMenu items={activeShortcuts} />;
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
export default function usePromptShortcuts() {
|
||||
const { data, error, isLoading, mutate } = useSWR<InputPrompt[]>(
|
||||
"/api/input_prompt",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
// Filter to only user-created prompts (exclude public/system prompts)
|
||||
const promptShortcuts = data?.filter((p) => !p.is_public) ?? [];
|
||||
|
||||
return {
|
||||
promptShortcuts,
|
||||
isLoading,
|
||||
error,
|
||||
refetch: mutate,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import useSWR, { KeyedMutator } from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { getActionIcon } from "@/lib/tools/mcpUtils";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { User } from "@/lib/types";
|
||||
import { NO_AUTH_USER_ID } from "@/lib/extension/constants";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { User, UserPersonalization } from "@/lib/types";
|
||||
|
||||
@@ -27,7 +29,64 @@ interface UseUserPersonalizationOptions {
|
||||
onError?: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export function useUserPersonalization(
|
||||
/**
|
||||
* Hook for managing user personalization settings
|
||||
*
|
||||
* Handles user personalization data including name, role, and memories.
|
||||
* Provides state management and persistence for personalization fields with
|
||||
* optimistic updates and error handling.
|
||||
*
|
||||
* @param user - The current user object containing personalization data
|
||||
* @param persistPersonalization - Async function to persist personalization changes to the server
|
||||
* @param options - Optional callbacks for success and error handling
|
||||
* @param options.onSuccess - Callback invoked when personalization is successfully saved
|
||||
* @param options.onError - Callback invoked when personalization save fails
|
||||
* @returns Object containing personalization state and handler functions
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import useUserPersonalization from "@/hooks/useUserPersonalization";
|
||||
* import { useUser } from "@/components/user/UserProvider";
|
||||
*
|
||||
* function PersonalizationSettings() {
|
||||
* const { user, updateUserPersonalization } = useUser();
|
||||
* const {
|
||||
* personalizationValues,
|
||||
* updatePersonalizationField,
|
||||
* toggleUseMemories,
|
||||
* updateMemoryAtIndex,
|
||||
* addMemory,
|
||||
* handleSavePersonalization,
|
||||
* isSavingPersonalization
|
||||
* } = useUserPersonalization(user, updateUserPersonalization, {
|
||||
* onSuccess: () => console.log("Saved!"),
|
||||
* onError: () => console.log("Failed!")
|
||||
* });
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <input
|
||||
* value={personalizationValues.name}
|
||||
* onChange={(e) => updatePersonalizationField("name", e.target.value)}
|
||||
* />
|
||||
* <button
|
||||
* onClick={handleSavePersonalization}
|
||||
* disabled={isSavingPersonalization}
|
||||
* >
|
||||
* Save
|
||||
* </button>
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @remarks
|
||||
* - Changes are optimistic - UI updates immediately before server persistence
|
||||
* - On error, state reverts to the last known good value from the user object
|
||||
* - Memories are automatically trimmed and filtered (empty strings removed) on save
|
||||
* - The hook synchronizes with user prop changes to stay in sync with external updates
|
||||
*/
|
||||
export default function useUserPersonalization(
|
||||
user: User | null,
|
||||
persistPersonalization: (
|
||||
personalization: UserPersonalization
|
||||
@@ -1,17 +0,0 @@
|
||||
import useSWR from "swr";
|
||||
import { CCPairBasicInfo } from "@/lib/types";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
|
||||
export function useCCPairs() {
|
||||
const { data, error, mutate } = useSWR<CCPairBasicInfo[]>(
|
||||
"/api/manage/connector-status",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
return {
|
||||
ccPairs: data,
|
||||
isLoading: !error && !data,
|
||||
error,
|
||||
refetch: mutate,
|
||||
};
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import { FederatedConnectorOAuthStatus } from "@/components/chat/FederatedOAuthModal";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
|
||||
export function useFederatedOAuthStatus() {
|
||||
const { data, error, mutate } = useSWR<FederatedConnectorOAuthStatus[]>(
|
||||
"/api/federated/oauth-status",
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
const connectors = data ?? [];
|
||||
const needsAuth = useMemo(
|
||||
() => connectors.filter((c) => !c.has_oauth_token),
|
||||
[connectors]
|
||||
);
|
||||
const hasUnauthenticatedConnectors = needsAuth.length > 0;
|
||||
|
||||
return {
|
||||
connectors,
|
||||
needsAuth,
|
||||
hasUnauthenticatedConnectors,
|
||||
loading: !error && !data,
|
||||
error,
|
||||
refetch: mutate,
|
||||
};
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import useSWR from "swr";
|
||||
import { InputPrompt } from "@/app/chat/interfaces";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
|
||||
export function useInputPrompts() {
|
||||
const { data, error, mutate } = useSWR<InputPrompt[]>(
|
||||
"/api/input_prompt?include_public=true",
|
||||
errorHandlingFetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 60000,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
inputPrompts: data ?? [],
|
||||
isLoading: !error && !data,
|
||||
error,
|
||||
refresh: mutate,
|
||||
};
|
||||
}
|
||||
@@ -66,7 +66,7 @@ function LLMProviderCardInner({
|
||||
disabled && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1 p-1 flex-1 min-w-0">
|
||||
<div className="flex gap-1 p-1 flex-1 min-w-0">
|
||||
<div className="flex items-start h-full pt-0.5">
|
||||
{providerName ? (
|
||||
<ProviderIcon provider={providerName} size={16} className="" />
|
||||
|
||||
@@ -26,7 +26,7 @@ import { ValidSources } from "@/lib/types";
|
||||
import { SourceMetadata } from "@/lib/search/interfaces";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { useAvailableTools } from "@/hooks/useAvailableTools";
|
||||
import { useCCPairs } from "@/lib/hooks/useCCPairs";
|
||||
import useCCPairs from "@/hooks/useCCPairs";
|
||||
import IconButton from "@/refresh-components/buttons/IconButton";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
import { useToolOAuthStatus } from "@/lib/hooks/useToolOAuthStatus";
|
||||
@@ -147,7 +147,7 @@ export default function ActionsPopover({
|
||||
const availableToolIds = availableTools.map((tool) => tool.id);
|
||||
|
||||
// Check if there are any connectors available
|
||||
const hasNoConnectors = !ccPairs || ccPairs.length === 0;
|
||||
const hasNoConnectors = ccPairs.length === 0;
|
||||
|
||||
const assistantPreference = assistantPreferences?.[selectedAssistant.id];
|
||||
const disabledToolIds = assistantPreference?.disabled_tool_ids || [];
|
||||
|
||||
@@ -19,10 +19,10 @@ import { deleteAllChatSessions } from "@/app/chat/services/lib";
|
||||
import { SourceIcon } from "@/components/SourceIcon";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { getSourceMetadata } from "@/lib/sources";
|
||||
import { useFederatedOAuthStatus } from "@/lib/hooks/useFederatedOAuthStatus";
|
||||
import { useCCPairs } from "@/lib/hooks/useCCPairs";
|
||||
import useFederatedOAuthStatus from "@/hooks/useFederatedOAuthStatus";
|
||||
import useCCPairs from "@/hooks/useCCPairs";
|
||||
import { useLLMProviders } from "@/lib/hooks/useLLMProviders";
|
||||
import { useUserPersonalization } from "@/lib/hooks/useUserPersonalization";
|
||||
import useUserPersonalization from "@/hooks/useUserPersonalization";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import PATManagement from "@/components/user/PATManagement";
|
||||
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
|
||||
@@ -85,7 +85,7 @@ export default function UserSettings() {
|
||||
const {
|
||||
connectors: federatedConnectors,
|
||||
refetch: refetchFederatedConnectors,
|
||||
loading: isFederatedLoading,
|
||||
isLoading: isFederatedLoading,
|
||||
} = useFederatedOAuthStatus();
|
||||
|
||||
const { ccPairs, isLoading: isCCPairsLoading } = useCCPairs();
|
||||
@@ -102,9 +102,7 @@ export default function UserSettings() {
|
||||
// Use currentDefaultModel for display, falling back to defaultModel
|
||||
const displayModel = currentDefaultModel ?? defaultModel;
|
||||
|
||||
const hasConnectors =
|
||||
(ccPairs && ccPairs.length > 0) ||
|
||||
(federatedConnectors && federatedConnectors.length > 0);
|
||||
const hasConnectors = ccPairs.length > 0 || federatedConnectors.length > 0;
|
||||
|
||||
const isLoadingConnectors = isCCPairsLoading || isFederatedLoading;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user