Compare commits

..

47 Commits

Author SHA1 Message Date
pablodanswer
d97e96b3f0 build org 2024-12-01 17:20:43 -08:00
pablodanswer
911fbfa5a6 k 2024-12-01 17:14:09 -08:00
pablodanswer
d02305671a k 2024-12-01 17:12:54 -08:00
pablodanswer
bdfa29dcb5 slack chat 2024-12-01 15:06:32 -08:00
pablodanswer
897ed03c19 fix memoization 2024-12-01 15:02:46 -08:00
pablodanswer
49f0c4f1f8 fix memoization 2024-12-01 15:02:28 -08:00
pablodanswer
338c02171b rm shs 2024-12-01 12:46:07 -08:00
pablodanswer
ef1ade84b6 k 2024-12-01 12:46:07 -08:00
pablodanswer
7c81566c54 k 2024-12-01 12:46:07 -08:00
pablodanswer
c9df0aea47 k 2024-12-01 12:46:07 -08:00
pablodanswer
92e0aeecba k 2024-12-01 12:46:07 -08:00
pablodanswer
30c7e07783 update for all screen sizes 2024-12-01 12:46:07 -08:00
pablodanswer
e99704e9bd update sidebar line 2024-12-01 12:46:07 -08:00
pablodanswer
7f36387f7f k 2024-12-01 12:46:07 -08:00
pablodanswer
407592445b minor nit 2024-12-01 12:46:07 -08:00
pablodanswer
2e533d8188 minor date range clarity 2024-12-01 12:46:07 -08:00
pablodanswer
5b56869937 quick unification of icons 2024-12-01 12:46:07 -08:00
pablodanswer
7baeab54e2 address comments 2024-12-01 12:46:07 -08:00
pablodanswer
aefcfb75ef k 2024-12-01 12:46:07 -08:00
pablodanswer
e5adcb457d k 2024-12-01 12:46:07 -08:00
pablodanswer
db6463644a small nit 2024-12-01 12:46:07 -08:00
pablodanswer
e26ba70cc6 update filters 2024-12-01 12:46:07 -08:00
pablodanswer
66ff723c94 badge up 2024-12-01 12:46:07 -08:00
pablodanswer
dda66f2178 finalize changes 2024-12-01 12:46:07 -08:00
pablodanswer
0a27f72d20 cleanup complete 2024-12-01 12:46:07 -08:00
pablodanswer
fe397601ed minor cleanup 2024-12-01 12:46:07 -08:00
pablodanswer
3bc187c1d1 clean up unused components 2024-12-01 12:46:07 -08:00
pablodanswer
9a0b9eecf0 source types update 2024-12-01 12:46:07 -08:00
pablodanswer
e08db414c0 viewport height update 2024-12-01 12:46:07 -08:00
pablodanswer
b5734057b7 various updates 2024-12-01 12:46:07 -08:00
pablodanswer
56beb3ec82 k 2024-12-01 12:46:07 -08:00
pablodanswer
9f2c8118d7 updates 2024-12-01 12:46:07 -08:00
pablodanswer
6e4a3d5d57 finalize tags 2024-12-01 12:46:07 -08:00
pablodanswer
5b3dcf718f scroll nit 2024-12-01 12:46:07 -08:00
pablodanswer
07bd20b5b9 push fade 2024-12-01 12:46:07 -08:00
pablodanswer
eb01b175ae update logs 2024-12-01 12:46:06 -08:00
pablodanswer
6f55e5fe56 default 2024-12-01 12:46:06 -08:00
pablodanswer
18e7609bfc update scroll 2024-12-01 12:46:06 -08:00
pablodanswer
dd69ec6cdb cleanup 2024-12-01 12:46:06 -08:00
pablodanswer
e961fa2820 fix mystery reorg 2024-12-01 12:46:06 -08:00
pablodanswer
d41bf9a3ff clean up 2024-12-01 12:46:06 -08:00
pablodanswer
e3a6c76d51 k 2024-12-01 12:46:06 -08:00
pablodanswer
719c2aa0df update 2024-12-01 12:46:06 -08:00
pablodanswer
09f487e402 updates 2024-12-01 12:46:06 -08:00
pablodanswer
33a1548fc1 k 2024-12-01 12:46:06 -08:00
pablodanswer
e87c93226a updated chat flow 2024-12-01 12:46:06 -08:00
pablodanswer
5e11a79593 proper no assistant typing + no assistant modal 2024-12-01 12:46:06 -08:00
88 changed files with 464 additions and 1554 deletions

View File

@@ -24,8 +24,6 @@ env:
GOOGLE_DRIVE_OAUTH_CREDENTIALS_JSON_STR: ${{ secrets.GOOGLE_DRIVE_OAUTH_CREDENTIALS_JSON_STR }}
GOOGLE_GMAIL_SERVICE_ACCOUNT_JSON_STR: ${{ secrets.GOOGLE_GMAIL_SERVICE_ACCOUNT_JSON_STR }}
GOOGLE_GMAIL_OAUTH_CREDENTIALS_JSON_STR: ${{ secrets.GOOGLE_GMAIL_OAUTH_CREDENTIALS_JSON_STR }}
# Slab
SLAB_BOT_TOKEN: ${{ secrets.SLAB_BOT_TOKEN }}
jobs:
connectors-check:

View File

