mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-18 00:05:47 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16e904a873 | ||
|
|
66f47d294c | ||
|
|
0a685bda7d | ||
|
|
23dc8b5dad | ||
|
|
cd5f2293ad | ||
|
|
6c2269e565 | ||
|
|
46315cddf1 | ||
|
|
5f28a1b0e4 | ||
|
|
9e9b7ed61d | ||
|
|
3fb2bfefec | ||
|
|
7c618c9d17 | ||
|
|
03e2789392 | ||
|
|
2783fa08a3 |
@@ -1,5 +1,5 @@
|
||||
from sqlalchemy.engine.base import Connection
|
||||
from typing import Any
|
||||
from typing import Literal
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
import logging
|
||||
@@ -8,6 +8,7 @@ from alembic import context
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy.sql import text
|
||||
from sqlalchemy.sql.schema import SchemaItem
|
||||
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
from danswer.db.engine import build_connection_string
|
||||
@@ -35,7 +36,18 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def include_object(
|
||||
object: Any, name: str, type_: str, reflected: bool, compare_to: Any
|
||||
object: SchemaItem,
|
||||
name: str | None,
|
||||
type_: Literal[
|
||||
"schema",
|
||||
"table",
|
||||
"column",
|
||||
"index",
|
||||
"unique_constraint",
|
||||
"foreign_key_constraint",
|
||||
],
|
||||
reflected: bool,
|
||||
compare_to: SchemaItem | None,
|
||||
) -> bool:
|
||||
"""
|
||||
Determines whether a database object should be included in migrations.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
from typing import Literal
|
||||
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
@@ -37,8 +38,15 @@ EXCLUDE_TABLES = {"kombu_queue", "kombu_message"}
|
||||
|
||||
def include_object(
|
||||
object: SchemaItem,
|
||||
name: str,
|
||||
type_: str,
|
||||
name: str | None,
|
||||
type_: Literal[
|
||||
"schema",
|
||||
"table",
|
||||
"column",
|
||||
"index",
|
||||
"unique_constraint",
|
||||
"foreign_key_constraint",
|
||||
],
|
||||
reflected: bool,
|
||||
compare_to: SchemaItem | None,
|
||||
) -> bool:
|
||||
|
||||
@@ -11,6 +11,7 @@ from celery.exceptions import WorkerShutdown
|
||||
from celery.states import READY_STATES
|
||||
from celery.utils.log import get_task_logger
|
||||
from celery.worker import strategy # type: ignore
|
||||
from redis.lock import Lock as RedisLock
|
||||
from sentry_sdk.integrations.celery import CeleryIntegration
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -332,16 +333,16 @@ def on_worker_shutdown(sender: Any, **kwargs: Any) -> None:
|
||||
return
|
||||
|
||||
logger.info("Releasing primary worker lock.")
|
||||
lock = sender.primary_worker_lock
|
||||
lock: RedisLock = sender.primary_worker_lock
|
||||
try:
|
||||
if lock.owned():
|
||||
try:
|
||||
lock.release()
|
||||
sender.primary_worker_lock = None
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to release primary worker lock: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check if primary worker lock is owned: {e}")
|
||||
except Exception:
|
||||
logger.exception("Failed to release primary worker lock")
|
||||
except Exception:
|
||||
logger.exception("Failed to check if primary worker lock is owned")
|
||||
|
||||
|
||||
def on_setup_logging(
|
||||
|
||||
@@ -11,6 +11,7 @@ from celery.signals import celeryd_init
|
||||
from celery.signals import worker_init
|
||||
from celery.signals import worker_ready
|
||||
from celery.signals import worker_shutdown
|
||||
from redis.lock import Lock as RedisLock
|
||||
|
||||
import danswer.background.celery.apps.app_base as app_base
|
||||
from danswer.background.celery.apps.app_base import task_logger
|
||||
@@ -116,7 +117,7 @@ def on_worker_init(sender: Any, **kwargs: Any) -> None:
|
||||
# it is planned to use this lock to enforce singleton behavior on the primary
|
||||
# worker, since the primary worker does redis cleanup on startup, but this isn't
|
||||
# implemented yet.
|
||||
lock = r.lock(
|
||||
lock: RedisLock = r.lock(
|
||||
DanswerRedisLocks.PRIMARY_WORKER,
|
||||
timeout=CELERY_PRIMARY_WORKER_LOCK_TIMEOUT,
|
||||
)
|
||||
@@ -227,7 +228,7 @@ class HubPeriodicTask(bootsteps.StartStopStep):
|
||||
if not hasattr(worker, "primary_worker_lock"):
|
||||
return
|
||||
|
||||
lock = worker.primary_worker_lock
|
||||
lock: RedisLock = worker.primary_worker_lock
|
||||
|
||||
r = get_redis_client(tenant_id=None)
|
||||
|
||||
|
||||
@@ -2,54 +2,55 @@ from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
|
||||
|
||||
tasks_to_schedule = [
|
||||
{
|
||||
"name": "check-for-vespa-sync",
|
||||
"task": "check_for_vespa_sync_task",
|
||||
"task": DanswerCeleryTask.CHECK_FOR_VESPA_SYNC_TASK,
|
||||
"schedule": timedelta(seconds=20),
|
||||
"options": {"priority": DanswerCeleryPriority.HIGH},
|
||||
},
|
||||
{
|
||||
"name": "check-for-connector-deletion",
|
||||
"task": "check_for_connector_deletion_task",
|
||||
"task": DanswerCeleryTask.CHECK_FOR_CONNECTOR_DELETION,
|
||||
"schedule": timedelta(seconds=20),
|
||||
"options": {"priority": DanswerCeleryPriority.HIGH},
|
||||
},
|
||||
{
|
||||
"name": "check-for-indexing",
|
||||
"task": "check_for_indexing",
|
||||
"task": DanswerCeleryTask.CHECK_FOR_INDEXING,
|
||||
"schedule": timedelta(seconds=15),
|
||||
"options": {"priority": DanswerCeleryPriority.HIGH},
|
||||
},
|
||||
{
|
||||
"name": "check-for-prune",
|
||||
"task": "check_for_pruning",
|
||||
"task": DanswerCeleryTask.CHECK_FOR_PRUNING,
|
||||
"schedule": timedelta(seconds=15),
|
||||
"options": {"priority": DanswerCeleryPriority.HIGH},
|
||||
},
|
||||
{
|
||||
"name": "kombu-message-cleanup",
|
||||
"task": "kombu_message_cleanup_task",
|
||||
"task": DanswerCeleryTask.KOMBU_MESSAGE_CLEANUP_TASK,
|
||||
"schedule": timedelta(seconds=3600),
|
||||
"options": {"priority": DanswerCeleryPriority.LOWEST},
|
||||
},
|
||||
{
|
||||
"name": "monitor-vespa-sync",
|
||||
"task": "monitor_vespa_sync",
|
||||
"task": DanswerCeleryTask.MONITOR_VESPA_SYNC,
|
||||
"schedule": timedelta(seconds=5),
|
||||
"options": {"priority": DanswerCeleryPriority.HIGH},
|
||||
},
|
||||
{
|
||||
"name": "check-for-doc-permissions-sync",
|
||||
"task": "check_for_doc_permissions_sync",
|
||||
"task": DanswerCeleryTask.CHECK_FOR_DOC_PERMISSIONS_SYNC,
|
||||
"schedule": timedelta(seconds=30),
|
||||
"options": {"priority": DanswerCeleryPriority.HIGH},
|
||||
},
|
||||
{
|
||||
"name": "check-for-external-group-sync",
|
||||
"task": "check_for_external_group_sync",
|
||||
"task": DanswerCeleryTask.CHECK_FOR_EXTERNAL_GROUP_SYNC,
|
||||
"schedule": timedelta(seconds=20),
|
||||
"options": {"priority": DanswerCeleryPriority.HIGH},
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
|
||||
from danswer.background.celery.apps.app_base import task_logger
|
||||
from danswer.configs.app_configs import JOB_TIMEOUT
|
||||
from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.configs.constants import DanswerRedisLocks
|
||||
from danswer.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
from danswer.db.connector_credential_pair import get_connector_credential_pairs
|
||||
@@ -28,7 +29,7 @@ class TaskDependencyError(RuntimeError):
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="check_for_connector_deletion_task",
|
||||
name=DanswerCeleryTask.CHECK_FOR_CONNECTOR_DELETION,
|
||||
soft_time_limit=JOB_TIMEOUT,
|
||||
trail=False,
|
||||
bind=True,
|
||||
|
||||
@@ -18,6 +18,7 @@ from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DANSWER_REDIS_FUNCTION_LOCK_PREFIX
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.configs.constants import DanswerRedisLocks
|
||||
from danswer.configs.constants import DocumentSource
|
||||
from danswer.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
@@ -82,7 +83,7 @@ def _is_external_doc_permissions_sync_due(cc_pair: ConnectorCredentialPair) -> b
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="check_for_doc_permissions_sync",
|
||||
name=DanswerCeleryTask.CHECK_FOR_DOC_PERMISSIONS_SYNC,
|
||||
soft_time_limit=JOB_TIMEOUT,
|
||||
bind=True,
|
||||
)
|
||||
@@ -164,7 +165,7 @@ def try_creating_permissions_sync_task(
|
||||
custom_task_id = f"{redis_connector.permissions.generator_task_key}_{uuid4()}"
|
||||
|
||||
result = app.send_task(
|
||||
"connector_permission_sync_generator_task",
|
||||
DanswerCeleryTask.CONNECTOR_PERMISSION_SYNC_GENERATOR_TASK,
|
||||
kwargs=dict(
|
||||
cc_pair_id=cc_pair_id,
|
||||
tenant_id=tenant_id,
|
||||
@@ -191,7 +192,7 @@ def try_creating_permissions_sync_task(
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="connector_permission_sync_generator_task",
|
||||
name=DanswerCeleryTask.CONNECTOR_PERMISSION_SYNC_GENERATOR_TASK,
|
||||
acks_late=False,
|
||||
soft_time_limit=JOB_TIMEOUT,
|
||||
track_started=True,
|
||||
@@ -286,7 +287,7 @@ def connector_permission_sync_generator_task(
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="update_external_document_permissions_task",
|
||||
name=DanswerCeleryTask.UPDATE_EXTERNAL_DOCUMENT_PERMISSIONS_TASK,
|
||||
soft_time_limit=LIGHT_SOFT_TIME_LIMIT,
|
||||
time_limit=LIGHT_TIME_LIMIT,
|
||||
max_retries=DOCUMENT_PERMISSIONS_UPDATE_MAX_RETRIES,
|
||||
|
||||
@@ -17,6 +17,7 @@ from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DANSWER_REDIS_FUNCTION_LOCK_PREFIX
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.configs.constants import DanswerRedisLocks
|
||||
from danswer.db.connector import mark_cc_pair_as_external_group_synced
|
||||
from danswer.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
@@ -85,7 +86,7 @@ def _is_external_group_sync_due(cc_pair: ConnectorCredentialPair) -> bool:
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="check_for_external_group_sync",
|
||||
name=DanswerCeleryTask.CHECK_FOR_EXTERNAL_GROUP_SYNC,
|
||||
soft_time_limit=JOB_TIMEOUT,
|
||||
bind=True,
|
||||
)
|
||||
@@ -161,7 +162,7 @@ def try_creating_external_group_sync_task(
|
||||
custom_task_id = f"{redis_connector.external_group_sync.taskset_key}_{uuid4()}"
|
||||
|
||||
result = app.send_task(
|
||||
"connector_external_group_sync_generator_task",
|
||||
DanswerCeleryTask.CONNECTOR_EXTERNAL_GROUP_SYNC_GENERATOR_TASK,
|
||||
kwargs=dict(
|
||||
cc_pair_id=cc_pair_id,
|
||||
tenant_id=tenant_id,
|
||||
@@ -191,7 +192,7 @@ def try_creating_external_group_sync_task(
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="connector_external_group_sync_generator_task",
|
||||
name=DanswerCeleryTask.CONNECTOR_EXTERNAL_GROUP_SYNC_GENERATOR_TASK,
|
||||
acks_late=False,
|
||||
soft_time_limit=JOB_TIMEOUT,
|
||||
track_started=True,
|
||||
|
||||
@@ -23,6 +23,7 @@ from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DANSWER_REDIS_FUNCTION_LOCK_PREFIX
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.configs.constants import DanswerRedisLocks
|
||||
from danswer.configs.constants import DocumentSource
|
||||
from danswer.db.connector import mark_ccpair_with_indexing_trigger
|
||||
@@ -156,7 +157,7 @@ def get_unfenced_index_attempt_ids(db_session: Session, r: redis.Redis) -> list[
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="check_for_indexing",
|
||||
name=DanswerCeleryTask.CHECK_FOR_INDEXING,
|
||||
soft_time_limit=300,
|
||||
bind=True,
|
||||
)
|
||||
@@ -486,7 +487,7 @@ def try_creating_indexing_task(
|
||||
# when the task is sent, we have yet to finish setting up the fence
|
||||
# therefore, the task must contain code that blocks until the fence is ready
|
||||
result = celery_app.send_task(
|
||||
"connector_indexing_proxy_task",
|
||||
DanswerCeleryTask.CONNECTOR_INDEXING_PROXY_TASK,
|
||||
kwargs=dict(
|
||||
index_attempt_id=index_attempt_id,
|
||||
cc_pair_id=cc_pair.id,
|
||||
@@ -524,7 +525,10 @@ def try_creating_indexing_task(
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="connector_indexing_proxy_task", bind=True, acks_late=False, track_started=True
|
||||
name=DanswerCeleryTask.CONNECTOR_INDEXING_PROXY_TASK,
|
||||
bind=True,
|
||||
acks_late=False,
|
||||
track_started=True,
|
||||
)
|
||||
def connector_indexing_proxy_task(
|
||||
self: Task,
|
||||
@@ -580,39 +584,64 @@ def connector_indexing_proxy_task(
|
||||
|
||||
if self.request.id and redis_connector_index.terminating(self.request.id):
|
||||
task_logger.warning(
|
||||
"Indexing proxy - termination signal detected: "
|
||||
"Indexing watchdog - termination signal detected: "
|
||||
f"attempt={index_attempt_id} "
|
||||
f"tenant={tenant_id} "
|
||||
f"cc_pair={cc_pair_id} "
|
||||
f"search_settings={search_settings_id}"
|
||||
)
|
||||
|
||||
with get_session_with_tenant(tenant_id) as db_session:
|
||||
mark_attempt_canceled(
|
||||
index_attempt_id,
|
||||
db_session,
|
||||
"Connector termination signal detected",
|
||||
try:
|
||||
with get_session_with_tenant(tenant_id) as db_session:
|
||||
mark_attempt_canceled(
|
||||
index_attempt_id,
|
||||
db_session,
|
||||
"Connector termination signal detected",
|
||||
)
|
||||
finally:
|
||||
# if the DB exceptions, we'll just get an unfriendly failure message
|
||||
# in the UI instead of the cancellation message
|
||||
logger.exception(
|
||||
"Indexing watchdog - transient exception marking index attempt as canceled: "
|
||||
f"attempt={index_attempt_id} "
|
||||
f"tenant={tenant_id} "
|
||||
f"cc_pair={cc_pair_id} "
|
||||
f"search_settings={search_settings_id}"
|
||||
)
|
||||
|
||||
job.cancel()
|
||||
job.cancel()
|
||||
|
||||
break
|
||||
|
||||
# do nothing for ongoing jobs that haven't been stopped
|
||||
if not job.done():
|
||||
with get_session_with_tenant(tenant_id) as db_session:
|
||||
index_attempt = get_index_attempt(
|
||||
db_session=db_session, index_attempt_id=index_attempt_id
|
||||
# if the spawned task is still running, restart the check once again
|
||||
# if the index attempt is not in a finished status
|
||||
try:
|
||||
with get_session_with_tenant(tenant_id) as db_session:
|
||||
index_attempt = get_index_attempt(
|
||||
db_session=db_session, index_attempt_id=index_attempt_id
|
||||
)
|
||||
|
||||
if not index_attempt:
|
||||
continue
|
||||
|
||||
if not index_attempt.is_finished():
|
||||
continue
|
||||
except Exception:
|
||||
# if the DB exceptioned, just restart the check.
|
||||
# polling the index attempt status doesn't need to be strongly consistent
|
||||
logger.exception(
|
||||
"Indexing watchdog - transient exception looking up index attempt: "
|
||||
f"attempt={index_attempt_id} "
|
||||
f"tenant={tenant_id} "
|
||||
f"cc_pair={cc_pair_id} "
|
||||
f"search_settings={search_settings_id}"
|
||||
)
|
||||
|
||||
if not index_attempt:
|
||||
continue
|
||||
|
||||
if not index_attempt.is_finished():
|
||||
continue
|
||||
continue
|
||||
|
||||
if job.status == "error":
|
||||
task_logger.error(
|
||||
f"Indexing watchdog - spawned task exceptioned: "
|
||||
"Indexing watchdog - spawned task exceptioned: "
|
||||
f"attempt={index_attempt_id} "
|
||||
f"tenant={tenant_id} "
|
||||
f"cc_pair={cc_pair_id} "
|
||||
|
||||
@@ -13,12 +13,13 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from danswer.background.celery.apps.app_base import task_logger
|
||||
from danswer.configs.app_configs import JOB_TIMEOUT
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.configs.constants import PostgresAdvisoryLocks
|
||||
from danswer.db.engine import get_session_with_tenant
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="kombu_message_cleanup_task",
|
||||
name=DanswerCeleryTask.KOMBU_MESSAGE_CLEANUP_TASK,
|
||||
soft_time_limit=JOB_TIMEOUT,
|
||||
bind=True,
|
||||
base=AbortableTask,
|
||||
|
||||
@@ -20,6 +20,7 @@ from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DANSWER_REDIS_FUNCTION_LOCK_PREFIX
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.configs.constants import DanswerRedisLocks
|
||||
from danswer.connectors.factory import instantiate_connector
|
||||
from danswer.connectors.models import InputType
|
||||
@@ -75,7 +76,7 @@ def _is_pruning_due(cc_pair: ConnectorCredentialPair) -> bool:
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="check_for_pruning",
|
||||
name=DanswerCeleryTask.CHECK_FOR_PRUNING,
|
||||
soft_time_limit=JOB_TIMEOUT,
|
||||
bind=True,
|
||||
)
|
||||
@@ -184,7 +185,7 @@ def try_creating_prune_generator_task(
|
||||
custom_task_id = f"{redis_connector.prune.generator_task_key}_{uuid4()}"
|
||||
|
||||
celery_app.send_task(
|
||||
"connector_pruning_generator_task",
|
||||
DanswerCeleryTask.CONNECTOR_PRUNING_GENERATOR_TASK,
|
||||
kwargs=dict(
|
||||
cc_pair_id=cc_pair.id,
|
||||
connector_id=cc_pair.connector_id,
|
||||
@@ -209,7 +210,7 @@ def try_creating_prune_generator_task(
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="connector_pruning_generator_task",
|
||||
name=DanswerCeleryTask.CONNECTOR_PRUNING_GENERATOR_TASK,
|
||||
acks_late=False,
|
||||
soft_time_limit=JOB_TIMEOUT,
|
||||
track_started=True,
|
||||
|
||||
@@ -9,6 +9,7 @@ from tenacity import RetryError
|
||||
from danswer.access.access import get_access_for_document
|
||||
from danswer.background.celery.apps.app_base import task_logger
|
||||
from danswer.background.celery.tasks.shared.RetryDocumentIndex import RetryDocumentIndex
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.db.document import delete_document_by_connector_credential_pair__no_commit
|
||||
from danswer.db.document import delete_documents_complete__no_commit
|
||||
from danswer.db.document import get_document
|
||||
@@ -31,7 +32,7 @@ LIGHT_TIME_LIMIT = LIGHT_SOFT_TIME_LIMIT + 15
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="document_by_cc_pair_cleanup_task",
|
||||
name=DanswerCeleryTask.DOCUMENT_BY_CC_PAIR_CLEANUP_TASK,
|
||||
soft_time_limit=LIGHT_SOFT_TIME_LIMIT,
|
||||
time_limit=LIGHT_TIME_LIMIT,
|
||||
max_retries=DOCUMENT_BY_CC_PAIR_CLEANUP_MAX_RETRIES,
|
||||
|
||||
@@ -25,6 +25,7 @@ from danswer.background.celery.tasks.shared.tasks import LIGHT_TIME_LIMIT
|
||||
from danswer.configs.app_configs import JOB_TIMEOUT
|
||||
from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.configs.constants import DanswerRedisLocks
|
||||
from danswer.db.connector import fetch_connector_by_id
|
||||
from danswer.db.connector import mark_cc_pair_as_permissions_synced
|
||||
@@ -80,7 +81,7 @@ logger = setup_logger()
|
||||
# celery auto associates tasks created inside another task,
|
||||
# which bloats the result metadata considerably. trail=False prevents this.
|
||||
@shared_task(
|
||||
name="check_for_vespa_sync_task",
|
||||
name=DanswerCeleryTask.CHECK_FOR_VESPA_SYNC_TASK,
|
||||
soft_time_limit=JOB_TIMEOUT,
|
||||
trail=False,
|
||||
bind=True,
|
||||
@@ -654,24 +655,28 @@ def monitor_ccpair_indexing_taskset(
|
||||
# outer = result.state in READY state
|
||||
status_int = redis_connector_index.get_completion()
|
||||
if status_int is None: # inner signal not set ... possible error
|
||||
result_state = result.state
|
||||
task_state = result.state
|
||||
if (
|
||||
result_state in READY_STATES
|
||||
task_state in READY_STATES
|
||||
): # outer signal in terminal state ... possible error
|
||||
# Now double check!
|
||||
if redis_connector_index.get_completion() is None:
|
||||
# inner signal still not set (and cannot change when outer result_state is READY)
|
||||
# Task is finished but generator complete isn't set.
|
||||
# We have a problem! Worker may have crashed.
|
||||
task_result = str(result.result)
|
||||
task_traceback = str(result.traceback)
|
||||
|
||||
msg = (
|
||||
f"Connector indexing aborted or exceptioned: "
|
||||
f"attempt={payload.index_attempt_id} "
|
||||
f"celery_task={payload.celery_task_id} "
|
||||
f"result_state={result_state} "
|
||||
f"cc_pair={cc_pair_id} "
|
||||
f"search_settings={search_settings_id} "
|
||||
f"elapsed_submitted={elapsed_submitted.total_seconds():.2f}"
|
||||
f"elapsed_submitted={elapsed_submitted.total_seconds():.2f} "
|
||||
f"result.state={task_state} "
|
||||
f"result.result={task_result} "
|
||||
f"result.traceback={task_traceback}"
|
||||
)
|
||||
task_logger.warning(msg)
|
||||
|
||||
@@ -703,7 +708,7 @@ def monitor_ccpair_indexing_taskset(
|
||||
redis_connector_index.reset()
|
||||
|
||||
|
||||
@shared_task(name="monitor_vespa_sync", soft_time_limit=300, bind=True)
|
||||
@shared_task(name=DanswerCeleryTask.MONITOR_VESPA_SYNC, soft_time_limit=300, bind=True)
|
||||
def monitor_vespa_sync(self: Task, tenant_id: str | None) -> bool:
|
||||
"""This is a celery beat task that monitors and finalizes metadata sync tasksets.
|
||||
It scans for fence values and then gets the counts of any associated tasksets.
|
||||
@@ -814,7 +819,7 @@ def monitor_vespa_sync(self: Task, tenant_id: str | None) -> bool:
|
||||
|
||||
|
||||
@shared_task(
|
||||
name="vespa_metadata_sync_task",
|
||||
name=DanswerCeleryTask.VESPA_METADATA_SYNC_TASK,
|
||||
bind=True,
|
||||
soft_time_limit=LIGHT_SOFT_TIME_LIMIT,
|
||||
time_limit=LIGHT_TIME_LIMIT,
|
||||
|
||||
@@ -31,6 +31,7 @@ def llm_doc_from_inference_section(inference_section: InferenceSection) -> LlmDo
|
||||
if inference_section.center_chunk.source_links
|
||||
else None,
|
||||
source_links=inference_section.center_chunk.source_links,
|
||||
match_highlights=inference_section.center_chunk.match_highlights,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class LlmDoc(BaseModel):
|
||||
updated_at: datetime | None
|
||||
link: str | None
|
||||
source_links: dict[int, str] | None
|
||||
match_highlights: list[str] | None
|
||||
|
||||
|
||||
# First chunk of info for streaming QA
|
||||
|
||||
@@ -308,6 +308,22 @@ CONFLUENCE_CONNECTOR_ATTACHMENT_CHAR_COUNT_THRESHOLD = int(
|
||||
os.environ.get("CONFLUENCE_CONNECTOR_ATTACHMENT_CHAR_COUNT_THRESHOLD", 200_000)
|
||||
)
|
||||
|
||||
# Due to breakages in the confluence API, the timezone offset must be specified client side
|
||||
# to match the user's specified timezone.
|
||||
|
||||
# The current state of affairs:
|
||||
# CQL queries are parsed in the user's timezone and cannot be specified in UTC
|
||||
# no API retrieves the user's timezone
|
||||
# All data is returned in UTC, so we can't derive the user's timezone from that
|
||||
|
||||
# https://community.developer.atlassian.com/t/confluence-cloud-time-zone-get-via-rest-api/35954/16
|
||||
# https://jira.atlassian.com/browse/CONFCLOUD-69670
|
||||
|
||||
# enter as a floating point offset from UTC in hours (-24 < val < 24)
|
||||
# this will be applied globally, so it probably makes sense to transition this to per
|
||||
# connector as some point.
|
||||
CONFLUENCE_TIMEZONE_OFFSET = float(os.environ.get("CONFLUENCE_TIMEZONE_OFFSET", 0.0))
|
||||
|
||||
JIRA_CONNECTOR_LABELS_TO_SKIP = [
|
||||
ignored_tag
|
||||
for ignored_tag in os.environ.get("JIRA_CONNECTOR_LABELS_TO_SKIP", "").split(",")
|
||||
|
||||
@@ -259,6 +259,32 @@ class DanswerCeleryPriority(int, Enum):
|
||||
LOWEST = auto()
|
||||
|
||||
|
||||
class DanswerCeleryTask:
|
||||
CHECK_FOR_CONNECTOR_DELETION = "check_for_connector_deletion_task"
|
||||
CHECK_FOR_VESPA_SYNC_TASK = "check_for_vespa_sync_task"
|
||||
CHECK_FOR_INDEXING = "check_for_indexing"
|
||||
CHECK_FOR_PRUNING = "check_for_pruning"
|
||||
CHECK_FOR_DOC_PERMISSIONS_SYNC = "check_for_doc_permissions_sync"
|
||||
CHECK_FOR_EXTERNAL_GROUP_SYNC = "check_for_external_group_sync"
|
||||
MONITOR_VESPA_SYNC = "monitor_vespa_sync"
|
||||
KOMBU_MESSAGE_CLEANUP_TASK = "kombu_message_cleanup_task"
|
||||
CONNECTOR_PERMISSION_SYNC_GENERATOR_TASK = (
|
||||
"connector_permission_sync_generator_task"
|
||||
)
|
||||
UPDATE_EXTERNAL_DOCUMENT_PERMISSIONS_TASK = (
|
||||
"update_external_document_permissions_task"
|
||||
)
|
||||
CONNECTOR_EXTERNAL_GROUP_SYNC_GENERATOR_TASK = (
|
||||
"connector_external_group_sync_generator_task"
|
||||
)
|
||||
CONNECTOR_INDEXING_PROXY_TASK = "connector_indexing_proxy_task"
|
||||
CONNECTOR_PRUNING_GENERATOR_TASK = "connector_pruning_generator_task"
|
||||
DOCUMENT_BY_CC_PAIR_CLEANUP_TASK = "document_by_cc_pair_cleanup_task"
|
||||
VESPA_METADATA_SYNC_TASK = "vespa_metadata_sync_task"
|
||||
CHECK_TTL_MANAGEMENT_TASK = "check_ttl_management_task"
|
||||
AUTOGENERATE_USAGE_REPORT_TASK = "autogenerate_usage_report_task"
|
||||
|
||||
|
||||
REDIS_SOCKET_KEEPALIVE_OPTIONS = {}
|
||||
REDIS_SOCKET_KEEPALIVE_OPTIONS[socket.TCP_KEEPINTVL] = 15
|
||||
REDIS_SOCKET_KEEPALIVE_OPTIONS[socket.TCP_KEEPCNT] = 3
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from danswer.configs.app_configs import CONFLUENCE_CONNECTOR_LABELS_TO_SKIP
|
||||
from danswer.configs.app_configs import CONFLUENCE_TIMEZONE_OFFSET
|
||||
from danswer.configs.app_configs import CONTINUE_ON_CONNECTOR_FAILURE
|
||||
from danswer.configs.app_configs import INDEX_BATCH_SIZE
|
||||
from danswer.configs.constants import DocumentSource
|
||||
@@ -69,6 +71,7 @@ class ConfluenceConnector(LoadConnector, PollConnector, SlimConnector):
|
||||
# skip it. This is generally used to avoid indexing extra sensitive
|
||||
# pages.
|
||||
labels_to_skip: list[str] = CONFLUENCE_CONNECTOR_LABELS_TO_SKIP,
|
||||
timezone_offset: float = CONFLUENCE_TIMEZONE_OFFSET,
|
||||
) -> None:
|
||||
self.batch_size = batch_size
|
||||
self.continue_on_failure = continue_on_failure
|
||||
@@ -104,6 +107,8 @@ class ConfluenceConnector(LoadConnector, PollConnector, SlimConnector):
|
||||
)
|
||||
self.cql_label_filter = f" and label not in ({comma_separated_labels})"
|
||||
|
||||
self.timezone: timezone = timezone(offset=timedelta(hours=timezone_offset))
|
||||
|
||||
@property
|
||||
def confluence_client(self) -> OnyxConfluence:
|
||||
if self._confluence_client is None:
|
||||
@@ -204,12 +209,14 @@ class ConfluenceConnector(LoadConnector, PollConnector, SlimConnector):
|
||||
confluence_page_ids: list[str] = []
|
||||
|
||||
page_query = self.cql_page_query + self.cql_label_filter + self.cql_time_filter
|
||||
logger.debug(f"page_query: {page_query}")
|
||||
# Fetch pages as Documents
|
||||
for page in self.confluence_client.paginated_cql_retrieval(
|
||||
cql=page_query,
|
||||
expand=",".join(_PAGE_EXPANSION_FIELDS),
|
||||
limit=self.batch_size,
|
||||
):
|
||||
logger.debug(f"_fetch_document_batches: {page['id']}")
|
||||
confluence_page_ids.append(page["id"])
|
||||
doc = self._convert_object_to_document(page)
|
||||
if doc is not None:
|
||||
@@ -242,10 +249,10 @@ class ConfluenceConnector(LoadConnector, PollConnector, SlimConnector):
|
||||
|
||||
def poll_source(self, start: float, end: float) -> GenerateDocumentsOutput:
|
||||
# Add time filters
|
||||
formatted_start_time = datetime.fromtimestamp(start, tz=timezone.utc).strftime(
|
||||
formatted_start_time = datetime.fromtimestamp(start, tz=self.timezone).strftime(
|
||||
"%Y-%m-%d %H:%M"
|
||||
)
|
||||
formatted_end_time = datetime.fromtimestamp(end, tz=timezone.utc).strftime(
|
||||
formatted_end_time = datetime.fromtimestamp(end, tz=self.timezone).strftime(
|
||||
"%Y-%m-%d %H:%M"
|
||||
)
|
||||
self.cql_time_filter = f" and lastmodified >= '{formatted_start_time}'"
|
||||
|
||||
@@ -134,6 +134,32 @@ class OnyxConfluence(Confluence):
|
||||
super(OnyxConfluence, self).__init__(url, *args, **kwargs)
|
||||
self._wrap_methods()
|
||||
|
||||
def get_current_user(self, expand: str | None = None) -> Any:
|
||||
"""
|
||||
Implements a method that isn't in the third party client.
|
||||
|
||||
Get information about the current user
|
||||
:param expand: OPTIONAL expand for get status of user.
|
||||
Possible param is "status". Results are "Active, Deactivated"
|
||||
:return: Returns the user details
|
||||
"""
|
||||
|
||||
from atlassian.errors import ApiPermissionError # type:ignore
|
||||
|
||||
url = "rest/api/user/current"
|
||||
params = {}
|
||||
if expand:
|
||||
params["expand"] = expand
|
||||
try:
|
||||
response = self.get(url, params=params)
|
||||
except HTTPError as e:
|
||||
if e.response.status_code == 403:
|
||||
raise ApiPermissionError(
|
||||
"The calling user does not have permission", reason=e
|
||||
)
|
||||
raise
|
||||
return response
|
||||
|
||||
def _wrap_methods(self) -> None:
|
||||
"""
|
||||
For each attribute that is callable (i.e., a method) and doesn't start with an underscore,
|
||||
@@ -306,6 +332,13 @@ def _validate_connector_configuration(
|
||||
)
|
||||
spaces = confluence_client_with_minimal_retries.get_all_spaces(limit=1)
|
||||
|
||||
# uncomment the following for testing
|
||||
# the following is an attempt to retrieve the user's timezone
|
||||
# Unfornately, all data is returned in UTC regardless of the user's time zone
|
||||
# even tho CQL parses incoming times based on the user's time zone
|
||||
# space_key = spaces["results"][0]["key"]
|
||||
# space_details = confluence_client_with_minimal_retries.cql(f"space.key={space_key}+AND+type=space")
|
||||
|
||||
if not spaces:
|
||||
raise RuntimeError(
|
||||
f"No spaces found at {wiki_base}! "
|
||||
|
||||
@@ -415,9 +415,6 @@ def upsert_prompt(
|
||||
return prompt
|
||||
|
||||
|
||||
# NOTE: This operation cannot update persona configuration options that
|
||||
# are core to the persona, such as its display priority and
|
||||
# whether or not the assistant is a built-in / default assistant
|
||||
def upsert_persona(
|
||||
user: User | None,
|
||||
name: str,
|
||||
@@ -449,6 +446,12 @@ def upsert_persona(
|
||||
chunks_above: int = CONTEXT_CHUNKS_ABOVE,
|
||||
chunks_below: int = CONTEXT_CHUNKS_BELOW,
|
||||
) -> Persona:
|
||||
"""
|
||||
NOTE: This operation cannot update persona configuration options that
|
||||
are core to the persona, such as its display priority and
|
||||
whether or not the assistant is a built-in / default assistant
|
||||
"""
|
||||
|
||||
if persona_id is not None:
|
||||
persona = db_session.query(Persona).filter_by(id=persona_id).first()
|
||||
else:
|
||||
@@ -486,6 +489,8 @@ def upsert_persona(
|
||||
validate_persona_tools(tools)
|
||||
|
||||
if persona:
|
||||
# Built-in personas can only be updated through YAML configuration.
|
||||
# This ensures that core system personas are not modified unintentionally.
|
||||
if persona.builtin_persona and not builtin_persona:
|
||||
raise ValueError("Cannot update builtin persona with non-builtin.")
|
||||
|
||||
@@ -494,6 +499,9 @@ def upsert_persona(
|
||||
db_session=db_session, persona_id=persona.id, user=user, get_editable=True
|
||||
)
|
||||
|
||||
# The following update excludes `default`, `built-in`, and display priority.
|
||||
# Display priority is handled separately in the `display-priority` endpoint.
|
||||
# `default` and `built-in` properties can only be set when creating a persona.
|
||||
persona.name = name
|
||||
persona.description = description
|
||||
persona.num_chunks = num_chunks
|
||||
|
||||
@@ -71,6 +71,7 @@ def get_llms_for_persona(
|
||||
api_base=llm_provider.api_base,
|
||||
api_version=llm_provider.api_version,
|
||||
custom_config=llm_provider.custom_config,
|
||||
temperature=temperature_override,
|
||||
additional_headers=additional_headers,
|
||||
long_term_logger=long_term_logger,
|
||||
)
|
||||
@@ -128,11 +129,13 @@ def get_llm(
|
||||
api_base: str | None = None,
|
||||
api_version: str | None = None,
|
||||
custom_config: dict[str, str] | None = None,
|
||||
temperature: float = GEN_AI_TEMPERATURE,
|
||||
temperature: float | None = None,
|
||||
timeout: int = QA_TIMEOUT,
|
||||
additional_headers: dict[str, str] | None = None,
|
||||
long_term_logger: LongTermLogger | None = None,
|
||||
) -> LLM:
|
||||
if temperature is None:
|
||||
temperature = GEN_AI_TEMPERATURE
|
||||
return DefaultMultiLLM(
|
||||
model_provider=provider,
|
||||
model_name=model,
|
||||
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
|
||||
from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
from danswer.db.document import (
|
||||
construct_document_select_for_connector_credential_pair_by_needs_sync,
|
||||
@@ -105,7 +106,7 @@ class RedisConnectorCredentialPair(RedisObjectHelper):
|
||||
|
||||
# Priority on sync's triggered by new indexing should be medium
|
||||
result = celery_app.send_task(
|
||||
"vespa_metadata_sync_task",
|
||||
DanswerCeleryTask.VESPA_METADATA_SYNC_TASK,
|
||||
kwargs=dict(document_id=doc.id, tenant_id=tenant_id),
|
||||
queue=DanswerCeleryQueues.VESPA_METADATA_SYNC,
|
||||
task_id=custom_task_id,
|
||||
|
||||
@@ -12,6 +12,7 @@ from sqlalchemy.orm import Session
|
||||
from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
from danswer.db.document import construct_document_select_for_connector_credential_pair
|
||||
from danswer.db.models import Document as DbDocument
|
||||
@@ -114,7 +115,7 @@ class RedisConnectorDelete:
|
||||
|
||||
# Priority on sync's triggered by new indexing should be medium
|
||||
result = celery_app.send_task(
|
||||
"document_by_cc_pair_cleanup_task",
|
||||
DanswerCeleryTask.DOCUMENT_BY_CC_PAIR_CLEANUP_TASK,
|
||||
kwargs=dict(
|
||||
document_id=doc.id,
|
||||
connector_id=cc_pair.connector_id,
|
||||
|
||||
@@ -12,6 +12,7 @@ from danswer.access.models import DocExternalAccess
|
||||
from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
|
||||
|
||||
class RedisConnectorPermissionSyncPayload(BaseModel):
|
||||
@@ -149,7 +150,7 @@ class RedisConnectorPermissionSync:
|
||||
self.redis.sadd(self.taskset_key, custom_task_id)
|
||||
|
||||
result = celery_app.send_task(
|
||||
"update_external_document_permissions_task",
|
||||
DanswerCeleryTask.UPDATE_EXTERNAL_DOCUMENT_PERMISSIONS_TASK,
|
||||
kwargs=dict(
|
||||
tenant_id=self.tenant_id,
|
||||
serialized_doc_external_access=doc_perm.to_dict(),
|
||||
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy.orm import Session
|
||||
from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.db.connector_credential_pair import get_connector_credential_pair_from_id
|
||||
|
||||
|
||||
@@ -134,7 +135,7 @@ class RedisConnectorPrune:
|
||||
|
||||
# Priority on sync's triggered by new indexing should be medium
|
||||
result = celery_app.send_task(
|
||||
"document_by_cc_pair_cleanup_task",
|
||||
DanswerCeleryTask.DOCUMENT_BY_CC_PAIR_CLEANUP_TASK,
|
||||
kwargs=dict(
|
||||
document_id=doc_id,
|
||||
connector_id=cc_pair.connector_id,
|
||||
|
||||
@@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
|
||||
from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.db.document_set import construct_document_select_by_docset
|
||||
from danswer.redis.redis_object_helper import RedisObjectHelper
|
||||
|
||||
@@ -76,7 +77,7 @@ class RedisDocumentSet(RedisObjectHelper):
|
||||
redis_client.sadd(self.taskset_key, custom_task_id)
|
||||
|
||||
result = celery_app.send_task(
|
||||
"vespa_metadata_sync_task",
|
||||
DanswerCeleryTask.VESPA_METADATA_SYNC_TASK,
|
||||
kwargs=dict(document_id=doc.id, tenant_id=tenant_id),
|
||||
queue=DanswerCeleryQueues.VESPA_METADATA_SYNC,
|
||||
task_id=custom_task_id,
|
||||
|
||||
@@ -11,6 +11,7 @@ from sqlalchemy.orm import Session
|
||||
from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryQueues
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.redis.redis_object_helper import RedisObjectHelper
|
||||
from danswer.utils.variable_functionality import fetch_versioned_implementation
|
||||
from danswer.utils.variable_functionality import global_version
|
||||
@@ -89,7 +90,7 @@ class RedisUserGroup(RedisObjectHelper):
|
||||
redis_client.sadd(self.taskset_key, custom_task_id)
|
||||
|
||||
result = celery_app.send_task(
|
||||
"vespa_metadata_sync_task",
|
||||
DanswerCeleryTask.VESPA_METADATA_SYNC_TASK,
|
||||
kwargs=dict(document_id=doc.id, tenant_id=tenant_id),
|
||||
queue=DanswerCeleryQueues.VESPA_METADATA_SYNC,
|
||||
task_id=custom_task_id,
|
||||
|
||||
@@ -20,6 +20,7 @@ from danswer.background.celery.celery_utils import get_deletion_attempt_snapshot
|
||||
from danswer.background.celery.versioned_apps.primary import app as primary_app
|
||||
from danswer.configs.app_configs import ENABLED_CONNECTOR_TYPES
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.configs.constants import DocumentSource
|
||||
from danswer.configs.constants import FileOrigin
|
||||
from danswer.connectors.google_utils.google_auth import (
|
||||
@@ -867,7 +868,7 @@ def connector_run_once(
|
||||
|
||||
# run the beat task to pick up the triggers immediately
|
||||
primary_app.send_task(
|
||||
"check_for_indexing",
|
||||
DanswerCeleryTask.CHECK_FOR_INDEXING,
|
||||
priority=DanswerCeleryPriority.HIGH,
|
||||
kwargs={"tenant_id": tenant_id},
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ from danswer.auth.users import current_curator_or_admin_user
|
||||
from danswer.background.celery.versioned_apps.primary import app as primary_app
|
||||
from danswer.configs.app_configs import GENERATIVE_MODEL_ACCESS_CHECK_FREQ
|
||||
from danswer.configs.constants import DanswerCeleryPriority
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
from danswer.configs.constants import DocumentSource
|
||||
from danswer.configs.constants import KV_GEN_AI_KEY_CHECK_TIME
|
||||
from danswer.db.connector_credential_pair import get_connector_credential_pair
|
||||
@@ -199,7 +200,7 @@ def create_deletion_attempt_for_connector_id(
|
||||
|
||||
# run the beat task to pick up this deletion from the db immediately
|
||||
primary_app.send_task(
|
||||
"check_for_connector_deletion_task",
|
||||
DanswerCeleryTask.CHECK_FOR_CONNECTOR_DELETION,
|
||||
priority=DanswerCeleryPriority.HIGH,
|
||||
kwargs={"tenant_id": tenant_id},
|
||||
)
|
||||
|
||||
@@ -77,6 +77,7 @@ def llm_doc_from_internet_search_result(result: InternetSearchResult) -> LlmDoc:
|
||||
updated_at=datetime.now(),
|
||||
link=result.link,
|
||||
source_links={0: result.link},
|
||||
match_highlights=[],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -4,16 +4,17 @@ from typing import Any
|
||||
from danswer.background.celery.tasks.beat_schedule import (
|
||||
tasks_to_schedule as base_tasks_to_schedule,
|
||||
)
|
||||
from danswer.configs.constants import DanswerCeleryTask
|
||||
|
||||
ee_tasks_to_schedule = [
|
||||
{
|
||||
"name": "autogenerate_usage_report",
|
||||
"task": "autogenerate_usage_report_task",
|
||||
"task": DanswerCeleryTask.AUTOGENERATE_USAGE_REPORT_TASK,
|
||||
"schedule": timedelta(days=30), # TODO: change this to config flag
|
||||
},
|
||||
{
|
||||
"name": "check-ttl-management",
|
||||
"task": "check_ttl_management_task",
|
||||
"task": DanswerCeleryTask.CHECK_TTL_MANAGEMENT_TASK,
|
||||
"schedule": timedelta(hours=1),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3,7 +3,7 @@ cohere==5.6.1
|
||||
fastapi==0.109.2
|
||||
google-cloud-aiplatform==1.58.0
|
||||
numpy==1.26.4
|
||||
openai==1.52.2
|
||||
openai==1.55.3
|
||||
pydantic==2.8.2
|
||||
retry==0.9.2
|
||||
safetensors==0.4.2
|
||||
|
||||
@@ -44,6 +44,7 @@ def test_persona_category_management(reset: None) -> None:
|
||||
category=updated_persona_category,
|
||||
user_performing_action=regular_user,
|
||||
)
|
||||
assert exc_info.value.response is not None
|
||||
assert exc_info.value.response.status_code == 403
|
||||
|
||||
assert PersonaCategoryManager.verify(
|
||||
|
||||
@@ -64,6 +64,7 @@ def mock_search_results() -> list[LlmDoc]:
|
||||
updated_at=datetime(2023, 1, 1),
|
||||
link="https://example.com/doc1",
|
||||
source_links={0: "https://example.com/doc1"},
|
||||
match_highlights=[],
|
||||
),
|
||||
LlmDoc(
|
||||
content="Search result 2",
|
||||
@@ -75,6 +76,7 @@ def mock_search_results() -> list[LlmDoc]:
|
||||
updated_at=datetime(2023, 1, 2),
|
||||
link="https://example.com/doc2",
|
||||
source_links={0: "https://example.com/doc2"},
|
||||
match_highlights=[],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ mock_docs = [
|
||||
updated_at=datetime.now(),
|
||||
link=f"https://{int(id/2)}.com" if int(id / 2) % 2 == 0 else None,
|
||||
source_links={0: "https://mintlify.com/docs/settings/broken-links"},
|
||||
match_highlights=[],
|
||||
)
|
||||
for id in range(10)
|
||||
]
|
||||
|
||||
@@ -20,6 +20,7 @@ mock_docs = [
|
||||
updated_at=datetime.now(),
|
||||
link=f"https://{int(id/2)}.com" if int(id / 2) % 2 == 0 else None,
|
||||
source_links={0: "https://mintlify.com/docs/settings/broken-links"},
|
||||
match_highlights=[],
|
||||
)
|
||||
for id in range(10)
|
||||
]
|
||||
|
||||
@@ -89,3 +89,6 @@ export const getProviderIcon = (providerName: string, modelName?: string) => {
|
||||
return CPUIcon;
|
||||
}
|
||||
};
|
||||
|
||||
export const isAnthropic = (provider: string, modelName: string) =>
|
||||
provider === "anthropic" || modelName.toLowerCase().includes("claude");
|
||||
|
||||
@@ -70,7 +70,7 @@ import { StarterMessages } from "../../components/assistants/StarterMessage";
|
||||
import {
|
||||
AnswerPiecePacket,
|
||||
DanswerDocument,
|
||||
FinalContextDocs,
|
||||
DocumentInfoPacket,
|
||||
StreamStopInfo,
|
||||
StreamStopReason,
|
||||
} from "@/lib/search/interfaces";
|
||||
@@ -109,6 +109,7 @@ import AssistantBanner from "../../components/assistants/AssistantBanner";
|
||||
import TextView from "@/components/chat_search/TextView";
|
||||
import AssistantSelector from "@/components/chat_search/AssistantSelector";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { createPostponedAbortSignal } from "next/dist/server/app-render/dynamic-rendering";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
@@ -411,7 +412,7 @@ export function ChatPage({
|
||||
|
||||
// reset LLM overrides (based on chat session!)
|
||||
llmOverrideManager.updateModelOverrideForChatSession(selectedChatSession);
|
||||
llmOverrideManager.setTemperature(null);
|
||||
llmOverrideManager.updateTemperature(null);
|
||||
|
||||
// remove uploaded files
|
||||
setCurrentMessageFiles([]);
|
||||
@@ -921,7 +922,6 @@ export function ChatPage({
|
||||
setHasPerformedInitialScroll(true);
|
||||
}, 100);
|
||||
} else {
|
||||
console.log("All messages are already rendered, scrolling immediately");
|
||||
// If all messages are already rendered, scroll immediately
|
||||
endDivRef.current.scrollIntoView({
|
||||
behavior: fast ? "auto" : "smooth",
|
||||
@@ -974,6 +974,16 @@ export function ChatPage({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(!selectedDocuments || selectedDocuments.length === 0) &&
|
||||
documentSidebarToggled &&
|
||||
!filtersToggled
|
||||
) {
|
||||
setDocumentSidebarToggled(false);
|
||||
}
|
||||
}, [selectedDocuments, filtersToggled]);
|
||||
|
||||
useEffect(() => {
|
||||
adjustDocumentSidebarWidth(); // Adjust the width on initial render
|
||||
window.addEventListener("resize", adjustDocumentSidebarWidth); // Add resize event listener
|
||||
@@ -1252,7 +1262,6 @@ export function ChatPage({
|
||||
if (!packet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!initialFetchDetails) {
|
||||
if (!Object.hasOwn(packet, "user_message_id")) {
|
||||
console.error(
|
||||
@@ -1326,8 +1335,8 @@ export function ChatPage({
|
||||
|
||||
if (Object.hasOwn(packet, "answer_piece")) {
|
||||
answer += (packet as AnswerPiecePacket).answer_piece;
|
||||
} else if (Object.hasOwn(packet, "final_context_docs")) {
|
||||
documents = (packet as FinalContextDocs).final_context_docs;
|
||||
} else if (Object.hasOwn(packet, "top_documents")) {
|
||||
documents = (packet as DocumentInfoPacket).top_documents;
|
||||
retrievalType = RetrievalType.Search;
|
||||
if (documents && documents.length > 0) {
|
||||
// point to the latest message (we don't know the messageId yet, which is why
|
||||
|
||||
@@ -14,7 +14,6 @@ import { destructureValue, getFinalLLM, structureValue } from "@/lib/llm/utils";
|
||||
import { useState } from "react";
|
||||
import { Hoverable } from "@/components/Hoverable";
|
||||
import { Popover } from "@/components/popover/Popover";
|
||||
import { StarFeedback } from "@/components/icons/icons";
|
||||
import { IconType } from "react-icons";
|
||||
import { FiRefreshCw } from "react-icons/fi";
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
SendIcon,
|
||||
StopGeneratingIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { DanswerDocument } from "@/lib/search/interfaces";
|
||||
import { DanswerDocument, SourceMetadata } from "@/lib/search/interfaces";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -37,9 +37,41 @@ import { AssistantsTab } from "../modal/configuration/AssistantsTab";
|
||||
import { IconType } from "react-icons";
|
||||
import { LlmTab } from "../modal/configuration/LlmTab";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { FilterPills } from "./FilterPills";
|
||||
import { Tag } from "@/lib/types";
|
||||
import FiltersDisplay from "./FilterDisplay";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
interface ChatInputBarProps {
|
||||
removeFilters: () => void;
|
||||
removeDocs: () => void;
|
||||
openModelSettings: () => void;
|
||||
showDocs: () => void;
|
||||
showConfigureAPIKey: () => void;
|
||||
selectedDocuments: DanswerDocument[];
|
||||
message: string;
|
||||
setMessage: (message: string) => void;
|
||||
stopGenerating: () => void;
|
||||
onSubmit: () => void;
|
||||
filterManager: FilterManager;
|
||||
llmOverrideManager: LlmOverrideManager;
|
||||
chatState: ChatState;
|
||||
alternativeAssistant: Persona | null;
|
||||
inputPrompts: InputPrompt[];
|
||||
// assistants
|
||||
selectedAssistant: Persona;
|
||||
setSelectedAssistant: (assistant: Persona) => void;
|
||||
setAlternativeAssistant: (alternativeAssistant: Persona | null) => void;
|
||||
|
||||
files: FileDescriptor[];
|
||||
setFiles: (files: FileDescriptor[]) => void;
|
||||
handleFileUpload: (files: File[]) => void;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
chatSessionId?: string;
|
||||
toggleFilters?: () => void;
|
||||
}
|
||||
|
||||
export function ChatInputBar({
|
||||
removeFilters,
|
||||
removeDocs,
|
||||
@@ -68,32 +100,7 @@ export function ChatInputBar({
|
||||
chatSessionId,
|
||||
inputPrompts,
|
||||
toggleFilters,
|
||||
}: {
|
||||
removeFilters: () => void;
|
||||
removeDocs: () => void;
|
||||
showConfigureAPIKey: () => void;
|
||||
openModelSettings: () => void;
|
||||
chatState: ChatState;
|
||||
stopGenerating: () => void;
|
||||
showDocs: () => void;
|
||||
selectedDocuments: DanswerDocument[];
|
||||
setAlternativeAssistant: (alternativeAssistant: Persona | null) => void;
|
||||
setSelectedAssistant: (assistant: Persona) => void;
|
||||
inputPrompts: InputPrompt[];
|
||||
message: string;
|
||||
setMessage: (message: string) => void;
|
||||
onSubmit: () => void;
|
||||
filterManager: FilterManager;
|
||||
llmOverrideManager: LlmOverrideManager;
|
||||
selectedAssistant: Persona;
|
||||
alternativeAssistant: Persona | null;
|
||||
files: FileDescriptor[];
|
||||
setFiles: (files: FileDescriptor[]) => void;
|
||||
handleFileUpload: (files: File[]) => void;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
chatSessionId?: string;
|
||||
toggleFilters?: () => void;
|
||||
}) {
|
||||
}: ChatInputBarProps) {
|
||||
useEffect(() => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (textarea) {
|
||||
@@ -340,23 +347,26 @@ export function ChatInputBar({
|
||||
className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full"
|
||||
>
|
||||
<div className="rounded-lg py-1.5 bg-white border border-border-medium overflow-hidden shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
|
||||
{filteredPrompts.map((currentPrompt, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`px-2 ${
|
||||
tabbingIconIndex == index && "bg-hover"
|
||||
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-hover cursor-pointer`}
|
||||
onClick={() => {
|
||||
updateInputPrompt(currentPrompt);
|
||||
}}
|
||||
>
|
||||
<p className="font-bold">{currentPrompt.prompt}:</p>
|
||||
<p className="text-left flex-grow mr-auto line-clamp-1">
|
||||
{currentPrompt.id == selectedAssistant.id && "(default) "}
|
||||
{currentPrompt.content?.trim()}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
{filteredPrompts.map(
|
||||
(currentPrompt: InputPrompt, index: number) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`px-2 ${
|
||||
tabbingIconIndex == index && "bg-hover"
|
||||
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-hover cursor-pointer`}
|
||||
onClick={() => {
|
||||
updateInputPrompt(currentPrompt);
|
||||
}}
|
||||
>
|
||||
<p className="font-bold">{currentPrompt.prompt}:</p>
|
||||
<p className="text-left flex-grow mr-auto line-clamp-1">
|
||||
{currentPrompt.id == selectedAssistant.id &&
|
||||
"(default) "}
|
||||
{currentPrompt.content?.trim()}
|
||||
</p>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
|
||||
<a
|
||||
key={filteredPrompts.length}
|
||||
@@ -430,6 +440,7 @@ export function ChatInputBar({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(selectedDocuments.length > 0 || files.length > 0) && (
|
||||
<div className="flex gap-x-2 px-2 pt-2">
|
||||
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
|
||||
@@ -564,6 +575,16 @@ export function ChatInputBar({
|
||||
onClick={toggleFilters}
|
||||
/>
|
||||
)}
|
||||
{(filterManager.selectedSources.length > 0 ||
|
||||
filterManager.selectedDocumentSets.length > 0 ||
|
||||
filterManager.selectedTags.length > 0 ||
|
||||
filterManager.timeRange) &&
|
||||
toggleFilters && (
|
||||
<FiltersDisplay
|
||||
filterManager={filterManager}
|
||||
toggleFilters={toggleFilters}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">
|
||||
|
||||
109
web/src/app/chat/input/FilterDisplay.tsx
Normal file
109
web/src/app/chat/input/FilterDisplay.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from "react";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { FilterPills } from "./FilterPills";
|
||||
import { SourceMetadata } from "@/lib/search/interfaces";
|
||||
import { FilterManager } from "@/lib/hooks";
|
||||
import { Tag } from "@/lib/types";
|
||||
|
||||
interface FiltersDisplayProps {
|
||||
filterManager: FilterManager;
|
||||
toggleFilters: () => void;
|
||||
}
|
||||
export default function FiltersDisplay({
|
||||
filterManager,
|
||||
toggleFilters,
|
||||
}: FiltersDisplayProps) {
|
||||
return (
|
||||
<div className="flex my-auto flex-wrap gap-2 px-2">
|
||||
{(() => {
|
||||
const allFilters = [
|
||||
...filterManager.selectedSources,
|
||||
...filterManager.selectedDocumentSets,
|
||||
...filterManager.selectedTags,
|
||||
...(filterManager.timeRange ? [filterManager.timeRange] : []),
|
||||
];
|
||||
const filtersToShow = allFilters.slice(0, 2);
|
||||
const remainingFilters = allFilters.length - 2;
|
||||
|
||||
return (
|
||||
<>
|
||||
{filtersToShow.map((filter, index) => {
|
||||
if (typeof filter === "object" && "displayName" in filter) {
|
||||
return (
|
||||
<FilterPills<SourceMetadata>
|
||||
key={index}
|
||||
item={filter}
|
||||
itemToString={(source) => source.displayName}
|
||||
onRemove={(source) =>
|
||||
filterManager.setSelectedSources((prev) =>
|
||||
prev.filter(
|
||||
(s) => s.internalName !== source.internalName
|
||||
)
|
||||
)
|
||||
}
|
||||
toggleFilters={toggleFilters}
|
||||
/>
|
||||
);
|
||||
} else if (typeof filter === "string") {
|
||||
return (
|
||||
<FilterPills<string>
|
||||
key={index}
|
||||
item={filter}
|
||||
itemToString={(set) => set}
|
||||
onRemove={(set) =>
|
||||
filterManager.setSelectedDocumentSets((prev) =>
|
||||
prev.filter((s) => s !== set)
|
||||
)
|
||||
}
|
||||
toggleFilters={toggleFilters}
|
||||
/>
|
||||
);
|
||||
} else if ("tag_key" in filter) {
|
||||
return (
|
||||
<FilterPills<Tag>
|
||||
key={index}
|
||||
item={filter}
|
||||
itemToString={(tag) => `${tag.tag_key}:${tag.tag_value}`}
|
||||
onRemove={(tag) =>
|
||||
filterManager.setSelectedTags((prev) =>
|
||||
prev.filter(
|
||||
(t) =>
|
||||
t.tag_key !== tag.tag_key ||
|
||||
t.tag_value !== tag.tag_value
|
||||
)
|
||||
)
|
||||
}
|
||||
toggleFilters={toggleFilters}
|
||||
/>
|
||||
);
|
||||
} else if ("from" in filter && "to" in filter) {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center bg-background-150 rounded-full px-3 py-1 text-sm"
|
||||
>
|
||||
<span>
|
||||
{filter.from.toLocaleDateString()} -{" "}
|
||||
{filter.to.toLocaleDateString()}
|
||||
</span>
|
||||
<XIcon
|
||||
onClick={() => filterManager.setTimeRange(null)}
|
||||
size={16}
|
||||
className="ml-2 text-text-400 hover:text-text-600 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
{remainingFilters > 0 && (
|
||||
<div className="flex items-center bg-background-150 rounded-full px-3 py-1 text-sm">
|
||||
<span>+{remainingFilters} more</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
web/src/app/chat/input/FilterPills.tsx
Normal file
39
web/src/app/chat/input/FilterPills.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { SourceMetadata } from "@/lib/search/interfaces";
|
||||
import { Tag } from "@/lib/types";
|
||||
|
||||
type FilterItem = SourceMetadata | string | Tag;
|
||||
|
||||
interface FilterPillsProps<T extends FilterItem> {
|
||||
item: T;
|
||||
itemToString: (item: T) => string;
|
||||
onRemove: (item: T) => void;
|
||||
toggleFilters?: () => void;
|
||||
}
|
||||
|
||||
export function FilterPills<T extends FilterItem>({
|
||||
item,
|
||||
itemToString,
|
||||
onRemove,
|
||||
toggleFilters,
|
||||
}: FilterPillsProps<T>) {
|
||||
return (
|
||||
<button
|
||||
onClick={toggleFilters}
|
||||
className="cursor-pointer flex flex-wrap gap-2"
|
||||
>
|
||||
<div className="flex items-center bg-background-150 rounded-full px-3 py-1 text-sm">
|
||||
<span>{itemToString(item)}</span>
|
||||
<XIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove(item);
|
||||
}}
|
||||
size={16}
|
||||
className="ml-2 text-text-400 hover:text-text-600 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
AnswerPiecePacket,
|
||||
DanswerDocument,
|
||||
Filters,
|
||||
FinalContextDocs,
|
||||
DocumentInfoPacket,
|
||||
StreamStopInfo,
|
||||
} from "@/lib/search/interfaces";
|
||||
import { handleSSEStream } from "@/lib/search/streamingUtils";
|
||||
@@ -103,7 +103,7 @@ export type PacketType =
|
||||
| ToolCallMetadata
|
||||
| BackendMessage
|
||||
| AnswerPiecePacket
|
||||
| FinalContextDocs
|
||||
| DocumentInfoPacket
|
||||
| DocumentsResponse
|
||||
| FileChatDisplay
|
||||
| StreamingError
|
||||
|
||||
@@ -35,25 +35,9 @@ export const LlmTab = forwardRef<HTMLDivElement, LlmTabProps>(
|
||||
checkPersonaRequiresImageGeneration(currentAssistant);
|
||||
|
||||
const { llmProviders } = useChatContext();
|
||||
const { setLlmOverride, temperature, setTemperature } = llmOverrideManager;
|
||||
const { setLlmOverride, temperature, updateTemperature } =
|
||||
llmOverrideManager;
|
||||
const [isTemperatureExpanded, setIsTemperatureExpanded] = useState(false);
|
||||
const [localTemperature, setLocalTemperature] = useState<number>(
|
||||
temperature || 0
|
||||
);
|
||||
const debouncedSetTemperature = useCallback(
|
||||
(value: number) => {
|
||||
const debouncedFunction = debounce((value: number) => {
|
||||
setTemperature(value);
|
||||
}, 300);
|
||||
return debouncedFunction(value);
|
||||
},
|
||||
[setTemperature]
|
||||
);
|
||||
|
||||
const handleTemperatureChange = (value: number) => {
|
||||
setLocalTemperature(value);
|
||||
debouncedSetTemperature(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -108,26 +92,26 @@ export const LlmTab = forwardRef<HTMLDivElement, LlmTabProps>(
|
||||
<input
|
||||
type="range"
|
||||
onChange={(e) =>
|
||||
handleTemperatureChange(parseFloat(e.target.value))
|
||||
updateTemperature(parseFloat(e.target.value))
|
||||
}
|
||||
className="w-full p-2 border border-border rounded-md"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={localTemperature}
|
||||
value={temperature || 0}
|
||||
/>
|
||||
<div
|
||||
className="absolute text-sm"
|
||||
style={{
|
||||
left: `${(localTemperature || 0) * 50}%`,
|
||||
left: `${(temperature || 0) * 50}%`,
|
||||
transform: `translateX(-${Math.min(
|
||||
Math.max((localTemperature || 0) * 50, 10),
|
||||
Math.max((temperature || 0) * 50, 10),
|
||||
90
|
||||
)}%)`,
|
||||
top: "-1.5rem",
|
||||
}}
|
||||
>
|
||||
{localTemperature}
|
||||
{temperature}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -96,7 +96,7 @@ export function SourceSelector({
|
||||
});
|
||||
};
|
||||
|
||||
let allSourcesSelected = selectedSources.length > 0;
|
||||
let allSourcesSelected = selectedSources.length == existingSources.length;
|
||||
|
||||
const toggleAllSources = () => {
|
||||
if (allSourcesSelected) {
|
||||
|
||||
@@ -46,7 +46,7 @@ const AssistantSelector = ({
|
||||
liveAssistant: Persona;
|
||||
onAssistantChange: (assistant: Persona) => void;
|
||||
chatSessionId?: string;
|
||||
llmOverrideManager?: LlmOverrideManager;
|
||||
llmOverrideManager: LlmOverrideManager;
|
||||
isMobile: boolean;
|
||||
}) => {
|
||||
const { finalAssistants } = useAssistants();
|
||||
@@ -54,11 +54,9 @@ const AssistantSelector = ({
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const { llmProviders } = useChatContext();
|
||||
const { user } = useUser();
|
||||
|
||||
const [assistants, setAssistants] = useState<Persona[]>(finalAssistants);
|
||||
const [isTemperatureExpanded, setIsTemperatureExpanded] = useState(false);
|
||||
const [localTemperature, setLocalTemperature] = useState<number>(
|
||||
llmOverrideManager?.temperature || 0
|
||||
);
|
||||
|
||||
// Initialize selectedTab from localStorage
|
||||
const [selectedTab, setSelectedTab] = useState<number>(() => {
|
||||
@@ -92,21 +90,6 @@ const AssistantSelector = ({
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedSetTemperature = useCallback(
|
||||
(value: number) => {
|
||||
const debouncedFunction = debounce((value: number) => {
|
||||
llmOverrideManager?.setTemperature(value);
|
||||
}, 300);
|
||||
return debouncedFunction(value);
|
||||
},
|
||||
[llmOverrideManager]
|
||||
);
|
||||
|
||||
const handleTemperatureChange = (value: number) => {
|
||||
setLocalTemperature(value);
|
||||
debouncedSetTemperature(value);
|
||||
};
|
||||
|
||||
// Handle tab change and update localStorage
|
||||
const handleTabChange = (index: number) => {
|
||||
setSelectedTab(index);
|
||||
@@ -119,7 +102,7 @@ const AssistantSelector = ({
|
||||
const [_, currentLlm] = getFinalLLM(
|
||||
llmProviders,
|
||||
liveAssistant,
|
||||
llmOverrideManager?.llmOverride ?? null
|
||||
llmOverrideManager.llmOverride ?? null
|
||||
);
|
||||
|
||||
const requiresImageGeneration =
|
||||
@@ -204,11 +187,10 @@ const AssistantSelector = ({
|
||||
llmProviders={llmProviders}
|
||||
currentLlm={currentLlm}
|
||||
userDefault={userDefaultModel}
|
||||
includeUserDefault={true}
|
||||
onSelect={(value: string | null) => {
|
||||
if (value == null) return;
|
||||
const { modelName, name, provider } = destructureValue(value);
|
||||
llmOverrideManager?.setLlmOverride({
|
||||
llmOverrideManager.setLlmOverride({
|
||||
name,
|
||||
provider,
|
||||
modelName,
|
||||
@@ -216,7 +198,6 @@ const AssistantSelector = ({
|
||||
if (chatSessionId) {
|
||||
updateModelOverrideForChatSession(chatSessionId, value);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
@@ -243,26 +224,31 @@ const AssistantSelector = ({
|
||||
<input
|
||||
type="range"
|
||||
onChange={(e) =>
|
||||
handleTemperatureChange(parseFloat(e.target.value))
|
||||
llmOverrideManager.updateTemperature(
|
||||
parseFloat(e.target.value)
|
||||
)
|
||||
}
|
||||
className="w-full p-2 border border-border rounded-md"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.01"
|
||||
value={localTemperature}
|
||||
value={llmOverrideManager.temperature?.toString() || "0"}
|
||||
/>
|
||||
<div
|
||||
className="absolute text-sm"
|
||||
style={{
|
||||
left: `${(localTemperature || 0) * 50}%`,
|
||||
left: `${(llmOverrideManager.temperature || 0) * 50}%`,
|
||||
transform: `translateX(-${Math.min(
|
||||
Math.max((localTemperature || 0) * 50, 10),
|
||||
Math.max(
|
||||
(llmOverrideManager.temperature || 0) * 50,
|
||||
10
|
||||
),
|
||||
90
|
||||
)}%)`,
|
||||
top: "-1.5rem",
|
||||
}}
|
||||
>
|
||||
{localTemperature}
|
||||
{llmOverrideManager.temperature}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
classifyAssistants,
|
||||
orderAssistantsForUser,
|
||||
getUserCreatedAssistants,
|
||||
filterAssistants,
|
||||
} from "@/lib/assistants/utils";
|
||||
import { useUser } from "../user/UserProvider";
|
||||
|
||||
@@ -145,22 +146,13 @@ export const AssistantsProvider: React.FC<{
|
||||
if (!response.ok) throw new Error("Failed to fetch assistants");
|
||||
let assistants: Persona[] = await response.json();
|
||||
|
||||
if (!hasImageCompatibleModel) {
|
||||
assistants = assistants.filter(
|
||||
(assistant) =>
|
||||
!assistant.tools.some(
|
||||
(tool) => tool.in_code_tool_id === "ImageGenerationTool"
|
||||
)
|
||||
);
|
||||
}
|
||||
let filteredAssistants = filterAssistants(
|
||||
assistants,
|
||||
hasAnyConnectors,
|
||||
hasImageCompatibleModel
|
||||
);
|
||||
|
||||
if (!hasAnyConnectors) {
|
||||
assistants = assistants.filter(
|
||||
(assistant) => assistant.num_chunks === 0
|
||||
);
|
||||
}
|
||||
|
||||
setAssistants(assistants);
|
||||
setAssistants(filteredAssistants);
|
||||
|
||||
// Fetch and update allAssistants for admins and curators
|
||||
await fetchPersonas();
|
||||
|
||||
@@ -19,7 +19,6 @@ interface LlmListProps {
|
||||
scrollable?: boolean;
|
||||
hideProviderIcon?: boolean;
|
||||
requiresImageGeneration?: boolean;
|
||||
includeUserDefault?: boolean;
|
||||
currentAssistant?: Persona;
|
||||
}
|
||||
|
||||
@@ -31,7 +30,6 @@ export const LlmList: React.FC<LlmListProps> = ({
|
||||
userDefault,
|
||||
scrollable,
|
||||
requiresImageGeneration,
|
||||
includeUserDefault = false,
|
||||
}) => {
|
||||
const llmOptionsByProvider: {
|
||||
[provider: string]: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { User } from "../types";
|
||||
import { checkUserIsNoAuthUser } from "../user";
|
||||
import { personaComparator } from "@/app/admin/assistants/lib";
|
||||
|
||||
export function checkUserOwnsAssistant(user: User | null, assistant: Persona) {
|
||||
return checkUserIdOwnsAssistant(user?.id, assistant);
|
||||
@@ -117,3 +118,31 @@ export function getUserCreatedAssistants(
|
||||
checkUserOwnsAssistant(user, assistant)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter assistants based on connector status, image compatibility and visibility
|
||||
export function filterAssistants(
|
||||
assistants: Persona[],
|
||||
hasAnyConnectors: boolean,
|
||||
hasImageCompatibleModel: boolean
|
||||
): Persona[] {
|
||||
let filteredAssistants = assistants.filter(
|
||||
(assistant) => assistant.is_visible
|
||||
);
|
||||
|
||||
if (!hasAnyConnectors) {
|
||||
filteredAssistants = filteredAssistants.filter(
|
||||
(assistant) => assistant.num_chunks === 0
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasImageCompatibleModel) {
|
||||
filteredAssistants = filteredAssistants.filter(
|
||||
(assistant) =>
|
||||
!assistant.tools.some(
|
||||
(tool) => tool.in_code_tool_id === "ImageGenerationTool"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return filteredAssistants.sort(personaComparator);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { personaComparator } from "@/app/admin/assistants/lib";
|
||||
import { fetchAssistantsSS } from "../assistants/fetchAssistantsSS";
|
||||
import { checkLLMSupportsImageInput } from "../llm/utils";
|
||||
import { filterAssistants } from "../assistants/utils";
|
||||
|
||||
interface AssistantData {
|
||||
assistants: Persona[];
|
||||
@@ -39,42 +40,21 @@ export async function fetchAssistantData(): Promise<AssistantData> {
|
||||
}),
|
||||
]);
|
||||
|
||||
// Process visible assistants
|
||||
let filteredAssistants = assistants.filter(
|
||||
(assistant) => assistant.is_visible
|
||||
);
|
||||
|
||||
// Process connector status
|
||||
const hasAnyConnectors = ccPairsResponse?.ok
|
||||
? (await ccPairsResponse.json()).length > 0
|
||||
: false;
|
||||
|
||||
// Filter assistants based on connector status
|
||||
if (!hasAnyConnectors) {
|
||||
filteredAssistants = filteredAssistants.filter(
|
||||
(assistant) => assistant.num_chunks === 0
|
||||
);
|
||||
}
|
||||
|
||||
// Sort assistants
|
||||
filteredAssistants.sort(personaComparator);
|
||||
|
||||
// Check for image-compatible models
|
||||
const hasImageCompatibleModel = llmProviders.some(
|
||||
(provider) =>
|
||||
provider.provider === "openai" ||
|
||||
provider.model_names.some((model) => checkLLMSupportsImageInput(model))
|
||||
);
|
||||
|
||||
// Filter out image generation tools if no compatible model
|
||||
if (!hasImageCompatibleModel) {
|
||||
filteredAssistants = filteredAssistants.filter(
|
||||
(assistant) =>
|
||||
!assistant.tools.some(
|
||||
(tool) => tool.in_code_tool_id === "ImageGenerationTool"
|
||||
)
|
||||
);
|
||||
}
|
||||
let filteredAssistants = filterAssistants(
|
||||
assistants,
|
||||
hasAnyConnectors,
|
||||
hasImageCompatibleModel
|
||||
);
|
||||
|
||||
return {
|
||||
assistants: filteredAssistants,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { UsersResponse } from "./users/interfaces";
|
||||
import { Credential } from "./connectors/credentials";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
import { PersonaCategory } from "@/app/admin/assistants/interfaces";
|
||||
import { isAnthropic } from "@/app/admin/configuration/llm/interfaces";
|
||||
|
||||
const CREDENTIAL_URL = "/api/manage/admin/credential";
|
||||
|
||||
@@ -71,7 +72,9 @@ export const useConnectorCredentialIndexingStatus = (
|
||||
getEditable = false
|
||||
) => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const url = `${INDEXING_STATUS_URL}${getEditable ? "?get_editable=true" : ""}`;
|
||||
const url = `${INDEXING_STATUS_URL}${
|
||||
getEditable ? "?get_editable=true" : ""
|
||||
}`;
|
||||
const swrResponse = useSWR<ConnectorIndexingStatus<any, any>[]>(
|
||||
url,
|
||||
errorHandlingFetcher,
|
||||
@@ -157,7 +160,7 @@ export interface LlmOverrideManager {
|
||||
globalDefault: LlmOverride;
|
||||
setGlobalDefault: React.Dispatch<React.SetStateAction<LlmOverride>>;
|
||||
temperature: number | null;
|
||||
setTemperature: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
updateTemperature: (temperature: number | null) => void;
|
||||
updateModelOverrideForChatSession: (chatSession?: ChatSession) => void;
|
||||
}
|
||||
export function useLlmOverride(
|
||||
@@ -212,6 +215,20 @@ export function useLlmOverride(
|
||||
setTemperature(defaultTemperature !== undefined ? defaultTemperature : 0);
|
||||
}, [defaultTemperature]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAnthropic(llmOverride.provider, llmOverride.modelName)) {
|
||||
setTemperature((prevTemp) => Math.min(prevTemp ?? 0, 1.0));
|
||||
}
|
||||
}, [llmOverride]);
|
||||
|
||||
const updateTemperature = (temperature: number | null) => {
|
||||
if (isAnthropic(llmOverride.provider, llmOverride.modelName)) {
|
||||
setTemperature((prevTemp) => Math.min(temperature ?? 0, 1.0));
|
||||
} else {
|
||||
setTemperature(temperature);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
updateModelOverrideForChatSession,
|
||||
llmOverride,
|
||||
@@ -219,9 +236,10 @@ export function useLlmOverride(
|
||||
globalDefault,
|
||||
setGlobalDefault,
|
||||
temperature,
|
||||
setTemperature,
|
||||
updateTemperature,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
EE Only APIs
|
||||
*/
|
||||
|
||||
@@ -19,10 +19,6 @@ export interface AnswerPiecePacket {
|
||||
answer_piece: string;
|
||||
}
|
||||
|
||||
export interface FinalContextDocs {
|
||||
final_context_docs: DanswerDocument[];
|
||||
}
|
||||
|
||||
export enum StreamStopReason {
|
||||
CONTEXT_LENGTH = "CONTEXT_LENGTH",
|
||||
CANCELLED = "CANCELLED",
|
||||
|
||||
@@ -5,9 +5,8 @@ import {
|
||||
import {
|
||||
AnswerPiecePacket,
|
||||
DanswerDocument,
|
||||
DocumentInfoPacket,
|
||||
ErrorMessagePacket,
|
||||
FinalContextDocs,
|
||||
DocumentInfoPacket,
|
||||
Quote,
|
||||
QuotesInfoPacket,
|
||||
RelevanceChunk,
|
||||
@@ -92,7 +91,7 @@ export const searchRequestStreamed = async ({
|
||||
| DocumentInfoPacket
|
||||
| LLMRelevanceFilterPacket
|
||||
| BackendMessage
|
||||
| FinalContextDocs
|
||||
| DocumentInfoPacket
|
||||
| RelevanceChunk
|
||||
>(decoder.decode(value, { stream: true }), previousPartialChunk);
|
||||
if (!completedChunks.length && !partialChunk) {
|
||||
|
||||
@@ -6,9 +6,12 @@ import { TEST_CREDENTIALS } from "./constants";
|
||||
setup("authenticate", async ({ page }) => {
|
||||
const { email, password } = TEST_CREDENTIALS;
|
||||
|
||||
await page.goto("http://localhost:3000/search");
|
||||
await page.goto("http://localhost:3000/chat");
|
||||
|
||||
await page.waitForURL("http://localhost:3000/auth/login?next=%2Fsearch");
|
||||
const url = page.url();
|
||||
console.log(`Initial URL after navigation: ${url}`);
|
||||
|
||||
await page.waitForURL("http://localhost:3000/auth/login?next=%2Fchat");
|
||||
|
||||
await expect(page).toHaveTitle("Danswer");
|
||||
|
||||
@@ -18,7 +21,7 @@ setup("authenticate", async ({ page }) => {
|
||||
// Click the login button
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await page.waitForURL("http://localhost:3000/search");
|
||||
await page.waitForURL("http://localhost:3000/chat");
|
||||
|
||||
await page.context().storageState({ path: "admin_auth.json" });
|
||||
});
|
||||
|
||||
@@ -8,12 +8,20 @@ test(
|
||||
async ({ page }, testInfo) => {
|
||||
// Test simple loading
|
||||
await page.goto("http://localhost:3000/chat");
|
||||
await expect(page.locator("div.text-2xl").nth(0)).toHaveText("General");
|
||||
await expect(page.getByRole("button", { name: "Search S" })).toHaveClass(
|
||||
/text-text-application-untoggled/
|
||||
);
|
||||
await expect(page.getByRole("button", { name: "Chat D" })).toHaveClass(
|
||||
/text-text-application-toggled/
|
||||
);
|
||||
|
||||
// Check for the "General" text in the new UI element
|
||||
await expect(
|
||||
page.locator("div.flex.items-center span.font-bold")
|
||||
).toHaveText("General");
|
||||
|
||||
// Check for the presence of the new UI element
|
||||
await expect(
|
||||
page.locator("div.flex.justify-center div.bg-black.rounded-full")
|
||||
).toBeVisible();
|
||||
|
||||
// Check for the SVG icon
|
||||
await expect(
|
||||
page.locator("div.flex.justify-center svg.w-5.h-5")
|
||||
).toBeVisible();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -12,9 +12,9 @@ test(
|
||||
// Test redirect to login, and redirect to search after login
|
||||
const { email, password } = TEST_CREDENTIALS;
|
||||
|
||||
await page.goto("http://localhost:3000/search");
|
||||
await page.goto("http://localhost:3000/chat");
|
||||
|
||||
await page.waitForURL("http://localhost:3000/auth/login?next=%2Fsearch");
|
||||
await page.waitForURL("http://localhost:3000/auth/login?next=%2Fchat");
|
||||
|
||||
await expect(page).toHaveTitle("Danswer");
|
||||
|
||||
@@ -26,6 +26,6 @@ test(
|
||||
// Click the login button
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await page.waitForURL("http://localhost:3000/search");
|
||||
await page.waitForURL("http://localhost:3000/chat");
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { test, expect } from "@chromatic-com/playwright";
|
||||
|
||||
test(
|
||||
"Search",
|
||||
{
|
||||
tag: "@admin",
|
||||
},
|
||||
async ({ page }, testInfo) => {
|
||||
// Test simple loading
|
||||
await page.goto("http://localhost:3000/search");
|
||||
await expect(page.locator("div.text-3xl")).toHaveText("Unlock Knowledge");
|
||||
await expect(page.getByRole("button", { name: "Search S" })).toHaveClass(
|
||||
/text-text-application-toggled/
|
||||
);
|
||||
await expect(page.getByRole("button", { name: "Chat D" })).toHaveClass(
|
||||
/text-text-application-untoggled/
|
||||
);
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user