@@ -73,7 +73,6 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/* && \
rm -f /usr/local/lib/python3.11/site-packages/tornado/test/test.key
# Pre-downloading models for setups with limited egress
RUN python -c "from tokenizers import Tokenizer; \
Tokenizer.from_pretrained('nomic-ai/nomic-embed-text-v1')"

View File

@@ -1,5 +1,5 @@
from sqlalchemy.engine.base import Connection
from typing import Literal
from typing import Any
import asyncio
from logging.config import fileConfig
import logging
@@ -8,7 +8,6 @@ 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
@@ -36,18 +35,7 @@ logger = logging.getLogger(__name__)
def include_object(
object: SchemaItem,
name: str | None,
type_: Literal[
"schema",
"table",
"column",
"index",
"unique_constraint",
"foreign_key_constraint",
],
reflected: bool,
compare_to: SchemaItem | None,
object: Any, name: str, type_: str, reflected: bool, compare_to: Any
) -> bool:
"""
Determines whether a database object should be included in migrations.

View File

@@ -1,6 +1,5 @@
import asyncio
from logging.config import fileConfig
from typing import Literal
from sqlalchemy import pool
from sqlalchemy.engine import Connection
@@ -38,15 +37,8 @@ EXCLUDE_TABLES = {"kombu_queue", "kombu_message"}
def include_object(
object: SchemaItem,
name: str | None,
type_: Literal[
"schema",
"table",
"column",
"index",
"unique_constraint",
"foreign_key_constraint",
],
name: str,
type_: str,
reflected: bool,
compare_to: SchemaItem | None,
) -> bool:

View File

@@ -11,7 +11,6 @@ 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
@@ -333,16 +332,16 @@ def on_worker_shutdown(sender: Any, **kwargs: Any) -> None:
return
logger.info("Releasing primary worker lock.")
lock: RedisLock = sender.primary_worker_lock
lock = sender.primary_worker_lock
try:
if lock.owned():
try:
lock.release()
sender.primary_worker_lock = None
except Exception:
logger.exception("Failed to release primary worker lock")
except Exception:
logger.exception("Failed to check if primary worker lock is owned")
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}")
def on_setup_logging(

View File

@@ -11,7 +11,6 @@ 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
@@ -117,7 +116,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: RedisLock = r.lock(
lock = r.lock(
DanswerRedisLocks.PRIMARY_WORKER,
timeout=CELERY_PRIMARY_WORKER_LOCK_TIMEOUT,
)
@@ -228,7 +227,7 @@ class HubPeriodicTask(bootsteps.StartStopStep):
if not hasattr(worker, "primary_worker_lock"):
return
lock: RedisLock = worker.primary_worker_lock
lock = worker.primary_worker_lock
r = get_redis_client(tenant_id=None)

View File

@@ -2,55 +2,54 @@ 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": DanswerCeleryTask.CHECK_FOR_VESPA_SYNC_TASK,
"task": "check_for_vespa_sync_task",
"schedule": timedelta(seconds=20),
"options": {"priority": DanswerCeleryPriority.HIGH},
},
{
"name": "check-for-connector-deletion",
"task": DanswerCeleryTask.CHECK_FOR_CONNECTOR_DELETION,
"task": "check_for_connector_deletion_task",
"schedule": timedelta(seconds=20),
"options": {"priority": DanswerCeleryPriority.HIGH},
},
{
"name": "check-for-indexing",
"task": DanswerCeleryTask.CHECK_FOR_INDEXING,
"task": "check_for_indexing",
"schedule": timedelta(seconds=15),
"options": {"priority": DanswerCeleryPriority.HIGH},
},
{
"name": "check-for-prune",
"task": DanswerCeleryTask.CHECK_FOR_PRUNING,
"task": "check_for_pruning",
"schedule": timedelta(seconds=15),
"options": {"priority": DanswerCeleryPriority.HIGH},
},
{
"name": "kombu-message-cleanup",
"task": DanswerCeleryTask.KOMBU_MESSAGE_CLEANUP_TASK,
"task": "kombu_message_cleanup_task",
"schedule": timedelta(seconds=3600),
"options": {"priority": DanswerCeleryPriority.LOWEST},
},
{
"name": "monitor-vespa-sync",
"task": DanswerCeleryTask.MONITOR_VESPA_SYNC,
"task": "monitor_vespa_sync",
"schedule": timedelta(seconds=5),
"options": {"priority": DanswerCeleryPriority.HIGH},
},
{
"name": "check-for-doc-permissions-sync",
"task": DanswerCeleryTask.CHECK_FOR_DOC_PERMISSIONS_SYNC,
"task": "check_for_doc_permissions_sync",
"schedule": timedelta(seconds=30),
"options": {"priority": DanswerCeleryPriority.HIGH},
},
{
"name": "check-for-external-group-sync",
"task": DanswerCeleryTask.CHECK_FOR_EXTERNAL_GROUP_SYNC,
"task": "check_for_external_group_sync",
"schedule": timedelta(seconds=20),
"options": {"priority": DanswerCeleryPriority.HIGH},
},

View File

@@ -11,7 +11,6 @@ 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
@@ -29,7 +28,7 @@ class TaskDependencyError(RuntimeError):
@shared_task(
name=DanswerCeleryTask.CHECK_FOR_CONNECTOR_DELETION,
name="check_for_connector_deletion_task",
soft_time_limit=JOB_TIMEOUT,
trail=False,
bind=True,

View File

@@ -18,7 +18,6 @@ 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
@@ -83,7 +82,7 @@ def _is_external_doc_permissions_sync_due(cc_pair: ConnectorCredentialPair) -> b
@shared_task(
name=DanswerCeleryTask.CHECK_FOR_DOC_PERMISSIONS_SYNC,
name="check_for_doc_permissions_sync",
soft_time_limit=JOB_TIMEOUT,
bind=True,
)
@@ -165,7 +164,7 @@ def try_creating_permissions_sync_task(
custom_task_id = f"{redis_connector.permissions.generator_task_key}_{uuid4()}"
result = app.send_task(
DanswerCeleryTask.CONNECTOR_PERMISSION_SYNC_GENERATOR_TASK,
"connector_permission_sync_generator_task",
kwargs=dict(
cc_pair_id=cc_pair_id,
tenant_id=tenant_id,
@@ -192,7 +191,7 @@ def try_creating_permissions_sync_task(
@shared_task(
name=DanswerCeleryTask.CONNECTOR_PERMISSION_SYNC_GENERATOR_TASK,
name="connector_permission_sync_generator_task",
acks_late=False,
soft_time_limit=JOB_TIMEOUT,
track_started=True,
@@ -287,7 +286,7 @@ def connector_permission_sync_generator_task(
@shared_task(
name=DanswerCeleryTask.UPDATE_EXTERNAL_DOCUMENT_PERMISSIONS_TASK,
name="update_external_document_permissions_task",
soft_time_limit=LIGHT_SOFT_TIME_LIMIT,
time_limit=LIGHT_TIME_LIMIT,
max_retries=DOCUMENT_PERMISSIONS_UPDATE_MAX_RETRIES,

View File

@@ -17,7 +17,6 @@ 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
@@ -86,7 +85,7 @@ def _is_external_group_sync_due(cc_pair: ConnectorCredentialPair) -> bool:
@shared_task(
name=DanswerCeleryTask.CHECK_FOR_EXTERNAL_GROUP_SYNC,
name="check_for_external_group_sync",
soft_time_limit=JOB_TIMEOUT,
bind=True,
)
@@ -162,7 +161,7 @@ def try_creating_external_group_sync_task(
custom_task_id = f"{redis_connector.external_group_sync.taskset_key}_{uuid4()}"
result = app.send_task(
DanswerCeleryTask.CONNECTOR_EXTERNAL_GROUP_SYNC_GENERATOR_TASK,
"connector_external_group_sync_generator_task",
kwargs=dict(
cc_pair_id=cc_pair_id,
tenant_id=tenant_id,
@@ -192,7 +191,7 @@ def try_creating_external_group_sync_task(
@shared_task(
name=DanswerCeleryTask.CONNECTOR_EXTERNAL_GROUP_SYNC_GENERATOR_TASK,
name="connector_external_group_sync_generator_task",
acks_late=False,
soft_time_limit=JOB_TIMEOUT,
track_started=True,

View File

@@ -23,7 +23,6 @@ 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
@@ -157,7 +156,7 @@ def get_unfenced_index_attempt_ids(db_session: Session, r: redis.Redis) -> list[
@shared_task(
name=DanswerCeleryTask.CHECK_FOR_INDEXING,
name="check_for_indexing",
soft_time_limit=300,
bind=True,
)
@@ -487,7 +486,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(
DanswerCeleryTask.CONNECTOR_INDEXING_PROXY_TASK,
"connector_indexing_proxy_task",
kwargs=dict(
index_attempt_id=index_attempt_id,
cc_pair_id=cc_pair.id,
@@ -525,10 +524,7 @@ def try_creating_indexing_task(
@shared_task(
name=DanswerCeleryTask.CONNECTOR_INDEXING_PROXY_TASK,
bind=True,
acks_late=False,
track_started=True,
name="connector_indexing_proxy_task", bind=True, acks_late=False, track_started=True
)
def connector_indexing_proxy_task(
self: Task,
@@ -584,64 +580,39 @@ def connector_indexing_proxy_task(
if self.request.id and redis_connector_index.terminating(self.request.id):
task_logger.warning(
"Indexing watchdog - termination signal detected: "
"Indexing proxy - termination signal detected: "
f"attempt={index_attempt_id} "
f"tenant={tenant_id} "
f"cc_pair={cc_pair_id} "
f"search_settings={search_settings_id}"
)
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}"
with get_session_with_tenant(tenant_id) as db_session:
mark_attempt_canceled(
index_attempt_id,
db_session,
"Connector termination signal detected",
)
job.cancel()
job.cancel()
break
# do nothing for ongoing jobs that haven't been stopped
if not job.done():
# 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}"
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
)
continue
if not index_attempt:
continue
if not index_attempt.is_finished():
continue
if job.status == "error":
task_logger.error(
"Indexing watchdog - spawned task exceptioned: "
f"Indexing watchdog - spawned task exceptioned: "
f"attempt={index_attempt_id} "
f"tenant={tenant_id} "
f"cc_pair={cc_pair_id} "

View File

@@ -13,13 +13,12 @@ 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=DanswerCeleryTask.KOMBU_MESSAGE_CLEANUP_TASK,
name="kombu_message_cleanup_task",
soft_time_limit=JOB_TIMEOUT,
bind=True,
base=AbortableTask,

View File

@@ -20,7 +20,6 @@ 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
@@ -76,7 +75,7 @@ def _is_pruning_due(cc_pair: ConnectorCredentialPair) -> bool:
@shared_task(
name=DanswerCeleryTask.CHECK_FOR_PRUNING,
name="check_for_pruning",
soft_time_limit=JOB_TIMEOUT,
bind=True,
)
@@ -185,7 +184,7 @@ def try_creating_prune_generator_task(
custom_task_id = f"{redis_connector.prune.generator_task_key}_{uuid4()}"
celery_app.send_task(
DanswerCeleryTask.CONNECTOR_PRUNING_GENERATOR_TASK,
"connector_pruning_generator_task",
kwargs=dict(
cc_pair_id=cc_pair.id,
connector_id=cc_pair.connector_id,
@@ -210,7 +209,7 @@ def try_creating_prune_generator_task(
@shared_task(
name=DanswerCeleryTask.CONNECTOR_PRUNING_GENERATOR_TASK,
name="connector_pruning_generator_task",
acks_late=False,
soft_time_limit=JOB_TIMEOUT,
track_started=True,

View File

@@ -9,7 +9,6 @@ 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
@@ -32,7 +31,7 @@ LIGHT_TIME_LIMIT = LIGHT_SOFT_TIME_LIMIT + 15
@shared_task(
name=DanswerCeleryTask.DOCUMENT_BY_CC_PAIR_CLEANUP_TASK,
name="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,

View File

@@ -25,7 +25,6 @@ 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
@@ -81,7 +80,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=DanswerCeleryTask.CHECK_FOR_VESPA_SYNC_TASK,
name="check_for_vespa_sync_task",
soft_time_limit=JOB_TIMEOUT,
trail=False,
bind=True,
@@ -655,28 +654,24 @@ 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
task_state = result.state
result_state = result.state
if (
task_state in READY_STATES
result_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"result.state={task_state} "
f"result.result={task_result} "
f"result.traceback={task_traceback}"
f"elapsed_submitted={elapsed_submitted.total_seconds():.2f}"
)
task_logger.warning(msg)
@@ -708,7 +703,7 @@ def monitor_ccpair_indexing_taskset(
redis_connector_index.reset()
@shared_task(name=DanswerCeleryTask.MONITOR_VESPA_SYNC, soft_time_limit=300, bind=True)
@shared_task(name="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.
@@ -819,7 +814,7 @@ def monitor_vespa_sync(self: Task, tenant_id: str | None) -> bool:
@shared_task(
name=DanswerCeleryTask.VESPA_METADATA_SYNC_TASK,
name="vespa_metadata_sync_task",
bind=True,
soft_time_limit=LIGHT_SOFT_TIME_LIMIT,
time_limit=LIGHT_TIME_LIMIT,

View File

@@ -31,7 +31,6 @@ 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,
)

View File

@@ -25,7 +25,6 @@ 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

View File

@@ -308,22 +308,6 @@ 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(",")
@@ -509,6 +493,10 @@ CONTROL_PLANE_API_BASE_URL = os.environ.get(
# JWT configuration
JWT_ALGORITHM = "HS256"
# Super Users
SUPER_USERS = json.loads(os.environ.get("SUPER_USERS", '["pablo@danswer.ai"]'))
SUPER_CLOUD_API_KEY = os.environ.get("SUPER_CLOUD_API_KEY", "api_key")
#####
# API Key Configs

View File

@@ -259,32 +259,6 @@ 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

View File

@@ -11,16 +11,11 @@ Connectors come in 3 different flows:
- Load Connector:
- Bulk indexes documents to reflect a point in time. This type of connector generally works by either pulling all
documents via a connector's API or loads the documents from some sort of a dump file.
- Poll Connector:
- Poll connector:
- Incrementally updates documents based on a provided time range. It is used by the background job to pull the latest
changes and additions since the last round of polling. This connector helps keep the document index up to date
without needing to fetch/embed/index every document which would be too slow to do frequently on large sets of
documents.
- Slim Connector:
- This connector should be a lighter weight method of checking all documents in the source to see if they still exist.
- This connector should be identical to the Poll or Load Connector except that it only fetches the IDs of the documents, not the documents themselves.
- This is used by our pruning job which removes old documents from the index.
- The optional start and end datetimes can be ignored.
- Event Based connectors:
- Connectors that listen to events and update documents accordingly.
- Currently not used by the background job, this exists for future design purposes.
@@ -31,14 +26,8 @@ Refer to [interfaces.py](https://github.com/danswer-ai/danswer/blob/main/backend
and this first contributor created Pull Request for a new connector (Shoutout to Dan Brown):
[Reference Pull Request](https://github.com/danswer-ai/danswer/pull/139)
For implementing a Slim Connector, refer to the comments in this PR:
[Slim Connector PR](https://github.com/danswer-ai/danswer/pull/3303/files)
All new connectors should have tests added to the `backend/tests/daily/connectors` directory. Refer to the above PR for an example of adding tests for a new connector.
#### Implementing the new Connector
The connector must subclass one or more of LoadConnector, PollConnector, SlimConnector, or EventConnector.
The connector must subclass one or more of LoadConnector, PollConnector, or EventConnector.
The `__init__` should take arguments for configuring what documents the connector will and where it finds those
documents. For example, if you have a wiki site, it may include the configuration for the team, topic, folder, etc. of

View File

@@ -1,11 +1,9 @@
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
@@ -71,7 +69,6 @@ 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
@@ -107,8 +104,6 @@ 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:
@@ -209,14 +204,12 @@ 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:
@@ -249,10 +242,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=self.timezone).strftime(
formatted_start_time = datetime.fromtimestamp(start, tz=timezone.utc).strftime(
"%Y-%m-%d %H:%M"
)
formatted_end_time = datetime.fromtimestamp(end, tz=self.timezone).strftime(
formatted_end_time = datetime.fromtimestamp(end, tz=timezone.utc).strftime(
"%Y-%m-%d %H:%M"
)
self.cql_time_filter = f" and lastmodified >= '{formatted_start_time}'"

View File

@@ -134,32 +134,6 @@ 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,
@@ -332,13 +306,6 @@ 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}! "

View File

@@ -12,15 +12,12 @@ from dateutil import parser
from danswer.configs.app_configs import INDEX_BATCH_SIZE
from danswer.configs.constants import DocumentSource
from danswer.connectors.interfaces import GenerateDocumentsOutput
from danswer.connectors.interfaces import GenerateSlimDocumentOutput
from danswer.connectors.interfaces import LoadConnector
from danswer.connectors.interfaces import PollConnector
from danswer.connectors.interfaces import SecondsSinceUnixEpoch
from danswer.connectors.interfaces import SlimConnector
from danswer.connectors.models import ConnectorMissingCredentialError
from danswer.connectors.models import Document
from danswer.connectors.models import Section
from danswer.connectors.models import SlimDocument
from danswer.utils.logger import setup_logger
@@ -31,8 +28,6 @@ logger = setup_logger()
SLAB_GRAPHQL_MAX_TRIES = 10
SLAB_API_URL = "https://api.slab.com/v1/graphql"
_SLIM_BATCH_SIZE = 1000
def run_graphql_request(
graphql_query: dict, bot_token: str, max_tries: int = SLAB_GRAPHQL_MAX_TRIES
@@ -163,26 +158,21 @@ def get_slab_url_from_title_id(base_url: str, title: str, page_id: str) -> str:
return urljoin(urljoin(base_url, "posts/"), url_id)
class SlabConnector(LoadConnector, PollConnector, SlimConnector):
class SlabConnector(LoadConnector, PollConnector):
def __init__(
self,
base_url: str,
batch_size: int = INDEX_BATCH_SIZE,
slab_bot_token: str | None = None,
) -> None:
self.base_url = base_url
self.batch_size = batch_size
self._slab_bot_token: str | None = None
self.slab_bot_token = slab_bot_token
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
self._slab_bot_token = credentials["slab_bot_token"]
self.slab_bot_token = credentials["slab_bot_token"]
return None
@property
def slab_bot_token(self) -> str:
if self._slab_bot_token is None:
raise ConnectorMissingCredentialError("Slab")
return self._slab_bot_token
def _iterate_posts(
self, time_filter: Callable[[datetime], bool] | None = None
) -> GenerateDocumentsOutput:
@@ -237,21 +227,3 @@ class SlabConnector(LoadConnector, PollConnector, SlimConnector):
yield from self._iterate_posts(
time_filter=lambda t: start_time <= t <= end_time
)
def retrieve_all_slim_documents(
self,
start: SecondsSinceUnixEpoch | None = None,
end: SecondsSinceUnixEpoch | None = None,
) -> GenerateSlimDocumentOutput:
slim_doc_batch: list[SlimDocument] = []
for post_id in get_all_post_ids(self.slab_bot_token):
slim_doc_batch.append(
SlimDocument(
id=post_id,
)
)
if len(slim_doc_batch) >= _SLIM_BATCH_SIZE:
yield slim_doc_batch
slim_doc_batch = []
if slim_doc_batch:
yield slim_doc_batch

View File

@@ -415,6 +415,9 @@ 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,
@@ -446,12 +449,6 @@ 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:
@@ -489,8 +486,6 @@ 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.")
@@ -499,9 +494,6 @@ 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

View File

@@ -59,12 +59,6 @@ class FileStore(ABC):
Contents of the file and metadata dict
"""
@abstractmethod
def read_file_record(self, file_name: str) -> PGFileStore:
"""
Read the file record by the name
"""
@abstractmethod
def delete_file(self, file_name: str) -> None:
"""

View File

@@ -67,9 +67,9 @@ class CitationProcessor:
if piece_that_comes_after == "\n" and in_code_block(self.llm_out):
self.curr_segment = self.curr_segment.replace("```", "```plaintext")
citation_pattern = r"\[(\d+)\]|\[\[(\d+)\]\]" # [1], [[1]], etc.
citation_pattern = r"\[(\d+)\]"
citations_found = list(re.finditer(citation_pattern, self.curr_segment))
possible_citation_pattern = r"(\[+\d*$)" # [1, [, [[, [[2, etc.
possible_citation_pattern = r"(\[\d*$)" # [1, [, etc
possible_citation_found = re.search(
possible_citation_pattern, self.curr_segment
)
@@ -77,15 +77,13 @@ class CitationProcessor:
if len(citations_found) == 0 and len(self.llm_out) - self.past_cite_count > 5:
self.current_citations = []
result = ""
result = "" # Initialize result here
if citations_found and not in_code_block(self.llm_out):
last_citation_end = 0
length_to_add = 0
while len(citations_found) > 0:
citation = citations_found.pop(0)
numerical_value = int(
next(group for group in citation.groups() if group is not None)
)
numerical_value = int(citation.group(1))
if 1 <= numerical_value <= self.max_citation_num:
context_llm_doc = self.context_docs[numerical_value - 1]
@@ -133,6 +131,14 @@ class CitationProcessor:
link = context_llm_doc.link
# Replace the citation in the current segment
start, end = citation.span()
self.curr_segment = (
self.curr_segment[: start + length_to_add]
+ f"[{target_citation_num}]"
+ self.curr_segment[end + length_to_add :]
)
self.past_cite_count = len(self.llm_out)
self.current_citations.append(target_citation_num)
@@ -143,7 +149,6 @@ class CitationProcessor:
document_id=context_llm_doc.document_id,
)
start, end = citation.span()
if link:
prev_length = len(self.curr_segment)
self.curr_segment = (

View File

@@ -71,7 +71,6 @@ 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,
)
@@ -129,13 +128,11 @@ def get_llm(
api_base: str | None = None,
api_version: str | None = None,
custom_config: dict[str, str] | None = None,
temperature: float | None = None,
temperature: float = GEN_AI_TEMPERATURE,
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,

View File

@@ -10,7 +10,6 @@ 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,
@@ -106,7 +105,7 @@ class RedisConnectorCredentialPair(RedisObjectHelper):
# Priority on sync's triggered by new indexing should be medium
result = celery_app.send_task(
DanswerCeleryTask.VESPA_METADATA_SYNC_TASK,
"vespa_metadata_sync_task",
kwargs=dict(document_id=doc.id, tenant_id=tenant_id),
queue=DanswerCeleryQueues.VESPA_METADATA_SYNC,
task_id=custom_task_id,

View File

@@ -12,7 +12,6 @@ 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
@@ -115,7 +114,7 @@ class RedisConnectorDelete:
# Priority on sync's triggered by new indexing should be medium
result = celery_app.send_task(
DanswerCeleryTask.DOCUMENT_BY_CC_PAIR_CLEANUP_TASK,
"document_by_cc_pair_cleanup_task",
kwargs=dict(
document_id=doc.id,
connector_id=cc_pair.connector_id,

View File

@@ -12,7 +12,6 @@ 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):
@@ -150,7 +149,7 @@ class RedisConnectorPermissionSync:
self.redis.sadd(self.taskset_key, custom_task_id)
result = celery_app.send_task(
DanswerCeleryTask.UPDATE_EXTERNAL_DOCUMENT_PERMISSIONS_TASK,
"update_external_document_permissions_task",
kwargs=dict(
tenant_id=self.tenant_id,
serialized_doc_external_access=doc_perm.to_dict(),

View File

@@ -10,7 +10,6 @@ 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
@@ -135,7 +134,7 @@ class RedisConnectorPrune:
# Priority on sync's triggered by new indexing should be medium
result = celery_app.send_task(
DanswerCeleryTask.DOCUMENT_BY_CC_PAIR_CLEANUP_TASK,
"document_by_cc_pair_cleanup_task",
kwargs=dict(
document_id=doc_id,
connector_id=cc_pair.connector_id,

View File

@@ -11,7 +11,6 @@ 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
@@ -77,7 +76,7 @@ class RedisDocumentSet(RedisObjectHelper):
redis_client.sadd(self.taskset_key, custom_task_id)
result = celery_app.send_task(
DanswerCeleryTask.VESPA_METADATA_SYNC_TASK,
"vespa_metadata_sync_task",
kwargs=dict(document_id=doc.id, tenant_id=tenant_id),
queue=DanswerCeleryQueues.VESPA_METADATA_SYNC,
task_id=custom_task_id,

View File

@@ -11,7 +11,6 @@ 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
@@ -90,7 +89,7 @@ class RedisUserGroup(RedisObjectHelper):
redis_client.sadd(self.taskset_key, custom_task_id)
result = celery_app.send_task(
DanswerCeleryTask.VESPA_METADATA_SYNC_TASK,
"vespa_metadata_sync_task",
kwargs=dict(document_id=doc.id, tenant_id=tenant_id),
queue=DanswerCeleryQueues.VESPA_METADATA_SYNC,
task_id=custom_task_id,

View File

@@ -20,7 +20,6 @@ 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 (
@@ -868,7 +867,7 @@ def connector_run_once(
# run the beat task to pick up the triggers immediately
primary_app.send_task(
DanswerCeleryTask.CHECK_FOR_INDEXING,
"check_for_indexing",
priority=DanswerCeleryPriority.HIGH,
kwargs={"tenant_id": tenant_id},
)

View File

@@ -13,7 +13,6 @@ 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
@@ -200,7 +199,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(
DanswerCeleryTask.CHECK_FOR_CONNECTOR_DELETION,
"check_for_connector_deletion_task",
priority=DanswerCeleryPriority.HIGH,
kwargs={"tenant_id": tenant_id},
)

View File

@@ -34,6 +34,7 @@ from danswer.auth.users import optional_user
from danswer.configs.app_configs import AUTH_TYPE
from danswer.configs.app_configs import ENABLE_EMAIL_INVITES
from danswer.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS
from danswer.configs.app_configs import SUPER_USERS
from danswer.configs.app_configs import VALID_EMAIL_DOMAINS
from danswer.configs.constants import AuthType
from danswer.db.api_key import is_api_key_email_address
@@ -63,7 +64,6 @@ from danswer.server.models import MinimalUserSnapshot
from danswer.server.utils import send_user_email_invite
from danswer.utils.logger import setup_logger
from danswer.utils.variable_functionality import fetch_ee_implementation_or_noop
from ee.danswer.configs.app_configs import SUPER_USERS
from shared_configs.configs import MULTI_TENANT
logger = setup_logger()

View File

@@ -707,18 +707,14 @@ def upload_files_for_chat(
}
@router.get("/file/{file_id:path}")
@router.get("/file/{file_id}")
def fetch_chat_file(
file_id: str,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_user),
) -> Response:
file_store = get_default_file_store(db_session)
file_record = file_store.read_file_record(file_id)
if not file_record:
raise HTTPException(status_code=404, detail="File not found")
media_type = file_record.file_type
file_io = file_store.read_file(file_id, mode="b")
return StreamingResponse(file_io, media_type=media_type)
# NOTE: specifying "image/jpeg" here, but it still works for pngs
# TODO: do this properly
return Response(content=file_io.read(), media_type="image/jpeg")

View File

@@ -77,7 +77,6 @@ def llm_doc_from_internet_search_result(result: InternetSearchResult) -> LlmDoc:
updated_at=datetime.now(),
link=result.link,
source_links={0: result.link},
match_highlights=[],
)

View File

@@ -1,72 +1,23 @@
from functools import lru_cache
import requests
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Request
from fastapi import status
from jwt import decode as jwt_decode
from jwt import InvalidTokenError
from jwt import PyJWTError
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from danswer.auth.users import current_admin_user
from danswer.configs.app_configs import AUTH_TYPE
from danswer.configs.app_configs import SUPER_CLOUD_API_KEY
from danswer.configs.app_configs import SUPER_USERS
from danswer.configs.constants import AuthType
from danswer.db.models import User
from danswer.utils.logger import setup_logger
from ee.danswer.configs.app_configs import JWT_PUBLIC_KEY_URL
from ee.danswer.configs.app_configs import SUPER_CLOUD_API_KEY
from ee.danswer.configs.app_configs import SUPER_USERS
from ee.danswer.db.saml import get_saml_account
from ee.danswer.server.seeding import get_seed_config
from ee.danswer.utils.secrets import extract_hashed_cookie
logger = setup_logger()
@lru_cache()
def get_public_key() -> str | None:
if JWT_PUBLIC_KEY_URL is None:
logger.error("JWT_PUBLIC_KEY_URL is not set")
return None
response = requests.get(JWT_PUBLIC_KEY_URL)
response.raise_for_status()
return response.text
async def verify_jwt_token(token: str, async_db_session: AsyncSession) -> User | None:
try:
public_key_pem = get_public_key()
if public_key_pem is None:
logger.error("Failed to retrieve public key")
return None
payload = jwt_decode(
token,
public_key_pem,
algorithms=["RS256"],
audience=None,
)
email = payload.get("email")
if email:
result = await async_db_session.execute(
select(User).where(func.lower(User.email) == func.lower(email))
)
return result.scalars().first()
except InvalidTokenError:
logger.error("Invalid JWT token")
get_public_key.cache_clear()
except PyJWTError as e:
logger.error(f"JWT decoding error: {str(e)}")
get_public_key.cache_clear()
return None
def verify_auth_setting() -> None:
# All the Auth flows are valid for EE version
logger.notice(f"Using Auth Type: {AUTH_TYPE.value}")
@@ -87,13 +38,6 @@ async def optional_user_(
)
user = saml_account.user if saml_account else None
# If user is still None, check for JWT in Authorization header
if user is None and JWT_PUBLIC_KEY_URL is not None:
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
token = auth_header[len("Bearer ") :].strip()
user = await verify_jwt_token(token, async_db_session)
return user

View File

@@ -4,17 +4,16 @@ 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": DanswerCeleryTask.AUTOGENERATE_USAGE_REPORT_TASK,
"task": "autogenerate_usage_report_task",
"schedule": timedelta(days=30), # TODO: change this to config flag
},
{
"name": "check-ttl-management",
"task": DanswerCeleryTask.CHECK_TTL_MANAGEMENT_TASK,
"task": "check_ttl_management_task",
"schedule": timedelta(hours=1),
},
]

View File

@@ -1,4 +1,3 @@
import json
import os
# Applicable for OIDC Auth
@@ -20,11 +19,3 @@ STRIPE_PRICE_ID = os.environ.get("STRIPE_PRICE")
OPENAI_DEFAULT_API_KEY = os.environ.get("OPENAI_DEFAULT_API_KEY")
ANTHROPIC_DEFAULT_API_KEY = os.environ.get("ANTHROPIC_DEFAULT_API_KEY")
COHERE_DEFAULT_API_KEY = os.environ.get("COHERE_DEFAULT_API_KEY")
# JWT Public Key URL
JWT_PUBLIC_KEY_URL: str | None = os.getenv("JWT_PUBLIC_KEY_URL", None)
# Super Users
SUPER_USERS = json.loads(os.environ.get("SUPER_USERS", '["pablo@danswer.ai"]'))
SUPER_CLOUD_API_KEY = os.environ.get("SUPER_CLOUD_API_KEY", "api_key")

View File

@@ -3,7 +3,7 @@ cohere==5.6.1
fastapi==0.109.2
google-cloud-aiplatform==1.58.0
numpy==1.26.4
openai==1.55.3
openai==1.52.2
pydantic==2.8.2
retry==0.9.2
safetensors==0.4.2

View File

@@ -163,92 +163,47 @@ SUPPORTED_EMBEDDING_MODELS = [
dim=1024,
index_name="danswer_chunk_cohere_embed_english_v3_0",
),
SupportedEmbeddingModel(
name="cohere/embed-english-v3.0",
dim=1024,
index_name="danswer_chunk_embed_english_v3_0",
),
SupportedEmbeddingModel(
name="cohere/embed-english-light-v3.0",
dim=384,
index_name="danswer_chunk_cohere_embed_english_light_v3_0",
),
SupportedEmbeddingModel(
name="cohere/embed-english-light-v3.0",
dim=384,
index_name="danswer_chunk_embed_english_light_v3_0",
),
SupportedEmbeddingModel(
name="openai/text-embedding-3-large",
dim=3072,
index_name="danswer_chunk_openai_text_embedding_3_large",
),
SupportedEmbeddingModel(
name="openai/text-embedding-3-large",
dim=3072,
index_name="danswer_chunk_text_embedding_3_large",
),
SupportedEmbeddingModel(
name="openai/text-embedding-3-small",
dim=1536,
index_name="danswer_chunk_openai_text_embedding_3_small",
),
SupportedEmbeddingModel(
name="openai/text-embedding-3-small",
dim=1536,
index_name="danswer_chunk_text_embedding_3_small",
),
SupportedEmbeddingModel(
name="google/text-embedding-004",
dim=768,
index_name="danswer_chunk_google_text_embedding_004",
),
SupportedEmbeddingModel(
name="google/text-embedding-004",
dim=768,
index_name="danswer_chunk_text_embedding_004",
),
SupportedEmbeddingModel(
name="google/textembedding-gecko@003",
dim=768,
index_name="danswer_chunk_google_textembedding_gecko_003",
),
SupportedEmbeddingModel(
name="google/textembedding-gecko@003",
dim=768,
index_name="danswer_chunk_textembedding_gecko_003",
),
SupportedEmbeddingModel(
name="voyage/voyage-large-2-instruct",
dim=1024,
index_name="danswer_chunk_voyage_large_2_instruct",
),
SupportedEmbeddingModel(
name="voyage/voyage-large-2-instruct",
dim=1024,
index_name="danswer_chunk_large_2_instruct",
),
SupportedEmbeddingModel(
name="voyage/voyage-light-2-instruct",
dim=384,
index_name="danswer_chunk_voyage_light_2_instruct",
),
SupportedEmbeddingModel(
name="voyage/voyage-light-2-instruct",
dim=384,
index_name="danswer_chunk_light_2_instruct",
),
# Self-hosted models
SupportedEmbeddingModel(
name="nomic-ai/nomic-embed-text-v1",
dim=768,
index_name="danswer_chunk_nomic_ai_nomic_embed_text_v1",
),
SupportedEmbeddingModel(
name="nomic-ai/nomic-embed-text-v1",
dim=768,
index_name="danswer_chunk_nomic_embed_text_v1",
),
SupportedEmbeddingModel(
name="intfloat/e5-base-v2",
dim=768,

View File

@@ -1,88 +0,0 @@
import json
import os
import time
from pathlib import Path
import pytest
from danswer.configs.constants import DocumentSource
from danswer.connectors.models import Document
from danswer.connectors.slab.connector import SlabConnector
def load_test_data(file_name: str = "test_slab_data.json") -> dict[str, str]:
current_dir = Path(__file__).parent
with open(current_dir / file_name, "r") as f:
return json.load(f)
@pytest.fixture
def slab_connector() -> SlabConnector:
connector = SlabConnector(
base_url="https://onyx-test.slab.com/",
)
connector.load_credentials(
{
"slab_bot_token": os.environ["SLAB_BOT_TOKEN"],
}
)
return connector
@pytest.mark.xfail(
reason=(
"Need a test account with a slab subscription to run this test."
"Trial only lasts 14 days."
)
)
def test_slab_connector_basic(slab_connector: SlabConnector) -> None:
all_docs: list[Document] = []
target_test_doc_id = "jcp6cohu"
target_test_doc: Document | None = None
for doc_batch in slab_connector.poll_source(0, time.time()):
for doc in doc_batch:
all_docs.append(doc)
if doc.id == target_test_doc_id:
target_test_doc = doc
assert len(all_docs) == 6
assert target_test_doc is not None
desired_test_data = load_test_data()
assert (
target_test_doc.semantic_identifier == desired_test_data["semantic_identifier"]
)
assert target_test_doc.source == DocumentSource.SLAB
assert target_test_doc.metadata == {}
assert target_test_doc.primary_owners is None
assert target_test_doc.secondary_owners is None
assert target_test_doc.title is None
assert target_test_doc.from_ingestion_api is False
assert target_test_doc.additional_info is None
assert len(target_test_doc.sections) == 1
section = target_test_doc.sections[0]
# Need to replace the weird apostrophe with a normal one
assert section.text.replace("\u2019", "'") == desired_test_data["section_text"]
assert section.link == desired_test_data["link"]
@pytest.mark.xfail(
reason=(
"Need a test account with a slab subscription to run this test."
"Trial only lasts 14 days."
)
)
def test_slab_connector_slim(slab_connector: SlabConnector) -> None:
# Get all doc IDs from the full connector
all_full_doc_ids = set()
for doc_batch in slab_connector.load_from_state():
all_full_doc_ids.update([doc.id for doc in doc_batch])
# Get all doc IDs from the slim connector
all_slim_doc_ids = set()
for slim_doc_batch in slab_connector.retrieve_all_slim_documents():
all_slim_doc_ids.update([doc.id for doc in slim_doc_batch])
# The set of full doc IDs should be always be a subset of the slim doc IDs
assert all_full_doc_ids.issubset(all_slim_doc_ids)

View File

@@ -1,5 +0,0 @@
{
"section_text": "Learn about Posts\nWelcome\nThis is a post, where you can edit, share, and collaborate in real time with your team. We'd love to show you how it works!\nReading and editing\nClick the mode button to toggle between read and edit modes. You can only make changes to a post when editing.\nOrganize your posts\nWhen in edit mode, you can add topics to a post, which will keep it organized for the right 👀 to see.\nSmart mentions\nMentions are references to users, posts, topics and third party tools that show details on hover. Paste in a link for automatic conversion.\nLook back in time\nYou are ready to begin writing. You can always bring back this tour in the help menu.\nGreat job!\nYou are ready to begin writing. You can always bring back this tour in the help menu.\n\n",
"link": "https://onyx-test.slab.com/posts/learn-about-posts-jcp6cohu",
"semantic_identifier": "Learn about Posts"
}

View File

@@ -1,62 +0,0 @@
import mimetypes
from typing import cast
from typing import IO
from typing import List
from typing import Tuple
import requests
from danswer.file_store.models import FileDescriptor
from tests.integration.common_utils.constants import API_SERVER_URL
from tests.integration.common_utils.constants import GENERAL_HEADERS
from tests.integration.common_utils.test_models import DATestUser
class FileManager:
@staticmethod
def upload_files(
files: List[Tuple[str, IO]],
user_performing_action: DATestUser | None = None,
) -> Tuple[List[FileDescriptor], str]:
headers = (
user_performing_action.headers
if user_performing_action
else GENERAL_HEADERS
)
headers.pop("Content-Type", None)
files_param = []
for filename, file_obj in files:
mime_type, _ = mimetypes.guess_type(filename)
if mime_type is None:
mime_type = "application/octet-stream"
files_param.append(("files", (filename, file_obj, mime_type)))
response = requests.post(
f"{API_SERVER_URL}/chat/file",
files=files_param,
headers=headers,
)
if not response.ok:
return (
cast(List[FileDescriptor], []),
f"Failed to upload files - {response.json().get('detail', 'Unknown error')}",
)
response_json = response.json()
return response_json.get("files", cast(List[FileDescriptor], [])), ""
@staticmethod
def fetch_uploaded_file(
file_id: str,
user_performing_action: DATestUser | None = None,
) -> bytes:
response = requests.get(
f"{API_SERVER_URL}/chat/file/{file_id}",
headers=user_performing_action.headers
if user_performing_action
else GENERAL_HEADERS,
)
response.raise_for_status()
return response.content

View File

@@ -44,7 +44,6 @@ 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(

View File

@@ -64,7 +64,6 @@ 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",
@@ -76,7 +75,6 @@ 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=[],
),
]

View File

@@ -46,7 +46,6 @@ 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)
]
@@ -386,16 +385,6 @@ def process_text(
"Here is some text[[1]](https://0.com). Some other text",
["doc_0"],
),
# ['To', ' set', ' up', ' D', 'answer', ',', ' if', ' you', ' are', ' running', ' it', ' yourself', ' and',
# ' need', ' access', ' to', ' certain', ' features', ' like', ' auto', '-sync', 'ing', ' document',
# '-level', ' access', ' permissions', ',', ' you', ' should', ' reach', ' out', ' to', ' the', ' D',
# 'answer', ' team', ' to', ' receive', ' access', ' [[', '4', ']].', '']
(
"Unique tokens with double brackets and a single token that ends the citation and has characters after it.",
["... to receive access", " [[", "1", "]].", ""],
"... to receive access [[1]](https://0.com).",
["doc_0"],
),
],
)
def test_citation_extraction(

View File

@@ -20,7 +20,6 @@ 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)
]

View File

@@ -130,7 +130,6 @@ services:
restart: always
environment:
- ENCRYPTION_KEY_SECRET=${ENCRYPTION_KEY_SECRET:-}
- JWT_PUBLIC_KEY_URL=${JWT_PUBLIC_KEY_URL:-} # used for JWT authentication of users via API
# Gen AI Settings (Needed by DanswerBot)
- GEN_AI_MAX_TOKENS=${GEN_AI_MAX_TOKENS:-}
- QA_TIMEOUT=${QA_TIMEOUT:-}

6
node_modules/.package-lock.json generated vendored
View File

@@ -1,6 +0,0 @@
{
"name": "danswer",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -24,6 +24,13 @@ import {
TextFormField,
} from "@/components/admin/connectors/Field";
import {
Card,
CardHeader,
CardTitle,
CardContent,
CardFooter,
} from "@/components/ui/card";
import { usePopup } from "@/components/admin/connectors/Popup";
import { getDisplayNameForModel, useCategories } from "@/lib/hooks";
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";

View File

@@ -89,6 +89,3 @@ export const getProviderIcon = (providerName: string, modelName?: string) => {
return CPUIcon;
}
};
export const isAnthropic = (provider: string, modelName: string) =>
provider === "anthropic" || modelName.toLowerCase().includes("claude");

View File

@@ -28,8 +28,7 @@ import { Modal } from "@/components/Modal";
import { useRouter } from "next/navigation";
import CardSection from "@/components/admin/CardSection";
import { combineSearchSettings } from "./utils";
import { CardDescription } from "@/components/ui/card";
export default function EmbeddingForm() {
const { formStep, nextFormStep, prevFormStep } = useEmbeddingFormContext();
const { popup, setPopup } = usePopup();
@@ -223,14 +222,15 @@ export default function EmbeddingForm() {
};
const updateSearch = async () => {
const searchSettings = combineSearchSettings(
selectedProvider,
advancedEmbeddingDetails,
rerankingDetails,
selectedProvider.provider_type?.toLowerCase() as EmbeddingProvider | null
);
const values: SavedSearchSettings = {
...rerankingDetails,
...advancedEmbeddingDetails,
...selectedProvider,
provider_type:
selectedProvider.provider_type?.toLowerCase() as EmbeddingProvider | null,
};
const response = await updateSearchSettings(searchSettings);
const response = await updateSearchSettings(values);
if (response.ok) {
return true;
} else {
@@ -247,35 +247,39 @@ export default function EmbeddingForm() {
if (!selectedProvider) {
return;
}
let searchSettings: SavedSearchSettings;
let newModel: SavedSearchSettings;
// We use a spread operation to merge properties from multiple objects into a single object.
// Advanced embedding details may update default values.
// Do NOT modify the order unless you are positive the new hierarchy is correct.
if (selectedProvider.provider_type != null) {
// This is a cloud model
searchSettings = combineSearchSettings(
selectedProvider,
advancedEmbeddingDetails,
rerankingDetails,
selectedProvider.provider_type
?.toLowerCase()
.split(" ")[0] as EmbeddingProvider | null
);
newModel = {
...selectedProvider,
...advancedEmbeddingDetails,
...rerankingDetails,
provider_type:
(selectedProvider.provider_type
?.toLowerCase()
.split(" ")[0] as EmbeddingProvider) || null,
};
} else {
// This is a locally hosted model
searchSettings = combineSearchSettings(
selectedProvider,
advancedEmbeddingDetails,
rerankingDetails,
null
);
newModel = {
...selectedProvider,
...advancedEmbeddingDetails,
...rerankingDetails,
provider_type: null,
};
}
searchSettings.index_name = null;
newModel.index_name = null;
const response = await fetch(
"/api/search-settings/set-new-search-settings",
{
method: "POST",
body: JSON.stringify(searchSettings),
body: JSON.stringify(newModel),
headers: {
"Content-Type": "application/json",
},

View File

@@ -1,16 +1,3 @@
import {
CloudEmbeddingProvider,
HostedEmbeddingModel,
} from "@/components/embedding/interfaces";
import {
AdvancedSearchConfiguration,
SavedSearchSettings,
} from "../interfaces";
import { EmbeddingProvider } from "@/components/embedding/interfaces";
import { RerankingDetails } from "../interfaces";
export const deleteSearchSettings = async (search_settings_id: number) => {
const response = await fetch(`/api/search-settings/delete-search-settings`, {
method: "DELETE",
@@ -55,20 +42,3 @@ export const testEmbedding = async ({
return testResponse;
};
// We use a spread operation to merge properties from multiple objects into a single object.
// Advanced embedding details may update default values.
// Do NOT modify the order unless you are positive the new hierarchy is correct.
export const combineSearchSettings = (
selectedProvider: CloudEmbeddingProvider | HostedEmbeddingModel,
advancedEmbeddingDetails: AdvancedSearchConfiguration,
rerankingDetails: RerankingDetails,
provider_type: EmbeddingProvider | null
): SavedSearchSettings => {
return {
...selectedProvider,
...advancedEmbeddingDetails,
...rerankingDetails,
provider_type: provider_type,
};
};

View File

@@ -5,6 +5,7 @@ import { usePopup } from "@/components/admin/connectors/Popup";
import { basicLogin, basicSignup } from "@/lib/user";
import { Button } from "@/components/ui/button";
import { Form, Formik } from "formik";
import { useRouter } from "next/navigation";
import * as Yup from "yup";
import { requestEmailVerification } from "../lib";
import { useState } from "react";
@@ -21,8 +22,10 @@ export function EmailPasswordForm({
referralSource?: string;
nextUrl?: string | null;
}) {
const router = useRouter();
const { popup, setPopup } = usePopup();
const [isWorking, setIsWorking] = useState(false);
return (
<>
{isWorking && <Spinner />}
@@ -66,13 +69,9 @@ export function EmailPasswordForm({
if (loginResponse.ok) {
if (isSignup && shouldVerify) {
await requestEmailVerification(values.email);
// Use window.location.href to force a full page reload,
// ensuring app re-initializes with the new state (including
// server-side provider values)
window.location.href = "/auth/waiting-on-verification";
router.push("/auth/waiting-on-verification");
} else {
// See above comment
window.location.href = nextUrl ? encodeURI(nextUrl) : "/";
router.push(nextUrl ? encodeURI(nextUrl) : "/");
}
} else {
setIsWorking(false);

View File

@@ -70,7 +70,7 @@ import { StarterMessages } from "../../components/assistants/StarterMessage";
import {
AnswerPiecePacket,
DanswerDocument,
DocumentInfoPacket,
FinalContextDocs,
StreamStopInfo,
StreamStopReason,
} from "@/lib/search/interfaces";
@@ -106,10 +106,8 @@ import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
import { useAssistants } from "@/components/context/AssistantsContext";
import { Separator } from "@/components/ui/separator";
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;
@@ -281,9 +279,6 @@ export function ChatPage({
const [alternativeAssistant, setAlternativeAssistant] =
useState<Persona | null>(null);
const [presentingDocument, setPresentingDocument] =
useState<DanswerDocument | null>(null);
const {
visibleAssistants: assistants,
recentAssistants,
@@ -412,7 +407,7 @@ export function ChatPage({
// reset LLM overrides (based on chat session!)
llmOverrideManager.updateModelOverrideForChatSession(selectedChatSession);
llmOverrideManager.updateTemperature(null);
llmOverrideManager.setTemperature(null);
// remove uploaded files
setCurrentMessageFiles([]);
@@ -495,7 +490,6 @@ export function ChatPage({
clientScrollToBottom(true);
}
}
setIsFetchingChatMessages(false);
// if this is a seeded chat, then kick off the AI message generation
@@ -922,6 +916,7 @@ 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,16 +969,6 @@ 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
@@ -1262,6 +1247,7 @@ export function ChatPage({
if (!packet) {
continue;
}
if (!initialFetchDetails) {
if (!Object.hasOwn(packet, "user_message_id")) {
console.error(
@@ -1335,8 +1321,8 @@ export function ChatPage({
if (Object.hasOwn(packet, "answer_piece")) {
answer += (packet as AnswerPiecePacket).answer_piece;
} else if (Object.hasOwn(packet, "top_documents")) {
documents = (packet as DocumentInfoPacket).top_documents;
} else if (Object.hasOwn(packet, "final_context_docs")) {
documents = (packet as FinalContextDocs).final_context_docs;
retrievalType = RetrievalType.Search;
if (documents && documents.length > 0) {
// point to the latest message (we don't know the messageId yet, which is why
@@ -1663,6 +1649,7 @@ export function ChatPage({
scrollDist,
endDivRef,
debounceNumber,
waitForScrollRef,
mobile: settings?.isMobile,
enableAutoScroll: autoScrollEnabled,
});
@@ -1959,7 +1946,6 @@ export function ChatPage({
{popup}
<ChatPopup />
{currentFeedback && (
<FeedbackModal
feedbackType={currentFeedback[0]}
@@ -1993,7 +1979,6 @@ export function ChatPage({
<div className="md:hidden">
<Modal noPadding noScroll>
<ChatFilters
setPresentingDocument={setPresentingDocument}
modal={true}
filterManager={filterManager}
ccPairs={ccPairs}
@@ -2039,13 +2024,6 @@ export function ChatPage({
/>
)}
{presentingDocument && (
<TextView
presentingDocument={presentingDocument}
onClose={() => setPresentingDocument(null)}
/>
)}
{stackTraceModalContent && (
<ExceptionTraceModal
onOutsideClick={() => setStackTraceModalContent(null)}
@@ -2149,7 +2127,6 @@ export function ChatPage({
`}
>
<ChatFilters
setPresentingDocument={setPresentingDocument}
modal={false}
filterManager={filterManager}
ccPairs={ccPairs}
@@ -2447,9 +2424,6 @@ export function ChatPage({
}
>
<AIMessage
setPresentingDocument={
setPresentingDocument
}
index={i}
selectedMessageForDocDisplay={
selectedMessageForDocDisplay

View File

@@ -14,6 +14,7 @@ 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";

View File

@@ -6,16 +6,13 @@ import { buildDocumentSummaryDisplay } from "@/components/search/DocumentDisplay
import { DocumentUpdatedAtBadge } from "@/components/search/DocumentUpdatedAtBadge";
import { MetadataBadge } from "@/components/MetadataBadge";
import { WebResultIcon } from "@/components/WebResultIcon";
import { Dispatch, SetStateAction } from "react";
interface DocumentDisplayProps {
closeSidebar: () => void;
document: DanswerDocument;
modal?: boolean;
isSelected: boolean;
handleSelect: (documentId: string) => void;
tokenLimitReached: boolean;
setPresentingDocument: Dispatch<SetStateAction<DanswerDocument | null>>;
}
export function DocumentMetadataBlock({
@@ -58,13 +55,11 @@ export function DocumentMetadataBlock({
}
export function ChatDocumentDisplay({
closeSidebar,
document,
modal,
isSelected,
handleSelect,
tokenLimitReached,
setPresentingDocument,
}: DocumentDisplayProps) {
const isInternet = document.is_internet;
@@ -72,18 +67,6 @@ export function ChatDocumentDisplay({
return null;
}
const handleViewFile = async () => {
if (document.link) {
window.open(document.link, "_blank");
} else {
closeSidebar();
setTimeout(async () => {
setPresentingDocument(document);
}, 100);
}
};
return (
<div className={`opacity-100 ${modal ? "w-[90vw]" : "w-full"}`}>
<div
@@ -91,9 +74,11 @@ export function ChatDocumentDisplay({
isSelected ? "bg-gray-200" : "hover:bg-background-125"
}`}
>
<button
onClick={handleViewFile}
className="cursor-pointer text-left flex flex-col px-2 py-1.5"
<a
href={document.link}
target="_blank"
rel="noopener noreferrer"
className="cursor-pointer flex flex-col px-2 py-1.5"
>
<div className="line-clamp-1 mb-1 flex h-6 items-center gap-2 text-xs">
{document.is_internet || document.source_type === "web" ? (
@@ -126,7 +111,7 @@ export function ChatDocumentDisplay({
/>
)}
</div>
</button>
</a>
</div>
</div>
);

View File

@@ -3,14 +3,7 @@ import { ChatDocumentDisplay } from "./ChatDocumentDisplay";
import { usePopup } from "@/components/admin/connectors/Popup";
import { removeDuplicateDocs } from "@/lib/documentUtils";
import { Message } from "../interfaces";
import {
Dispatch,
ForwardedRef,
forwardRef,
SetStateAction,
useEffect,
useState,
} from "react";
import { ForwardedRef, forwardRef, useEffect, useState } from "react";
import { FilterManager } from "@/lib/hooks";
import { CCPairBasicInfo, DocumentSet, Tag } from "@/lib/types";
import { SourceSelector } from "../shared_chat_search/SearchFilters";
@@ -32,7 +25,6 @@ interface ChatFiltersProps {
tags: Tag[];
documentSets: DocumentSet[];
showFilters: boolean;
setPresentingDocument: Dispatch<SetStateAction<DanswerDocument | null>>;
}
export const ChatFilters = forwardRef<HTMLDivElement, ChatFiltersProps>(
@@ -51,7 +43,6 @@ export const ChatFilters = forwardRef<HTMLDivElement, ChatFiltersProps>(
isOpen,
ccPairs,
tags,
setPresentingDocument,
documentSets,
showFilters,
},
@@ -143,8 +134,6 @@ export const ChatFilters = forwardRef<HTMLDivElement, ChatFiltersProps>(
}`}
>
<ChatDocumentDisplay
setPresentingDocument={setPresentingDocument}
closeSidebar={closeSidebar}
modal={modal}
document={document}
isSelected={selectedDocumentIds.includes(

View File

@@ -18,7 +18,7 @@ import {
SendIcon,
StopGeneratingIcon,
} from "@/components/icons/icons";
import { DanswerDocument, SourceMetadata } from "@/lib/search/interfaces";
import { DanswerDocument } from "@/lib/search/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import {
Tooltip,
@@ -37,41 +37,9 @@ 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,
@@ -100,7 +68,32 @@ export function ChatInputBar({
chatSessionId,
inputPrompts,
toggleFilters,
}: ChatInputBarProps) {
}: {
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;
}) {
useEffect(() => {
const textarea = textAreaRef.current;
if (textarea) {
@@ -347,26 +340,23 @@ 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: 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>
)
)}
{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>
))}
<a
key={filteredPrompts.length}
@@ -440,7 +430,6 @@ 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">
@@ -575,16 +564,6 @@ 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">

View File

@@ -1,109 +0,0 @@
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>
);
}

View File

@@ -1,39 +0,0 @@
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>
);
}

View File

@@ -2,7 +2,7 @@ import {
AnswerPiecePacket,
DanswerDocument,
Filters,
DocumentInfoPacket,
FinalContextDocs,
StreamStopInfo,
} from "@/lib/search/interfaces";
import { handleSSEStream } from "@/lib/search/streamingUtils";
@@ -103,7 +103,7 @@ export type PacketType =
| ToolCallMetadata
| BackendMessage
| AnswerPiecePacket
| DocumentInfoPacket
| FinalContextDocs
| DocumentsResponse
| FileChatDisplay
| StreamingError
@@ -644,6 +644,7 @@ export async function useScrollonStream({
}: {
chatState: ChatState;
scrollableDivRef: RefObject<HTMLDivElement>;
waitForScrollRef: RefObject<boolean>;
scrollDist: MutableRefObject<number>;
endDivRef: RefObject<HTMLDivElement>;
debounceNumber: number;

View File

@@ -6,53 +6,45 @@ import { ValidSources } from "@/lib/types";
import React, { memo } from "react";
import isEqual from "lodash/isEqual";
export const MemoizedAnchor = memo(
({ docs, updatePresentingDocument, children }: any) => {
const value = children?.toString();
if (value?.startsWith("[") && value?.endsWith("]")) {
const match = value.match(/\[(\d+)\]/);
if (match) {
const index = parseInt(match[1], 10) - 1;
const associatedDoc = docs && docs[index];
export const MemoizedAnchor = memo(({ docs, children }: any) => {
console.log(children);
const value = children?.toString();
if (value?.startsWith("[") && value?.endsWith("]")) {
const match = value.match(/\[(\d+)\]/);
if (match) {
const index = parseInt(match[1], 10) - 1;
const associatedDoc = docs && docs[index];
const url = associatedDoc?.link
? new URL(associatedDoc.link).origin + "/favicon.ico"
: "";
const url = associatedDoc?.link
? new URL(associatedDoc.link).origin + "/favicon.ico"
: "";
const getIcon = (sourceType: ValidSources, link: string) => {
return getSourceMetadata(sourceType).icon({ size: 18 });
};
const getIcon = (sourceType: ValidSources, link: string) => {
return getSourceMetadata(sourceType).icon({ size: 18 });
};
const icon =
associatedDoc?.source_type === "web" ? (
<WebResultIcon url={associatedDoc.link} />
) : (
getIcon(
associatedDoc?.source_type || "web",
associatedDoc?.link || ""
)
);
return (
<MemoizedLink
updatePresentingDocument={updatePresentingDocument}
document={{ ...associatedDoc, icon, url }}
>
{children}
</MemoizedLink>
const icon =
associatedDoc?.source_type === "web" ? (
<WebResultIcon url={associatedDoc.link} />
) : (
getIcon(
associatedDoc?.source_type || "web",
associatedDoc?.link || ""
)
);
}
return (
<MemoizedLink document={{ ...associatedDoc, icon, url }}>
{children}
</MemoizedLink>
);
}
return (
<MemoizedLink updatePresentingDocument={updatePresentingDocument}>
{children}
</MemoizedLink>
);
}
);
return <MemoizedLink>{children}</MemoizedLink>;
});
export const MemoizedLink = memo((props: any) => {
const { node, document, updatePresentingDocument, ...rest } = props;
const { node, document, ...rest } = props;
const value = rest.children;
if (value?.toString().startsWith("*")) {
@@ -66,21 +58,22 @@ export const MemoizedLink = memo((props: any) => {
icon={document?.icon as React.ReactNode}
link={rest?.href}
document={document as LoadedDanswerDocument}
updatePresentingDocument={updatePresentingDocument}
>
{rest.children}
</Citation>
);
} else {
return (
<a
onMouseDown={() =>
rest.href ? window.open(rest.href, "_blank") : undefined
}
className="cursor-pointer text-link hover:text-link-hover"
>
{rest.children}
</a>
);
}
return (
<a
onMouseDown={() => rest.href && window.open(rest.href, "_blank")}
className="cursor-pointer text-link hover:text-link-hover"
>
{rest.children}
</a>
);
});
export const MemoizedParagraph = memo(

View File

@@ -10,7 +10,6 @@ import {
import { FeedbackType } from "../types";
import React, {
memo,
ReactNode,
useCallback,
useContext,
useEffect,
@@ -22,7 +21,6 @@ import ReactMarkdown from "react-markdown";
import {
DanswerDocument,
FilteredDanswerDocument,
LoadedDanswerDocument,
} from "@/lib/search/interfaces";
import { SearchSummary } from "./SearchSummary";
@@ -190,7 +188,6 @@ export const AIMessage = ({
currentPersona,
otherMessagesCanSwitchTo,
onMessageSelection,
setPresentingDocument,
index,
}: {
index?: number;
@@ -221,7 +218,6 @@ export const AIMessage = ({
retrievalDisabled?: boolean;
overriddenModel?: string;
regenerate?: (modelOverRide: LlmOverride) => Promise<void>;
setPresentingDocument?: (document: DanswerDocument) => void;
}) => {
const toolCallGenerating = toolCall && !toolCall.tool_result;
const processContent = (content: string | JSX.Element) => {
@@ -312,12 +308,7 @@ export const AIMessage = ({
const anchorCallback = useCallback(
(props: any) => (
<MemoizedAnchor
updatePresentingDocument={setPresentingDocument}
docs={docs}
>
{props.children}
</MemoizedAnchor>
<MemoizedAnchor docs={docs}>{props.children}</MemoizedAnchor>
),
[docs]
);

View File

@@ -35,9 +35,25 @@ export const LlmTab = forwardRef<HTMLDivElement, LlmTabProps>(
checkPersonaRequiresImageGeneration(currentAssistant);
const { llmProviders } = useChatContext();
const { setLlmOverride, temperature, updateTemperature } =
llmOverrideManager;
const { setLlmOverride, temperature, setTemperature } = 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">
@@ -92,26 +108,26 @@ export const LlmTab = forwardRef<HTMLDivElement, LlmTabProps>(
<input
type="range"
onChange={(e) =>
updateTemperature(parseFloat(e.target.value))
handleTemperatureChange(parseFloat(e.target.value))
}
className="w-full p-2 border border-border rounded-md"
min="0"
max="2"
step="0.01"
value={temperature || 0}
value={localTemperature}
/>
<div
className="absolute text-sm"
style={{
left: `${(temperature || 0) * 50}%`,
left: `${(localTemperature || 0) * 50}%`,
transform: `translateX(-${Math.min(
Math.max((temperature || 0) * 50, 10),
Math.max((localTemperature || 0) * 50, 10),
90
)}%)`,
top: "-1.5rem",
}}
>
{temperature}
{localTemperature}
</div>
</div>
</>

View File

@@ -17,8 +17,6 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoader";
import { Persona } from "@/app/admin/assistants/interfaces";
import { Button } from "@/components/ui/button";
import { DanswerDocument } from "@/lib/search/interfaces";
import TextView from "@/components/chat_search/TextView";
function BackToDanswerButton() {
const router = useRouter();
@@ -43,9 +41,6 @@ export function SharedChatDisplay({
persona: Persona;
}) {
const [isReady, setIsReady] = useState(false);
const [presentingDocument, setPresentingDocument] =
useState<DanswerDocument | null>(null);
useEffect(() => {
Prism.highlightAll();
setIsReady(true);
@@ -68,70 +63,61 @@ export function SharedChatDisplay({
);
return (
<>
{presentingDocument && (
<TextView
presentingDocument={presentingDocument}
onClose={() => setPresentingDocument(null)}
/>
)}
<div className="w-full h-[100dvh] overflow-hidden">
<div className="flex max-h-full overflow-hidden pb-[72px]">
<div className="flex w-full overflow-hidden overflow-y-scroll">
<div className="w-full h-full flex-col flex max-w-message-max mx-auto">
<div className="px-5 pt-8">
<h1 className="text-3xl text-strong font-bold">
{chatSession.description ||
`Chat ${chatSession.chat_session_id}`}
</h1>
<p className="text-emphasis">
{humanReadableFormat(chatSession.time_created)}
</p>
<div className="w-full h-[100dvh] overflow-hidden">
<div className="flex max-h-full overflow-hidden pb-[72px]">
<div className="flex w-full overflow-hidden overflow-y-scroll">
<div className="w-full h-full flex-col flex max-w-message-max mx-auto">
<div className="px-5 pt-8">
<h1 className="text-3xl text-strong font-bold">
{chatSession.description ||
`Chat ${chatSession.chat_session_id}`}
</h1>
<p className="text-emphasis">
{humanReadableFormat(chatSession.time_created)}
</p>
<Separator />
</div>
{isReady ? (
<div className="w-full pb-16">
{messages.map((message) => {
if (message.type === "user") {
return (
<HumanMessage
shared
key={message.messageId}
content={message.message}
files={message.files}
/>
);
} else {
return (
<AIMessage
shared
setPresentingDocument={setPresentingDocument}
currentPersona={persona}
key={message.messageId}
messageId={message.messageId}
content={message.message}
files={message.files || []}
citedDocuments={getCitedDocumentsFromMessage(message)}
isComplete
/>
);
}
})}
</div>
) : (
<div className="grow flex-0 h-screen w-full flex items-center justify-center">
<div className="mb-[33vh]">
<DanswerInitializingLoader />
</div>
</div>
)}
<Separator />
</div>
{isReady ? (
<div className="w-full pb-16">
{messages.map((message) => {
if (message.type === "user") {
return (
<HumanMessage
shared
key={message.messageId}
content={message.message}
files={message.files}
/>
);
} else {
return (
<AIMessage
shared
currentPersona={persona}
key={message.messageId}
messageId={message.messageId}
content={message.message}
files={message.files || []}
citedDocuments={getCitedDocumentsFromMessage(message)}
isComplete
/>
);
}
})}
</div>
) : (
<div className="grow flex-0 h-screen w-full flex items-center justify-center">
<div className="mb-[33vh]">
<DanswerInitializingLoader />
</div>
</div>
)}
</div>
</div>
<BackToDanswerButton />
</div>
</>
<BackToDanswerButton />
</div>
);
}

View File

@@ -96,7 +96,7 @@ export function SourceSelector({
});
};
let allSourcesSelected = selectedSources.length == existingSources.length;
let allSourcesSelected = selectedSources.length > 0;
const toggleAllSources = () => {
if (allSourcesSelected) {

View File

@@ -28,13 +28,12 @@ import { DraggableAssistantCard } from "@/components/assistants/AssistantCards";
import { updateUserAssistantList } from "@/lib/assistants/updateAssistantPreferences";
import Text from "@/components/ui/text";
import { getDisplayNameForModel, LlmOverrideManager } from "@/lib/hooks";
import { LlmOverrideManager } from "@/lib/hooks";
import { Tab } from "@headlessui/react";
import { AssistantIcon } from "../assistants/AssistantIcon";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { restrictToParentElement } from "@dnd-kit/modifiers";
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "../ui/drawer";
import { truncateString } from "@/lib/utils";
const AssistantSelector = ({
liveAssistant,
@@ -46,7 +45,7 @@ const AssistantSelector = ({
liveAssistant: Persona;
onAssistantChange: (assistant: Persona) => void;
chatSessionId?: string;
llmOverrideManager: LlmOverrideManager;
llmOverrideManager?: LlmOverrideManager;
isMobile: boolean;
}) => {
const { finalAssistants } = useAssistants();
@@ -54,9 +53,11 @@ 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>(() => {
@@ -90,6 +91,21 @@ 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);
@@ -102,7 +118,7 @@ const AssistantSelector = ({
const [_, currentLlm] = getFinalLLM(
llmProviders,
liveAssistant,
llmOverrideManager.llmOverride ?? null
llmOverrideManager?.llmOverride ?? null
);
const requiresImageGeneration =
@@ -187,10 +203,11 @@ 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,
@@ -198,6 +215,7 @@ const AssistantSelector = ({
if (chatSessionId) {
updateModelOverrideForChatSession(chatSessionId, value);
}
setIsOpen(false);
}}
/>
<div className="mt-4">
@@ -224,31 +242,26 @@ const AssistantSelector = ({
<input
type="range"
onChange={(e) =>
llmOverrideManager.updateTemperature(
parseFloat(e.target.value)
)
handleTemperatureChange(parseFloat(e.target.value))
}
className="w-full p-2 border border-border rounded-md"
min="0"
max="2"
step="0.01"
value={llmOverrideManager.temperature?.toString() || "0"}
value={localTemperature}
/>
<div
className="absolute text-sm"
style={{
left: `${(llmOverrideManager.temperature || 0) * 50}%`,
left: `${(localTemperature || 0) * 50}%`,
transform: `translateX(-${Math.min(
Math.max(
(llmOverrideManager.temperature || 0) * 50,
10
),
Math.max((localTemperature || 0) * 50, 10),
90
)}%)`,
top: "-1.5rem",
}}
>
{llmOverrideManager.temperature}
{localTemperature}
</div>
</div>
</>
@@ -297,9 +310,7 @@ const AssistantSelector = ({
<span className="font-bold">{liveAssistant.name}</span>
</div>
<div className="flex items-center">
<span className="mr-2 text-xs">
{truncateString(getDisplayNameForModel(currentLlm), 30)}
</span>
<span className="mr-2 text-xs">{currentLlm}</span>
<FiChevronDown
className={`w-5 h-5 text-white transition-transform duration-300 transform ${
isOpen ? "rotate-180" : ""

View File

@@ -1,173 +0,0 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Download, XIcon, ZoomIn, ZoomOut } from "lucide-react";
import { DanswerDocument } from "@/lib/search/interfaces";
import { MinimalMarkdown } from "./MinimalMarkdown";
interface TextViewProps {
presentingDocument: DanswerDocument;
onClose: () => void;
}
export default function TextView({
presentingDocument,
onClose,
}: TextViewProps) {
const [zoom, setZoom] = useState(100);
const [fileContent, setFileContent] = useState<string>("");
const [fileUrl, setFileUrl] = useState<string>("");
const [fileName, setFileName] = useState<string>("");
const [isLoading, setIsLoading] = useState(true);
const [fileType, setFileType] = useState<string>("application/octet-stream");
const isMarkdownFormat = (mimeType: string): boolean => {
const markdownFormats = [
"text/markdown",
"text/x-markdown",
"text/plain",
"text/x-rst",
"text/x-org",
];
return markdownFormats.some((format) => mimeType.startsWith(format));
};
const isSupportedIframeFormat = (mimeType: string): boolean => {
const supportedFormats = [
"application/pdf",
"image/png",
"image/jpeg",
"image/gif",
"image/svg+xml",
];
return supportedFormats.some((format) => mimeType.startsWith(format));
};
const fetchFile = useCallback(async () => {
setIsLoading(true);
const fileId = presentingDocument.document_id.split("__")[1];
try {
const response = await fetch(
`/api/chat/file/${encodeURIComponent(fileId)}`,
{
method: "GET",
}
);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
setFileUrl(url);
setFileName(presentingDocument.semantic_identifier || "document");
const contentType =
response.headers.get("Content-Type") || "application/octet-stream";
setFileType(contentType);
if (isMarkdownFormat(blob.type)) {
const text = await blob.text();
setFileContent(text);
}
} catch (error) {
console.error("Error fetching file:", error);
} finally {
setTimeout(() => {
setIsLoading(false);
}, 1000);
}
}, [presentingDocument]);
useEffect(() => {
fetchFile();
}, [fetchFile]);
const handleDownload = () => {
const link = document.createElement("a");
link.href = fileUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const handleZoomIn = () => setZoom((prev) => Math.min(prev + 25, 200));
const handleZoomOut = () => setZoom((prev) => Math.max(prev - 25, 100));
return (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent
hideCloseIcon
className="max-w-5xl w-[90vw] flex flex-col justify-between gap-y-0 h-full max-h-[80vh] p-0"
>
<DialogHeader className="px-4 mb-0 pt-2 pb-3 flex flex-row items-center justify-between border-b">
<DialogTitle className="text-lg font-medium truncate">
{fileName}
</DialogTitle>
<div className="flex items-center space-x-2">
<Button variant="ghost" size="icon" onClick={handleZoomOut}>
<ZoomOut className="h-4 w-4" />
<span className="sr-only">Zoom Out</span>
</Button>
<span className="text-sm">{zoom}%</span>
<Button variant="ghost" size="icon" onClick={handleZoomIn}>
<ZoomIn className="h-4 w-4" />
<span className="sr-only">Zoom In</span>
</Button>
<Button variant="ghost" size="icon" onClick={handleDownload}>
<Download className="h-4 w-4" />
<span className="sr-only">Download</span>
</Button>
<Button variant="ghost" size="icon" onClick={() => onClose()}>
<XIcon className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</div>
</DialogHeader>
<div className="mt-0 rounded-b-lg flex-1 overflow-hidden">
<div className="flex items-center justify-center w-full h-full">
{isLoading ? (
<div className="flex flex-col items-center justify-center h-full">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-primary"></div>
<p className="mt-6 text-lg font-medium text-muted-foreground">
Loading document...
</p>
</div>
) : (
<div
className={`w-full h-full transform origin-center transition-transform duration-300 ease-in-out`}
style={{ transform: `scale(${zoom / 100})` }}
>
{isSupportedIframeFormat(fileType) ? (
<iframe
src={`${fileUrl}#toolbar=0`}
className="w-full h-full border-none"
title="File Viewer"
/>
) : isMarkdownFormat(fileType) ? (
<div className="w-full h-full p-6 overflow-y-scroll overflow-x-hidden">
<MinimalMarkdown
content={fileContent}
className="w-full pb-4 h-full text-lg text-wrap break-words"
/>
</div>
) : (
<div className="flex flex-col items-center justify-center h-full">
<p className="text-lg font-medium text-muted-foreground">
This file format is not supported for preview.
</p>
<Button className="mt-4" onClick={handleDownload}>
Download File
</Button>
</div>
)}
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,7 +1,6 @@
import { WebResultIcon } from "@/components/WebResultIcon";
import { SourceIcon } from "@/components/SourceIcon";
import { DanswerDocument } from "@/lib/search/interfaces";
import { truncateString } from "@/lib/utils";
export default function SourceCard({ doc }: { doc: DanswerDocument }) {
return (
@@ -18,7 +17,12 @@ export default function SourceCard({ doc }: { doc: DanswerDocument }) {
) : (
<SourceIcon sourceType={doc.source_type} iconSize={18} />
)}
<p>{truncateString(doc.semantic_identifier || doc.document_id, 12)}</p>
<p>
{(doc.semantic_identifier || doc.document_id).slice(0, 12).trim()}
{(doc.semantic_identifier || doc.document_id).length > 12 && (
<span className="text-text-500">...</span>
)}
</p>
</div>
<div className="line-clamp-2 text-sm font-semibold"></div>
<div className="line-clamp-2 text-sm font-normal leading-snug text-text-700">

View File

@@ -11,7 +11,6 @@ import {
classifyAssistants,
orderAssistantsForUser,
getUserCreatedAssistants,
filterAssistants,
} from "@/lib/assistants/utils";
import { useUser } from "../user/UserProvider";
@@ -146,13 +145,22 @@ export const AssistantsProvider: React.FC<{
if (!response.ok) throw new Error("Failed to fetch assistants");
let assistants: Persona[] = await response.json();
let filteredAssistants = filterAssistants(
assistants,
hasAnyConnectors,
hasImageCompatibleModel
);
if (!hasImageCompatibleModel) {
assistants = assistants.filter(
(assistant) =>
!assistant.tools.some(
(tool) => tool.in_code_tool_id === "ImageGenerationTool"
)
);
}
setAssistants(filteredAssistants);
if (!hasAnyConnectors) {
assistants = assistants.filter(
(assistant) => assistant.num_chunks === 0
);
}
setAssistants(assistants);
// Fetch and update allAssistants for admins and curators
await fetchPersonas();

View File

@@ -19,6 +19,7 @@ interface LlmListProps {
scrollable?: boolean;
hideProviderIcon?: boolean;
requiresImageGeneration?: boolean;
includeUserDefault?: boolean;
currentAssistant?: Persona;
}
@@ -30,6 +31,7 @@ export const LlmList: React.FC<LlmListProps> = ({
userDefault,
scrollable,
requiresImageGeneration,
includeUserDefault = false,
}) => {
const llmOptionsByProvider: {
[provider: string]: {

View File

@@ -19,7 +19,6 @@ import { FiTag } from "react-icons/fi";
import { SettingsContext } from "../settings/SettingsProvider";
import { CustomTooltip, TooltipGroup } from "../tooltip/CustomTooltip";
import { WarningCircle } from "@phosphor-icons/react";
import TextView from "../chat_search/TextView";
import { SearchResultIcon } from "../SearchResultIcon";
export const buildDocumentSummaryDisplay = (
@@ -189,12 +188,6 @@ export const DocumentDisplay = ({
const relevance_explanation =
document.relevance_explanation ?? additional_relevance?.content;
const settings = useContext(SettingsContext);
const [presentingDocument, setPresentingDocument] =
useState<DanswerDocument | null>(null);
const handleViewFile = async () => {
setPresentingDocument(document);
};
return (
<div
@@ -226,22 +219,19 @@ export const DocumentDisplay = ({
}`}
>
<div className="flex relative">
<button
type="button"
className={`rounded-lg flex font-bold text-link max-w-full`}
onClick={() => {
if (document.link) {
window.open(document.link, "_blank");
} else {
handleViewFile();
}
}}
<a
className={`rounded-lg flex font-bold text-link max-w-full ${
document.link ? "" : "pointer-events-none"
}`}
href={document.link}
target="_blank"
rel="noopener noreferrer"
>
<SourceIcon sourceType={document.source_type} iconSize={22} />
<p className="truncate text-wrap break-all ml-2 my-auto line-clamp-1 text-base max-w-full">
{document.semantic_identifier || document.document_id}
</p>
</button>
</a>
<div className="ml-auto flex items-center">
<TooltipGroup>
{isHovered && messageId && (
@@ -280,13 +270,6 @@ export const DocumentDisplay = ({
<DocumentMetadataBlock document={document} />
</div>
{presentingDocument && (
<TextView
presentingDocument={presentingDocument}
onClose={() => setPresentingDocument(null)}
/>
)}
<p
style={{ transition: "height 0.30s ease-in-out" }}
className="pl-1 pt-2 pb-3 break-words text-wrap"
@@ -314,14 +297,11 @@ export const AgenticDocumentDisplay = ({
setPopup,
}: DocumentDisplayProps) => {
const [isHovered, setIsHovered] = useState(false);
const [presentingDocument, setPresentingDocument] =
useState<DanswerDocument | null>(null);
const [alternativeToggled, setAlternativeToggled] = useState(false);
const relevance_explanation =
document.relevance_explanation ?? additional_relevance?.content;
return (
<div
key={document.semantic_identifier}
@@ -340,24 +320,19 @@ export const AgenticDocumentDisplay = ({
}`}
>
<div className="flex relative">
<button
type="button"
<a
className={`rounded-lg flex font-bold text-link max-w-full ${
document.link ? "" : "pointer-events-none"
}`}
onClick={() => {
if (document.link) {
window.open(document.link, "_blank");
} else {
setPresentingDocument(document);
}
}}
href={document.link}
target="_blank"
rel="noopener noreferrer"
>
<SourceIcon sourceType={document.source_type} iconSize={22} />
<p className="truncate text-wrap break-all ml-2 my-auto line-clamp-1 text-base max-w-full">
{document.semantic_identifier || document.document_id}
</p>
</button>
</a>
<div className="ml-auto items-center flex">
<TooltipGroup>
@@ -390,12 +365,6 @@ export const AgenticDocumentDisplay = ({
<div className="mt-1">
<DocumentMetadataBlock document={document} />
</div>
{presentingDocument && (
<TextView
presentingDocument={presentingDocument}
onClose={() => setPresentingDocument(null)}
/>
)}
<div className="pt-2 break-words flex gap-x-2">
<p

View File

@@ -13,14 +13,12 @@ export function Citation({
link,
document,
index,
updatePresentingDocument,
icon,
url,
}: {
link?: string;
children?: JSX.Element | string | null | ReactNode;
index?: number;
updatePresentingDocument: (documentIndex: LoadedDanswerDocument) => void;
document: LoadedDanswerDocument;
icon?: React.ReactNode;
url?: string;
@@ -35,13 +33,7 @@ export function Citation({
<Tooltip>
<TooltipTrigger asChild>
<div
onMouseDown={() => {
if (!link) {
updatePresentingDocument(document);
} else {
window.open(link, "_blank");
}
}}
onMouseDown={() => window.open(link, "_blank")}
className="inline-flex items-center ml-1 cursor-pointer transition-all duration-200 ease-in-out"
>
<span className="relative min-w-[1.4rem] text-center no-underline -top-0.5 px-1.5 py-0.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-full border border-gray-300 hover:bg-gray-200 hover:text-gray-900 shadow-sm no-underline">
@@ -61,13 +53,7 @@ export function Citation({
<Tooltip>
<TooltipTrigger asChild>
<div
onMouseDown={() => {
if (!link) {
updatePresentingDocument(document);
} else {
window.open(link, "_blank");
}
}}
onMouseDown={() => window.open(link, "_blank")}
className="inline-flex items-center ml-1 cursor-pointer transition-all duration-200 ease-in-out"
>
<span className="relative min-w-[1.4rem] pchatno-underline -top-0.5 px-1.5 py-0.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-full border border-gray-300 hover:bg-gray-200 hover:text-gray-900 shadow-sm no-underline">

View File

@@ -1,125 +0,0 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
hideCloseIcon?: boolean;
}
>(({ className, children, hideCloseIcon = false, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg dark:border-neutral-800 dark:bg-neutral-950",
className
)}
{...props}
>
{children}
{!hideCloseIcon && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-neutral-500 dark:text-neutral-400", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@@ -1,7 +1,6 @@
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);
@@ -118,31 +117,3 @@ 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);
}

View File

@@ -4,7 +4,6 @@ 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[];
@@ -40,21 +39,42 @@ 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))
);
let filteredAssistants = filterAssistants(
assistants,
hasAnyConnectors,
hasImageCompatibleModel
);
// 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"
)
);
}
return {
assistants: filteredAssistants,

View File

@@ -16,7 +16,6 @@ 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";
@@ -72,9 +71,7 @@ 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,
@@ -160,7 +157,7 @@ export interface LlmOverrideManager {
globalDefault: LlmOverride;
setGlobalDefault: React.Dispatch<React.SetStateAction<LlmOverride>>;
temperature: number | null;
updateTemperature: (temperature: number | null) => void;
setTemperature: React.Dispatch<React.SetStateAction<number | null>>;
updateModelOverrideForChatSession: (chatSession?: ChatSession) => void;
}
export function useLlmOverride(
@@ -215,20 +212,6 @@ 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,
@@ -236,10 +219,9 @@ export function useLlmOverride(
globalDefault,
setGlobalDefault,
temperature,
updateTemperature,
setTemperature,
};
}
/*
EE Only APIs
*/

View File

@@ -19,6 +19,10 @@ export interface AnswerPiecePacket {
answer_piece: string;
}
export interface FinalContextDocs {
final_context_docs: DanswerDocument[];
}
export enum StreamStopReason {
CONTEXT_LENGTH = "CONTEXT_LENGTH",
CANCELLED = "CANCELLED",

View File

@@ -5,8 +5,9 @@ import {
import {
AnswerPiecePacket,
DanswerDocument,
ErrorMessagePacket,
DocumentInfoPacket,
ErrorMessagePacket,
FinalContextDocs,
Quote,
QuotesInfoPacket,
RelevanceChunk,
@@ -91,7 +92,7 @@ export const searchRequestStreamed = async ({
| DocumentInfoPacket
| LLMRelevanceFilterPacket
| BackendMessage
| DocumentInfoPacket
| FinalContextDocs
| RelevanceChunk
>(decoder.decode(value, { stream: true }), previousPartialChunk);
if (!completedChunks.length && !partialChunk) {

View File

@@ -4,7 +4,3 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export const truncateString = (str: string, maxLength: number) => {
return str.length > maxLength ? str.slice(0, maxLength - 1) + "..." : str;
};

View File

@@ -6,12 +6,9 @@ import { TEST_CREDENTIALS } from "./constants";
setup("authenticate", async ({ page }) => {
const { email, password } = TEST_CREDENTIALS;
await page.goto("http://localhost:3000/chat");
await page.goto("http://localhost:3000/search");
const url = page.url();
console.log(`Initial URL after navigation: ${url}`);
await page.waitForURL("http://localhost:3000/auth/login?next=%2Fchat");
await page.waitForURL("http://localhost:3000/auth/login?next=%2Fsearch");
await expect(page).toHaveTitle("Danswer");
@@ -21,7 +18,7 @@ setup("authenticate", async ({ page }) => {
// Click the login button
await page.click('button[type="submit"]');
await page.waitForURL("http://localhost:3000/chat");
await page.waitForURL("http://localhost:3000/search");
await page.context().storageState({ path: "admin_auth.json" });
});

View File

@@ -8,20 +8,12 @@ test(
async ({ page }, testInfo) => {
// Test simple loading
await page.goto("http://localhost:3000/chat");
// 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();
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/
);
}
);

View File

@@ -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/chat");
await page.goto("http://localhost:3000/search");
await page.waitForURL("http://localhost:3000/auth/login?next=%2Fchat");
await page.waitForURL("http://localhost:3000/auth/login?next=%2Fsearch");
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/chat");
await page.waitForURL("http://localhost:3000/search");
}
);

View File

@@ -0,0 +1,19 @@
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/
);
}
);