mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-18 08:15:48 +00:00
Compare commits
1 Commits
remove_emp
...
cloud_buil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32d428eb7 |
@@ -1,136 +0,0 @@
|
||||
name: Build and Push Cloud Web Image on Tag
|
||||
# Identical to the web container build, but with correct image tag and build args
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
REGISTRY_IMAGE: danswer/danswer-cloud-web-server
|
||||
LATEST_TAG: ${{ contains(github.ref_name, 'latest') }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on:
|
||||
- runs-on
|
||||
- runner=${{ matrix.platform == 'linux/amd64' && '8cpu-linux-x64' || '8cpu-linux-arm64' }}
|
||||
- run-id=${{ github.run_id }}
|
||||
- tag=platform-${{ matrix.platform }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=raw,value=${{ env.REGISTRY_IMAGE }}:${{ github.ref_name }}
|
||||
type=raw,value=${{ env.LATEST_TAG == 'true' && format('{0}:latest', env.REGISTRY_IMAGE) || '' }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./web
|
||||
file: ./web/Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
build-args: |
|
||||
DANSWER_VERSION=${{ github.ref_name }}
|
||||
NEXT_PUBLIC_CLOUD_ENABLED=true
|
||||
NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.POSTHOG_KEY }}
|
||||
NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.POSTHOG_HOST }}
|
||||
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
|
||||
# needed due to weird interactions with the builds for different platforms
|
||||
no-cache: true
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
# trivy has their own rate limiting issues causing this action to flake
|
||||
# we worked around it by hardcoding to different db repos in env
|
||||
# can re-enable when they figure it out
|
||||
# https://github.com/aquasecurity/trivy/discussions/7538
|
||||
# https://github.com/aquasecurity/trivy-action/issues/389
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
env:
|
||||
TRIVY_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-db:2'
|
||||
TRIVY_JAVA_DB_REPOSITORY: 'public.ecr.aws/aquasecurity/trivy-java-db:1'
|
||||
with:
|
||||
image-ref: docker.io/${{ env.REGISTRY_IMAGE }}:${{ github.ref_name }}
|
||||
severity: 'CRITICAL,HIGH'
|
||||
66
.github/workflows/pr-backport-autotrigger.yml
vendored
66
.github/workflows/pr-backport-autotrigger.yml
vendored
@@ -1,34 +1,30 @@
|
||||
name: Backport on Merge
|
||||
|
||||
# Note this workflow does not trigger the builds, be sure to manually tag the branches to trigger the builds
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed] # Later we check for merge so only PRs that go in can get backported
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
actions: write # Allows the workflow to trigger other workflows on push
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.YUHONG_GH_ACTIONS }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ssh-key: "${{ secrets.RKUO_DEPLOY_KEY }}"
|
||||
fetch-depth: 0
|
||||
fetch-depth: 0 # Fetch all history for all branches and tags
|
||||
|
||||
- name: Set up Git user
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config user.name "Richard Kuo [bot]"
|
||||
git config user.email "rkuo[bot]@danswer.ai"
|
||||
git fetch --prune
|
||||
|
||||
git config --global user.name "yuhongsun96"
|
||||
git config --global user.email "yuhongsun96@gmail.com"
|
||||
# Configure Git to use the PAT for authentication
|
||||
git remote set-url origin https://yuhongsun96:${{ secrets.YUHONG_GH_ACTIONS }}@github.com/${{ github.repository }}.git
|
||||
|
||||
- name: Check for Backport Checkbox
|
||||
id: checkbox-check
|
||||
run: |
|
||||
@@ -51,14 +47,8 @@ jobs:
|
||||
# Fetch latest tags for beta and stable
|
||||
LATEST_BETA_TAG=$(git tag -l "v[0-9]*.[0-9]*.[0-9]*-beta.[0-9]*" | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$" | grep -v -- "-cloud" | sort -Vr | head -n 1)
|
||||
LATEST_STABLE_TAG=$(git tag -l "v[0-9]*.[0-9]*.[0-9]*" | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$" | sort -Vr | head -n 1)
|
||||
|
||||
# Handle case where no beta tags exist
|
||||
if [[ -z "$LATEST_BETA_TAG" ]]; then
|
||||
NEW_BETA_TAG="v1.0.0-beta.1"
|
||||
else
|
||||
NEW_BETA_TAG=$(echo $LATEST_BETA_TAG | awk -F '[.-]' '{print $1 "." $2 "." $3 "-beta." ($NF+1)}')
|
||||
fi
|
||||
|
||||
# Increment latest beta tag
|
||||
NEW_BETA_TAG=$(echo $LATEST_BETA_TAG | awk -F '[.-]' '{print $1 "." $2 "." $3 "-beta." ($NF+1)}')
|
||||
# Increment latest stable tag
|
||||
NEW_STABLE_TAG=$(echo $LATEST_STABLE_TAG | awk -F '.' '{print $1 "." $2 "." ($3+1)}')
|
||||
echo "latest_beta_tag=$LATEST_BETA_TAG" >> $GITHUB_OUTPUT
|
||||
@@ -80,45 +70,29 @@ jobs:
|
||||
run: |
|
||||
set -e
|
||||
echo "Backporting to beta ${{ steps.list-branches.outputs.beta }} and stable ${{ steps.list-branches.outputs.stable }}"
|
||||
|
||||
# Echo the merge commit SHA
|
||||
echo "Merge commit SHA: ${{ github.event.pull_request.merge_commit_sha }}"
|
||||
|
||||
# Fetch all history for all branches and tags
|
||||
git fetch --prune
|
||||
|
||||
# Reset and prepare the beta branch
|
||||
# Checkout the beta branch
|
||||
git checkout ${{ steps.list-branches.outputs.beta }}
|
||||
echo "Last 5 commits on beta branch:"
|
||||
git log -n 5 --pretty=format:"%H"
|
||||
echo "" # Newline for formatting
|
||||
|
||||
# Cherry-pick the merge commit from the merged PR
|
||||
git cherry-pick -m 1 ${{ github.event.pull_request.merge_commit_sha }} || {
|
||||
echo "Cherry-pick to beta failed due to conflicts."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create new beta branch/tag
|
||||
# Create new beta tag
|
||||
git tag ${{ steps.list-branches.outputs.new_beta_tag }}
|
||||
# Push the changes and tag to the beta branch using PAT
|
||||
git push origin ${{ steps.list-branches.outputs.beta }}
|
||||
# Push the changes and tag to the beta branch
|
||||
git push origin ${{ steps.list-branches.outputs.beta }} --force
|
||||
git push origin ${{ steps.list-branches.outputs.new_beta_tag }}
|
||||
|
||||
# Reset and prepare the stable branch
|
||||
# Checkout the stable branch
|
||||
git checkout ${{ steps.list-branches.outputs.stable }}
|
||||
echo "Last 5 commits on stable branch:"
|
||||
git log -n 5 --pretty=format:"%H"
|
||||
echo "" # Newline for formatting
|
||||
|
||||
# Cherry-pick the merge commit from the merged PR
|
||||
git cherry-pick -m 1 ${{ github.event.pull_request.merge_commit_sha }} || {
|
||||
echo "Cherry-pick to stable failed due to conflicts."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create new stable branch/tag
|
||||
# Create new stable tag
|
||||
git tag ${{ steps.list-branches.outputs.new_stable_tag }}
|
||||
# Push the changes and tag to the stable branch using PAT
|
||||
git push origin ${{ steps.list-branches.outputs.stable }}
|
||||
git push origin ${{ steps.list-branches.outputs.new_stable_tag }}
|
||||
# Push the changes and tag to the stable branch
|
||||
git push origin ${{ steps.list-branches.outputs.stable }} --force
|
||||
git push origin ${{ steps.list-branches.outputs.new_stable_tag }}
|
||||
@@ -9,7 +9,7 @@ from sqlalchemy import pool
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.db.engine import build_connection_string
|
||||
from danswer.db.models import Base
|
||||
from celery.backends.database.session import ResultModelBase # type: ignore
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
"""remove rt
|
||||
|
||||
Revision ID: 949b4a92a401
|
||||
Revises: 1b10e1fda030
|
||||
Create Date: 2024-10-26 13:06:06.937969
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Import your models and constants
|
||||
from danswer.db.models import (
|
||||
Connector,
|
||||
ConnectorCredentialPair,
|
||||
Credential,
|
||||
IndexAttempt,
|
||||
)
|
||||
from danswer.configs.constants import DocumentSource
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "949b4a92a401"
|
||||
down_revision = "1b10e1fda030"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Deletes all RequestTracker connectors and associated data
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
|
||||
connectors_to_delete = (
|
||||
session.query(Connector)
|
||||
.filter(Connector.source == DocumentSource.REQUESTTRACKER)
|
||||
.all()
|
||||
)
|
||||
|
||||
connector_ids = [connector.id for connector in connectors_to_delete]
|
||||
|
||||
if connector_ids:
|
||||
cc_pairs_to_delete = (
|
||||
session.query(ConnectorCredentialPair)
|
||||
.filter(ConnectorCredentialPair.connector_id.in_(connector_ids))
|
||||
.all()
|
||||
)
|
||||
|
||||
cc_pair_ids = [cc_pair.id for cc_pair in cc_pairs_to_delete]
|
||||
|
||||
if cc_pair_ids:
|
||||
session.query(IndexAttempt).filter(
|
||||
IndexAttempt.connector_credential_pair_id.in_(cc_pair_ids)
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
session.query(ConnectorCredentialPair).filter(
|
||||
ConnectorCredentialPair.id.in_(cc_pair_ids)
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
credential_ids = [cc_pair.credential_id for cc_pair in cc_pairs_to_delete]
|
||||
if credential_ids:
|
||||
session.query(Credential).filter(Credential.id.in_(credential_ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
session.query(Connector).filter(Connector.id.in_(connector_ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# No-op downgrade as we cannot restore deleted data
|
||||
pass
|
||||
@@ -49,7 +49,6 @@ from httpx_oauth.oauth2 import BaseOAuth2
|
||||
from httpx_oauth.oauth2 import OAuth2Token
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import attributes
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -61,7 +60,9 @@ from danswer.configs.app_configs import AUTH_TYPE
|
||||
from danswer.configs.app_configs import DISABLE_AUTH
|
||||
from danswer.configs.app_configs import DISABLE_VERIFICATION
|
||||
from danswer.configs.app_configs import EMAIL_FROM
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.app_configs import REQUIRE_EMAIL_VERIFICATION
|
||||
from danswer.configs.app_configs import SECRET_JWT_KEY
|
||||
from danswer.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS
|
||||
from danswer.configs.app_configs import SMTP_PASS
|
||||
from danswer.configs.app_configs import SMTP_PORT
|
||||
@@ -94,7 +95,6 @@ from danswer.utils.telemetry import optional_telemetry
|
||||
from danswer.utils.telemetry import RecordType
|
||||
from danswer.utils.variable_functionality import fetch_versioned_implementation
|
||||
from shared_configs.configs import CURRENT_TENANT_ID_CONTEXTVAR
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
from shared_configs.configs import POSTGRES_DEFAULT_SCHEMA
|
||||
|
||||
|
||||
@@ -295,6 +295,29 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
CURRENT_TENANT_ID_CONTEXTVAR.reset(token)
|
||||
return user
|
||||
|
||||
async def on_after_login(
|
||||
self,
|
||||
user: User,
|
||||
request: Request | None = None,
|
||||
response: Response | None = None,
|
||||
) -> None:
|
||||
if response is None or not MULTI_TENANT:
|
||||
return
|
||||
|
||||
tenant_id = get_tenant_id_for_email(user.email)
|
||||
|
||||
tenant_token = jwt.encode(
|
||||
{"tenant_id": tenant_id}, SECRET_JWT_KEY, algorithm="HS256"
|
||||
)
|
||||
|
||||
response.set_cookie(
|
||||
key="tenant_details",
|
||||
value=tenant_token,
|
||||
httponly=True,
|
||||
secure=WEB_DOMAIN.startswith("https"),
|
||||
samesite="lax",
|
||||
)
|
||||
|
||||
async def oauth_callback(
|
||||
self: "BaseUserManager[models.UOAP, models.ID]",
|
||||
oauth_name: str,
|
||||
@@ -367,10 +390,6 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||
}
|
||||
|
||||
user = await self.user_db.create(user_dict)
|
||||
|
||||
# Explicitly set the Postgres schema for this session to ensure
|
||||
# OAuth account creation happens in the correct tenant schema
|
||||
await db_session.execute(text(f'SET search_path = "{tenant_id}"'))
|
||||
user = await self.user_db.add_oauth_account(
|
||||
user, oauth_account_dict
|
||||
)
|
||||
@@ -508,22 +527,8 @@ cookie_transport = CookieTransport(
|
||||
)
|
||||
|
||||
|
||||
# This strategy is used to add tenant_id to the JWT token
|
||||
class TenantAwareJWTStrategy(JWTStrategy):
|
||||
async def write_token(self, user: User) -> str:
|
||||
tenant_id = get_tenant_id_for_email(user.email)
|
||||
data = {
|
||||
"sub": str(user.id),
|
||||
"aud": self.token_audience,
|
||||
"tenant_id": tenant_id,
|
||||
}
|
||||
return generate_jwt(
|
||||
data, self.encode_key, self.lifetime_seconds, algorithm=self.algorithm
|
||||
)
|
||||
|
||||
|
||||
def get_jwt_strategy() -> JWTStrategy:
|
||||
return TenantAwareJWTStrategy(
|
||||
return JWTStrategy(
|
||||
secret=USER_AUTH_SECRET,
|
||||
lifetime_seconds=SESSION_EXPIRE_TIME_SECONDS,
|
||||
)
|
||||
|
||||
@@ -78,7 +78,6 @@ tasks_to_schedule = [
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Build the celery beat schedule dynamically
|
||||
beat_schedule = {}
|
||||
|
||||
|
||||
@@ -162,8 +162,8 @@ def on_worker_init(sender: Any, **kwargs: Any) -> None:
|
||||
for key in r.scan_iter(RedisConnectorIndexing.FENCE_PREFIX + "*"):
|
||||
r.delete(key)
|
||||
|
||||
for key in r.scan_iter(RedisConnectorStop.FENCE_PREFIX + "*"):
|
||||
r.delete(key)
|
||||
for key in r.scan_iter(RedisConnectorStop.FENCE_PREFIX + "*"):
|
||||
r.delete(key)
|
||||
|
||||
|
||||
# @worker_process_init.connect
|
||||
|
||||
@@ -81,7 +81,7 @@ def check_for_connector_deletion_task(self: Task, *, tenant_id: str | None) -> N
|
||||
"Soft time limit exceeded, task is being terminated gracefully."
|
||||
)
|
||||
except Exception:
|
||||
task_logger.exception(f"Unexpected exception: tenant={tenant_id}")
|
||||
task_logger.exception("Unexpected exception")
|
||||
finally:
|
||||
if lock_beat.owned():
|
||||
lock_beat.release()
|
||||
|
||||
@@ -24,6 +24,7 @@ from danswer.background.indexing.job_client import SimpleJobClient
|
||||
from danswer.background.indexing.run_indexing import run_indexing_entrypoint
|
||||
from danswer.background.indexing.run_indexing import RunIndexingCallbackInterface
|
||||
from danswer.configs.app_configs import DISABLE_INDEX_UPDATE_ON_SWAP
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.constants import CELERY_INDEXING_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT
|
||||
from danswer.configs.constants import DANSWER_REDIS_FUNCTION_LOCK_PREFIX
|
||||
@@ -55,7 +56,6 @@ from danswer.utils.logger import setup_logger
|
||||
from danswer.utils.variable_functionality import global_version
|
||||
from shared_configs.configs import INDEXING_MODEL_SERVER_HOST
|
||||
from shared_configs.configs import INDEXING_MODEL_SERVER_PORT
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
@@ -175,7 +175,7 @@ def check_for_indexing(self: Task, *, tenant_id: str | None) -> int | None:
|
||||
)
|
||||
if attempt_id:
|
||||
task_logger.info(
|
||||
f"Indexing queued: cc_pair={cc_pair.id} index_attempt={attempt_id}"
|
||||
f"Indexing queued: cc_pair_id={cc_pair.id} index_attempt_id={attempt_id}"
|
||||
)
|
||||
tasks_created += 1
|
||||
except SoftTimeLimitExceeded:
|
||||
@@ -183,7 +183,7 @@ def check_for_indexing(self: Task, *, tenant_id: str | None) -> int | None:
|
||||
"Soft time limit exceeded, task is being terminated gracefully."
|
||||
)
|
||||
except Exception:
|
||||
task_logger.exception(f"Unexpected exception: tenant={tenant_id}")
|
||||
task_logger.exception("Unexpected exception")
|
||||
finally:
|
||||
if lock_beat.owned():
|
||||
lock_beat.release()
|
||||
@@ -366,12 +366,7 @@ def try_creating_indexing_task(
|
||||
r.set(rci.fence_key, fence_value.model_dump_json())
|
||||
except Exception:
|
||||
r.delete(rci.fence_key)
|
||||
task_logger.exception(
|
||||
f"Unexpected exception: "
|
||||
f"tenant={tenant_id} "
|
||||
f"cc_pair={cc_pair.id} "
|
||||
f"search_settings={search_settings.id}"
|
||||
)
|
||||
task_logger.exception("Unexpected exception")
|
||||
return None
|
||||
finally:
|
||||
if lock.owned():
|
||||
@@ -475,9 +470,10 @@ def connector_indexing_task(
|
||||
# read related data and evaluate/print task progress
|
||||
fence_value = cast(bytes, r.get(rci.fence_key))
|
||||
if fence_value is None:
|
||||
raise ValueError(
|
||||
task_logger.info(
|
||||
f"connector_indexing_task: fence_value not found: fence={rci.fence_key}"
|
||||
)
|
||||
raise RuntimeError(f"Fence not found: fence={rci.fence_key}")
|
||||
|
||||
try:
|
||||
fence_json = fence_value.decode("utf-8")
|
||||
|
||||
@@ -79,13 +79,13 @@ def check_for_pruning(self: Task, *, tenant_id: str | None) -> None:
|
||||
if not tasks_created:
|
||||
continue
|
||||
|
||||
task_logger.info(f"Pruning queued: cc_pair={cc_pair.id}")
|
||||
task_logger.info(f"Pruning queued: cc_pair_id={cc_pair.id}")
|
||||
except SoftTimeLimitExceeded:
|
||||
task_logger.info(
|
||||
"Soft time limit exceeded, task is being terminated gracefully."
|
||||
)
|
||||
except Exception:
|
||||
task_logger.exception(f"Unexpected exception: tenant={tenant_id}")
|
||||
task_logger.exception("Unexpected exception")
|
||||
finally:
|
||||
if lock_beat.owned():
|
||||
lock_beat.release()
|
||||
@@ -201,7 +201,7 @@ def try_creating_prune_generator_task(
|
||||
# set this only after all tasks have been added
|
||||
r.set(rcp.fence_key, 1)
|
||||
except Exception:
|
||||
task_logger.exception(f"Unexpected exception: cc_pair={cc_pair.id}")
|
||||
task_logger.exception("Unexpected exception")
|
||||
return None
|
||||
finally:
|
||||
if lock.owned():
|
||||
@@ -300,7 +300,7 @@ def connector_pruning_generator_task(
|
||||
rcp.documents_to_prune = set(doc_ids_to_remove)
|
||||
|
||||
task_logger.info(
|
||||
f"RedisConnectorPruning.generate_tasks starting. cc_pair={cc_pair.id}"
|
||||
f"RedisConnectorPruning.generate_tasks starting. cc_pair_id={cc_pair_id}"
|
||||
)
|
||||
tasks_generated = rcp.generate_tasks(
|
||||
self.app, db_session, r, None, tenant_id
|
||||
@@ -310,7 +310,7 @@ def connector_pruning_generator_task(
|
||||
|
||||
task_logger.info(
|
||||
f"RedisConnectorPruning.generate_tasks finished. "
|
||||
f"cc_pair={cc_pair.id} tasks_generated={tasks_generated}"
|
||||
f"cc_pair={cc_pair_id} tasks_generated={tasks_generated}"
|
||||
)
|
||||
|
||||
r.set(rcp.generator_complete_key, tasks_generated)
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import httpx
|
||||
from tenacity import retry
|
||||
from tenacity import retry_if_exception_type
|
||||
from tenacity import stop_after_delay
|
||||
from tenacity import wait_random_exponential
|
||||
|
||||
from danswer.document_index.interfaces import DocumentIndex
|
||||
from danswer.document_index.interfaces import VespaDocumentFields
|
||||
|
||||
|
||||
class RetryDocumentIndex:
|
||||
"""A wrapper class to help with specific retries against Vespa involving
|
||||
read timeouts.
|
||||
|
||||
wait_random_exponential implements full jitter as per this article:
|
||||
https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/"""
|
||||
|
||||
MAX_WAIT = 30
|
||||
|
||||
# STOP_AFTER + MAX_WAIT should be slightly less (5?) than the celery soft_time_limit
|
||||
STOP_AFTER = 70
|
||||
|
||||
def __init__(self, index: DocumentIndex):
|
||||
self.index: DocumentIndex = index
|
||||
|
||||
@retry(
|
||||
retry=retry_if_exception_type(httpx.ReadTimeout),
|
||||
wait=wait_random_exponential(multiplier=1, max=MAX_WAIT),
|
||||
stop=stop_after_delay(STOP_AFTER),
|
||||
)
|
||||
def delete_single(self, doc_id: str) -> int:
|
||||
return self.index.delete_single(doc_id)
|
||||
|
||||
@retry(
|
||||
retry=retry_if_exception_type(httpx.ReadTimeout),
|
||||
wait=wait_random_exponential(multiplier=1, max=MAX_WAIT),
|
||||
stop=stop_after_delay(STOP_AFTER),
|
||||
)
|
||||
def update_single(self, doc_id: str, fields: VespaDocumentFields) -> int:
|
||||
return self.index.update_single(doc_id, fields)
|
||||
@@ -1,14 +1,9 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
from celery import shared_task
|
||||
from celery import Task
|
||||
from celery.exceptions import SoftTimeLimitExceeded
|
||||
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.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
|
||||
@@ -25,17 +20,12 @@ from danswer.server.documents.models import ConnectorCredentialPairIdentifier
|
||||
DOCUMENT_BY_CC_PAIR_CLEANUP_MAX_RETRIES = 3
|
||||
|
||||
|
||||
# 5 seconds more than RetryDocumentIndex STOP_AFTER+MAX_WAIT
|
||||
LIGHT_SOFT_TIME_LIMIT = 105
|
||||
LIGHT_TIME_LIMIT = LIGHT_SOFT_TIME_LIMIT + 15
|
||||
|
||||
|
||||
@shared_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,
|
||||
bind=True,
|
||||
soft_time_limit=45,
|
||||
time_limit=60,
|
||||
max_retries=DOCUMENT_BY_CC_PAIR_CLEANUP_MAX_RETRIES,
|
||||
)
|
||||
def document_by_cc_pair_cleanup_task(
|
||||
self: Task,
|
||||
@@ -59,7 +49,7 @@ def document_by_cc_pair_cleanup_task(
|
||||
connector / credential pair from the access list
|
||||
(6) delete all relevant entries from postgres
|
||||
"""
|
||||
task_logger.info(f"tenant={tenant_id} doc={document_id}")
|
||||
task_logger.info(f"tenant_id={tenant_id} document_id={document_id}")
|
||||
|
||||
try:
|
||||
with get_session_with_tenant(tenant_id) as db_session:
|
||||
@@ -67,19 +57,17 @@ def document_by_cc_pair_cleanup_task(
|
||||
chunks_affected = 0
|
||||
|
||||
curr_ind_name, sec_ind_name = get_both_index_names(db_session)
|
||||
doc_index = get_default_document_index(
|
||||
document_index = get_default_document_index(
|
||||
primary_index_name=curr_ind_name, secondary_index_name=sec_ind_name
|
||||
)
|
||||
|
||||
retry_index = RetryDocumentIndex(doc_index)
|
||||
|
||||
count = get_document_connector_count(db_session, document_id)
|
||||
if count == 1:
|
||||
# count == 1 means this is the only remaining cc_pair reference to the doc
|
||||
# delete it from vespa and the db
|
||||
action = "delete"
|
||||
|
||||
chunks_affected = retry_index.delete_single(document_id)
|
||||
chunks_affected = document_index.delete_single(document_id)
|
||||
delete_documents_complete__no_commit(
|
||||
db_session=db_session,
|
||||
document_ids=[document_id],
|
||||
@@ -109,7 +97,9 @@ def document_by_cc_pair_cleanup_task(
|
||||
)
|
||||
|
||||
# update Vespa. OK if doc doesn't exist. Raises exception otherwise.
|
||||
chunks_affected = retry_index.update_single(document_id, fields=fields)
|
||||
chunks_affected = document_index.update_single(
|
||||
document_id, fields=fields
|
||||
)
|
||||
|
||||
# there are still other cc_pair references to the doc, so just resync to Vespa
|
||||
delete_document_by_connector_credential_pair__no_commit(
|
||||
@@ -128,41 +118,19 @@ def document_by_cc_pair_cleanup_task(
|
||||
db_session.commit()
|
||||
|
||||
task_logger.info(
|
||||
f"tenant={tenant_id} "
|
||||
f"doc={document_id} "
|
||||
f"tenant_id={tenant_id} "
|
||||
f"document_id={document_id} "
|
||||
f"action={action} "
|
||||
f"refcount={count} "
|
||||
f"chunks={chunks_affected}"
|
||||
)
|
||||
except SoftTimeLimitExceeded:
|
||||
task_logger.info(
|
||||
f"SoftTimeLimitExceeded exception. tenant={tenant_id} doc={document_id}"
|
||||
f"SoftTimeLimitExceeded exception. tenant_id={tenant_id} doc_id={document_id}"
|
||||
)
|
||||
return False
|
||||
except Exception as ex:
|
||||
if isinstance(ex, RetryError):
|
||||
task_logger.info(f"Retry failed: {ex.last_attempt.attempt_number}")
|
||||
|
||||
# only set the inner exception if it is of type Exception
|
||||
e_temp = ex.last_attempt.exception()
|
||||
if isinstance(e_temp, Exception):
|
||||
e = e_temp
|
||||
else:
|
||||
e = ex
|
||||
|
||||
if isinstance(e, httpx.HTTPStatusError):
|
||||
if e.response.status_code == HTTPStatus.BAD_REQUEST:
|
||||
task_logger.exception(
|
||||
f"Non-retryable HTTPStatusError: "
|
||||
f"tenant={tenant_id} "
|
||||
f"doc={document_id} "
|
||||
f"status={e.response.status_code}"
|
||||
)
|
||||
return False
|
||||
|
||||
task_logger.exception(
|
||||
f"Unexpected exception: tenant={tenant_id} doc={document_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
task_logger.exception("Unexpected exception")
|
||||
|
||||
if self.request.retries < DOCUMENT_BY_CC_PAIR_CLEANUP_MAX_RETRIES:
|
||||
# Still retrying. Exponential backoff from 2^4 to 2^6 ... i.e. 16, 32, 64
|
||||
@@ -173,7 +141,7 @@ def document_by_cc_pair_cleanup_task(
|
||||
# eventually gets fixed out of band via stale document reconciliation
|
||||
task_logger.info(
|
||||
f"Max retries reached. Marking doc as dirty for reconciliation: "
|
||||
f"tenant={tenant_id} doc={document_id}"
|
||||
f"tenant_id={tenant_id} document_id={document_id}"
|
||||
)
|
||||
with get_session_with_tenant(tenant_id):
|
||||
mark_document_as_modified(document_id, db_session)
|
||||
|
||||
@@ -4,7 +4,6 @@ from datetime import timezone
|
||||
from http import HTTPStatus
|
||||
from typing import cast
|
||||
|
||||
import httpx
|
||||
import redis
|
||||
from celery import Celery
|
||||
from celery import shared_task
|
||||
@@ -14,7 +13,6 @@ from celery.result import AsyncResult
|
||||
from celery.states import READY_STATES
|
||||
from redis import Redis
|
||||
from sqlalchemy.orm import Session
|
||||
from tenacity import RetryError
|
||||
|
||||
from danswer.access.access import get_access_for_document
|
||||
from danswer.background.celery.apps.app_base import task_logger
|
||||
@@ -31,9 +29,6 @@ from danswer.background.celery.tasks.shared.RedisConnectorDeletionFenceData impo
|
||||
from danswer.background.celery.tasks.shared.RedisConnectorIndexingFenceData import (
|
||||
RedisConnectorIndexingFenceData,
|
||||
)
|
||||
from danswer.background.celery.tasks.shared.RetryDocumentIndex import RetryDocumentIndex
|
||||
from danswer.background.celery.tasks.shared.tasks import LIGHT_SOFT_TIME_LIMIT
|
||||
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
|
||||
@@ -157,7 +152,7 @@ def check_for_vespa_sync_task(self: Task, *, tenant_id: str | None) -> None:
|
||||
"Soft time limit exceeded, task is being terminated gracefully."
|
||||
)
|
||||
except Exception:
|
||||
task_logger.exception(f"Unexpected exception: tenant={tenant_id}")
|
||||
task_logger.exception("Unexpected exception")
|
||||
finally:
|
||||
if lock_beat.owned():
|
||||
lock_beat.release()
|
||||
@@ -814,22 +809,22 @@ def monitor_vespa_sync(self: Task, tenant_id: str | None) -> bool:
|
||||
@shared_task(
|
||||
name="vespa_metadata_sync_task",
|
||||
bind=True,
|
||||
soft_time_limit=LIGHT_SOFT_TIME_LIMIT,
|
||||
time_limit=LIGHT_TIME_LIMIT,
|
||||
soft_time_limit=45,
|
||||
time_limit=60,
|
||||
max_retries=3,
|
||||
)
|
||||
def vespa_metadata_sync_task(
|
||||
self: Task, document_id: str, tenant_id: str | None
|
||||
) -> bool:
|
||||
task_logger.info(f"document_id={document_id}")
|
||||
|
||||
try:
|
||||
with get_session_with_tenant(tenant_id) as db_session:
|
||||
curr_ind_name, sec_ind_name = get_both_index_names(db_session)
|
||||
doc_index = get_default_document_index(
|
||||
document_index = get_default_document_index(
|
||||
primary_index_name=curr_ind_name, secondary_index_name=sec_ind_name
|
||||
)
|
||||
|
||||
retry_index = RetryDocumentIndex(doc_index)
|
||||
|
||||
doc = get_document(document_id, db_session)
|
||||
if not doc:
|
||||
return False
|
||||
@@ -851,43 +846,19 @@ def vespa_metadata_sync_task(
|
||||
)
|
||||
|
||||
# update Vespa. OK if doc doesn't exist. Raises exception otherwise.
|
||||
chunks_affected = retry_index.update_single(document_id, fields)
|
||||
chunks_affected = document_index.update_single(document_id, fields=fields)
|
||||
|
||||
# update db last. Worst case = we crash right before this and
|
||||
# the sync might repeat again later
|
||||
mark_document_as_synced(document_id, db_session)
|
||||
|
||||
task_logger.info(
|
||||
f"tenant={tenant_id} doc={document_id} action=sync chunks={chunks_affected}"
|
||||
f"document_id={document_id} action=sync chunks={chunks_affected}"
|
||||
)
|
||||
except SoftTimeLimitExceeded:
|
||||
task_logger.info(
|
||||
f"SoftTimeLimitExceeded exception. tenant={tenant_id} doc={document_id}"
|
||||
)
|
||||
except Exception as ex:
|
||||
if isinstance(ex, RetryError):
|
||||
task_logger.warning(f"Retry failed: {ex.last_attempt.attempt_number}")
|
||||
|
||||
# only set the inner exception if it is of type Exception
|
||||
e_temp = ex.last_attempt.exception()
|
||||
if isinstance(e_temp, Exception):
|
||||
e = e_temp
|
||||
else:
|
||||
e = ex
|
||||
|
||||
if isinstance(e, httpx.HTTPStatusError):
|
||||
if e.response.status_code == HTTPStatus.BAD_REQUEST:
|
||||
task_logger.exception(
|
||||
f"Non-retryable HTTPStatusError: "
|
||||
f"tenant={tenant_id} "
|
||||
f"doc={document_id} "
|
||||
f"status={e.response.status_code}"
|
||||
)
|
||||
return False
|
||||
|
||||
task_logger.exception(
|
||||
f"Unexpected exception: tenant={tenant_id} doc={document_id}"
|
||||
)
|
||||
task_logger.info(f"SoftTimeLimitExceeded exception. doc_id={document_id}")
|
||||
except Exception as e:
|
||||
task_logger.exception("Unexpected exception")
|
||||
|
||||
# Exponential backoff from 2^4 to 2^6 ... i.e. 16, 32, 64
|
||||
countdown = 2 ** (self.request.retries + 4)
|
||||
|
||||
@@ -41,19 +41,6 @@ personas:
|
||||
icon_color: "#6FB1FF"
|
||||
display_priority: 1
|
||||
is_visible: true
|
||||
starter_messages:
|
||||
- name: "General Information"
|
||||
description: "Ask about available information"
|
||||
message: "Hello! I'm interested in learning more about the information available here. Could you give me an overview of the types of data or documents that might be accessible?"
|
||||
- name: "Specific Topic Search"
|
||||
description: "Search for specific information"
|
||||
message: "Hi! I'd like to learn more about a specific topic. Could you help me find relevant documents and information?"
|
||||
- name: "Recent Updates"
|
||||
description: "Inquire about latest additions"
|
||||
message: "Hello! I'm curious about any recent updates or additions to the knowledge base. Can you tell me what new information has been added lately?"
|
||||
- name: "Cross-referencing Information"
|
||||
description: "Connect information from different sources"
|
||||
message: "Hi! I'm working on a project that requires connecting information from multiple sources. How can I effectively cross-reference data across different documents or categories?"
|
||||
|
||||
- id: 1
|
||||
name: "General"
|
||||
@@ -70,19 +57,6 @@ personas:
|
||||
icon_color: "#FF6F6F"
|
||||
display_priority: 0
|
||||
is_visible: true
|
||||
starter_messages:
|
||||
- name: "Open Discussion"
|
||||
description: "Start an open-ended conversation"
|
||||
message: "Hi! Can you help me write a professional email?"
|
||||
- name: "Problem Solving"
|
||||
description: "Get help with a challenge"
|
||||
message: "Hello! I need help managing my daily tasks better. Do you have any simple tips?"
|
||||
- name: "Learn Something New"
|
||||
description: "Explore a new topic"
|
||||
message: "Hi! Could you explain what project management is in simple terms?"
|
||||
- name: "Creative Brainstorming"
|
||||
description: "Generate creative ideas"
|
||||
message: "Hello! I need to brainstorm some team building activities. Do you have any fun suggestions?"
|
||||
|
||||
- id: 2
|
||||
name: "Paraphrase"
|
||||
@@ -99,19 +73,7 @@ personas:
|
||||
icon_color: "#6FFF8D"
|
||||
display_priority: 2
|
||||
is_visible: false
|
||||
starter_messages:
|
||||
- name: "Document Search"
|
||||
description: "Find exact information"
|
||||
message: "Hi! Could you help me find information about our team structure and reporting lines from our internal documents?"
|
||||
- name: "Process Verification"
|
||||
description: "Find exact quotes"
|
||||
message: "Hello! I need to understand our project approval process. Could you find the exact steps from our documentation?"
|
||||
- name: "Technical Documentation"
|
||||
description: "Search technical details"
|
||||
message: "Hi there! I'm looking for information about our deployment procedures. Can you find the specific steps from our technical guides?"
|
||||
- name: "Policy Reference"
|
||||
description: "Check official policies"
|
||||
message: "Hello! Could you help me find our official guidelines about client communication? I need the exact wording from our documentation."
|
||||
|
||||
|
||||
- id: 3
|
||||
name: "Art"
|
||||
@@ -124,21 +86,8 @@ personas:
|
||||
llm_filter_extraction: false
|
||||
recency_bias: "no_decay"
|
||||
document_sets: []
|
||||
icon_shape: 234124
|
||||
icon_shape: 234124
|
||||
icon_color: "#9B59B6"
|
||||
image_generation: true
|
||||
image_generation: true
|
||||
display_priority: 3
|
||||
is_visible: true
|
||||
starter_messages:
|
||||
- name: "Landscape"
|
||||
description: "Generate a landscape image"
|
||||
message: "Create an image of a serene mountain lake at sunset, with snow-capped peaks reflected in the calm water and a small wooden cabin on the shore."
|
||||
- name: "Character"
|
||||
description: "Generate a character image"
|
||||
message: "Generate an image of a futuristic robot with glowing blue eyes, sleek metallic body, and intricate circuitry visible through transparent panels on its chest and arms."
|
||||
- name: "Abstract"
|
||||
description: "Create an abstract image"
|
||||
message: "Create an abstract image representing the concept of time, using swirling clock hands, fragmented hourglasses, and streaks of light to convey the passage of moments and eras."
|
||||
- name: "Urban Scene"
|
||||
description: "Generate an urban landscape"
|
||||
message: "Generate an image of a bustling futuristic cityscape at night, with towering skyscrapers, flying vehicles, holographic advertisements, and a mix of neon and bioluminescent lighting."
|
||||
|
||||
@@ -437,7 +437,7 @@ CUSTOM_ANSWER_VALIDITY_CONDITIONS = json.loads(
|
||||
os.environ.get("CUSTOM_ANSWER_VALIDITY_CONDITIONS", "[]")
|
||||
)
|
||||
|
||||
VESPA_REQUEST_TIMEOUT = int(os.environ.get("VESPA_REQUEST_TIMEOUT") or "15")
|
||||
VESPA_REQUEST_TIMEOUT = int(os.environ.get("VESPA_REQUEST_TIMEOUT") or "5")
|
||||
|
||||
SYSTEM_RECURSION_LIMIT = int(os.environ.get("SYSTEM_RECURSION_LIMIT") or "1000")
|
||||
|
||||
@@ -461,12 +461,20 @@ AZURE_DALLE_API_BASE = os.environ.get("AZURE_DALLE_API_BASE")
|
||||
AZURE_DALLE_DEPLOYMENT_NAME = os.environ.get("AZURE_DALLE_DEPLOYMENT_NAME")
|
||||
|
||||
|
||||
# Cloud configuration
|
||||
|
||||
# Multi-tenancy configuration
|
||||
MULTI_TENANT = os.environ.get("MULTI_TENANT", "").lower() == "true"
|
||||
|
||||
# Use managed Vespa (Vespa Cloud). If set, must also set VESPA_CLOUD_URL, VESPA_CLOUD_CERT_PATH and VESPA_CLOUD_KEY_PATH
|
||||
MANAGED_VESPA = os.environ.get("MANAGED_VESPA", "").lower() == "true"
|
||||
|
||||
ENABLE_EMAIL_INVITES = os.environ.get("ENABLE_EMAIL_INVITES", "").lower() == "true"
|
||||
|
||||
# Security and authentication
|
||||
SECRET_JWT_KEY = os.environ.get(
|
||||
"SECRET_JWT_KEY", ""
|
||||
) # Used for encryption of the JWT token for user's tenant context
|
||||
DATA_PLANE_SECRET = os.environ.get(
|
||||
"DATA_PLANE_SECRET", ""
|
||||
) # Used for secure communication between the control and data plane
|
||||
|
||||
@@ -31,6 +31,9 @@ DISABLED_GEN_AI_MSG = (
|
||||
"You can still use Danswer as a search engine."
|
||||
)
|
||||
|
||||
# Prefix used for all tenant ids
|
||||
TENANT_ID_PREFIX = "tenant_"
|
||||
|
||||
# Postgres connection constants for application_name
|
||||
POSTGRES_WEB_APP_NAME = "web"
|
||||
POSTGRES_INDEXER_APP_NAME = "indexer"
|
||||
|
||||
@@ -44,6 +44,8 @@ class BookstackConnector(LoadConnector, PollConnector):
|
||||
start: SecondsSinceUnixEpoch | None = None,
|
||||
end: SecondsSinceUnixEpoch | None = None,
|
||||
) -> tuple[list[Document], int]:
|
||||
doc_batch: list[Document] = []
|
||||
|
||||
params = {
|
||||
"count": str(batch_size),
|
||||
"offset": str(start_ind),
|
||||
@@ -61,7 +63,8 @@ class BookstackConnector(LoadConnector, PollConnector):
|
||||
)
|
||||
|
||||
batch = bookstack_client.get(endpoint, params=params).get("data", [])
|
||||
doc_batch = [transformer(bookstack_client, item) for item in batch]
|
||||
for item in batch:
|
||||
doc_batch.append(transformer(bookstack_client, item))
|
||||
|
||||
return doc_batch, len(batch)
|
||||
|
||||
|
||||
@@ -210,7 +210,6 @@ if __name__ == "__main__":
|
||||
"clickup_team_id": os.environ["clickup_team_id"],
|
||||
}
|
||||
)
|
||||
|
||||
latest_docs = clickup_connector.load_from_state()
|
||||
|
||||
for doc in latest_docs:
|
||||
|
||||
@@ -11,10 +11,6 @@ from danswer.connectors.models import BasicExpertInfo
|
||||
from danswer.utils.text_processing import is_valid_email
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
|
||||
def datetime_to_utc(dt: datetime) -> datetime:
|
||||
if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
@@ -53,6 +49,10 @@ def get_experts_stores_representations(
|
||||
return [owner for owner in reps if owner is not None]
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
|
||||
def process_in_batches(
|
||||
objects: list[T], process_function: Callable[[T], U], batch_size: int
|
||||
) -> Iterator[list[U]]:
|
||||
|
||||
@@ -34,6 +34,7 @@ from danswer.connectors.mediawiki.wiki import MediaWikiConnector
|
||||
from danswer.connectors.models import InputType
|
||||
from danswer.connectors.notion.connector import NotionConnector
|
||||
from danswer.connectors.productboard.connector import ProductboardConnector
|
||||
from danswer.connectors.requesttracker.connector import RequestTrackerConnector
|
||||
from danswer.connectors.salesforce.connector import SalesforceConnector
|
||||
from danswer.connectors.sharepoint.connector import SharepointConnector
|
||||
from danswer.connectors.slab.connector import SlabConnector
|
||||
@@ -76,6 +77,7 @@ def identify_connector_class(
|
||||
DocumentSource.SLAB: SlabConnector,
|
||||
DocumentSource.NOTION: NotionConnector,
|
||||
DocumentSource.ZULIP: ZulipConnector,
|
||||
DocumentSource.REQUESTTRACKER: RequestTrackerConnector,
|
||||
DocumentSource.GURU: GuruConnector,
|
||||
DocumentSource.LINEAR: LinearConnector,
|
||||
DocumentSource.HUBSPOT: HubSpotConnector,
|
||||
|
||||
@@ -24,9 +24,6 @@ from danswer.connectors.models import Document
|
||||
from danswer.connectors.models import Section
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
# List of directories/Files to exclude
|
||||
exclude_patterns = [
|
||||
"logs",
|
||||
@@ -34,6 +31,7 @@ exclude_patterns = [
|
||||
".gitlab/",
|
||||
".pre-commit-config.yaml",
|
||||
]
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def _batch_gitlab_objects(
|
||||
|
||||
@@ -19,14 +19,13 @@ from danswer.connectors.models import Section
|
||||
from danswer.file_processing.html_utils import parse_html_page_basic
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
# Potential Improvements
|
||||
# 1. Support fetching per collection via collection token (configured at connector creation)
|
||||
|
||||
GURU_API_BASE = "https://api.getguru.com/api/v1/"
|
||||
GURU_QUERY_ENDPOINT = GURU_API_BASE + "search/query"
|
||||
GURU_CARDS_URL = "https://app.getguru.com/card/"
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def unixtime_to_guru_time_str(unix_time: SecondsSinceUnixEpoch) -> str:
|
||||
|
||||
@@ -18,7 +18,6 @@ from danswer.connectors.models import Document
|
||||
from danswer.connectors.models import Section
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
_NUM_RETRIES = 5
|
||||
|
||||
@@ -22,7 +22,6 @@ from danswer.connectors.models import Document
|
||||
from danswer.connectors.models import Section
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
@@ -231,7 +230,5 @@ if __name__ == "__main__":
|
||||
print("All docs", all_docs)
|
||||
current = datetime.datetime.now().timestamp()
|
||||
one_day_ago = current - 30 * 24 * 60 * 60 # 30 days
|
||||
|
||||
latest_docs = list(test_connector.poll_source(one_day_ago, current))
|
||||
|
||||
print("Latest docs", latest_docs)
|
||||
|
||||
@@ -134,14 +134,9 @@ class NotionConnector(LoadConnector, PollConnector):
|
||||
f"This is likely due to the block not being shared "
|
||||
f"with the Danswer integration. Exact exception:\n\n{e}"
|
||||
)
|
||||
else:
|
||||
logger.exception(
|
||||
f"Error fetching blocks with status code {res.status_code}: {res.json()}"
|
||||
)
|
||||
|
||||
# This can occasionally happen, the reason is unknown and cannot be reproduced on our internal Notion
|
||||
# Assuming this will not be a critical loss of data
|
||||
return None
|
||||
return None
|
||||
logger.exception(f"Error fetching blocks - {res.json()}")
|
||||
raise e
|
||||
return res.json()
|
||||
|
||||
@retry(tries=3, delay=1, backoff=2)
|
||||
|
||||
@@ -1,124 +1,153 @@
|
||||
# from datetime import datetime
|
||||
# from datetime import timezone
|
||||
# from logging import DEBUG as LOG_LVL_DEBUG
|
||||
# from typing import Any
|
||||
# from typing import List
|
||||
# from typing import Optional
|
||||
# from rt.rest1 import ALL_QUEUES
|
||||
# from rt.rest1 import Rt
|
||||
# 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 PollConnector
|
||||
# from danswer.connectors.interfaces import SecondsSinceUnixEpoch
|
||||
# from danswer.connectors.models import ConnectorMissingCredentialError
|
||||
# from danswer.connectors.models import Document
|
||||
# from danswer.connectors.models import Section
|
||||
# from danswer.utils.logger import setup_logger
|
||||
# logger = setup_logger()
|
||||
# class RequestTrackerError(Exception):
|
||||
# pass
|
||||
# class RequestTrackerConnector(PollConnector):
|
||||
# def __init__(
|
||||
# self,
|
||||
# batch_size: int = INDEX_BATCH_SIZE,
|
||||
# ) -> None:
|
||||
# self.batch_size = batch_size
|
||||
# def txn_link(self, tid: int, txn: int) -> str:
|
||||
# return f"{self.rt_base_url}/Ticket/Display.html?id={tid}&txn={txn}"
|
||||
# def build_doc_sections_from_txn(
|
||||
# self, connection: Rt, ticket_id: int
|
||||
# ) -> List[Section]:
|
||||
# Sections: List[Section] = []
|
||||
# get_history_resp = connection.get_history(ticket_id)
|
||||
# if get_history_resp is None:
|
||||
# raise RequestTrackerError(f"Ticket {ticket_id} cannot be found")
|
||||
# for tx in get_history_resp:
|
||||
# Sections.append(
|
||||
# Section(
|
||||
# link=self.txn_link(ticket_id, int(tx["id"])),
|
||||
# text="\n".join(
|
||||
# [
|
||||
# f"{k}:\n{v}\n" if k != "Attachments" else ""
|
||||
# for (k, v) in tx.items()
|
||||
# ]
|
||||
# ),
|
||||
# )
|
||||
# )
|
||||
# return Sections
|
||||
# def load_credentials(self, credentials: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||
# self.rt_username = credentials.get("requesttracker_username")
|
||||
# self.rt_password = credentials.get("requesttracker_password")
|
||||
# self.rt_base_url = credentials.get("requesttracker_base_url")
|
||||
# return None
|
||||
# # This does not include RT file attachments yet.
|
||||
# def _process_tickets(
|
||||
# self, start: datetime, end: datetime
|
||||
# ) -> GenerateDocumentsOutput:
|
||||
# if any([self.rt_username, self.rt_password, self.rt_base_url]) is None:
|
||||
# raise ConnectorMissingCredentialError("requesttracker")
|
||||
# Rt0 = Rt(
|
||||
# f"{self.rt_base_url}/REST/1.0/",
|
||||
# self.rt_username,
|
||||
# self.rt_password,
|
||||
# )
|
||||
# Rt0.login()
|
||||
# d0 = start.strftime("%Y-%m-%d %H:%M:%S")
|
||||
# d1 = end.strftime("%Y-%m-%d %H:%M:%S")
|
||||
# tickets = Rt0.search(
|
||||
# Queue=ALL_QUEUES,
|
||||
# raw_query=f"Updated > '{d0}' AND Updated < '{d1}'",
|
||||
# )
|
||||
# doc_batch: List[Document] = []
|
||||
# for ticket in tickets:
|
||||
# ticket_keys_to_omit = ["id", "Subject"]
|
||||
# tid: int = int(ticket["numerical_id"])
|
||||
# ticketLink: str = f"{self.rt_base_url}/Ticket/Display.html?id={tid}"
|
||||
# logger.info(f"Processing ticket {tid}")
|
||||
# doc = Document(
|
||||
# id=ticket["id"],
|
||||
# # Will add title to the first section later in processing
|
||||
# sections=[Section(link=ticketLink, text="")]
|
||||
# + self.build_doc_sections_from_txn(Rt0, tid),
|
||||
# source=DocumentSource.REQUESTTRACKER,
|
||||
# semantic_identifier=ticket["Subject"],
|
||||
# metadata={
|
||||
# key: value
|
||||
# for key, value in ticket.items()
|
||||
# if key not in ticket_keys_to_omit
|
||||
# },
|
||||
# )
|
||||
# doc_batch.append(doc)
|
||||
# if len(doc_batch) >= self.batch_size:
|
||||
# yield doc_batch
|
||||
# doc_batch = []
|
||||
# if doc_batch:
|
||||
# yield doc_batch
|
||||
# def poll_source(
|
||||
# self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
|
||||
# ) -> GenerateDocumentsOutput:
|
||||
# # Keep query short, only look behind 1 day at maximum
|
||||
# one_day_ago: float = end - (24 * 60 * 60)
|
||||
# _start: float = start if start > one_day_ago else one_day_ago
|
||||
# start_datetime = datetime.fromtimestamp(_start, tz=timezone.utc)
|
||||
# end_datetime = datetime.fromtimestamp(end, tz=timezone.utc)
|
||||
# yield from self._process_tickets(start_datetime, end_datetime)
|
||||
# if __name__ == "__main__":
|
||||
# import time
|
||||
# import os
|
||||
# from dotenv import load_dotenv
|
||||
# load_dotenv()
|
||||
# logger.setLevel(LOG_LVL_DEBUG)
|
||||
# rt_connector = RequestTrackerConnector()
|
||||
# rt_connector.load_credentials(
|
||||
# {
|
||||
# "requesttracker_username": os.getenv("RT_USERNAME"),
|
||||
# "requesttracker_password": os.getenv("RT_PASSWORD"),
|
||||
# "requesttracker_base_url": os.getenv("RT_BASE_URL"),
|
||||
# }
|
||||
# )
|
||||
# current = time.time()
|
||||
# one_day_ago = current - (24 * 60 * 60) # 1 days
|
||||
# latest_docs = rt_connector.poll_source(one_day_ago, current)
|
||||
# for doc in latest_docs:
|
||||
# print(doc)
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from logging import DEBUG as LOG_LVL_DEBUG
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from rt.rest1 import ALL_QUEUES
|
||||
from rt.rest1 import Rt
|
||||
|
||||
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 PollConnector
|
||||
from danswer.connectors.interfaces import SecondsSinceUnixEpoch
|
||||
from danswer.connectors.models import ConnectorMissingCredentialError
|
||||
from danswer.connectors.models import Document
|
||||
from danswer.connectors.models import Section
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
class RequestTrackerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RequestTrackerConnector(PollConnector):
|
||||
def __init__(
|
||||
self,
|
||||
batch_size: int = INDEX_BATCH_SIZE,
|
||||
) -> None:
|
||||
self.batch_size = batch_size
|
||||
|
||||
def txn_link(self, tid: int, txn: int) -> str:
|
||||
return f"{self.rt_base_url}/Ticket/Display.html?id={tid}&txn={txn}"
|
||||
|
||||
def build_doc_sections_from_txn(
|
||||
self, connection: Rt, ticket_id: int
|
||||
) -> List[Section]:
|
||||
Sections: List[Section] = []
|
||||
|
||||
get_history_resp = connection.get_history(ticket_id)
|
||||
|
||||
if get_history_resp is None:
|
||||
raise RequestTrackerError(f"Ticket {ticket_id} cannot be found")
|
||||
|
||||
for tx in get_history_resp:
|
||||
Sections.append(
|
||||
Section(
|
||||
link=self.txn_link(ticket_id, int(tx["id"])),
|
||||
text="\n".join(
|
||||
[
|
||||
f"{k}:\n{v}\n" if k != "Attachments" else ""
|
||||
for (k, v) in tx.items()
|
||||
]
|
||||
),
|
||||
)
|
||||
)
|
||||
return Sections
|
||||
|
||||
def load_credentials(self, credentials: dict[str, Any]) -> Optional[dict[str, Any]]:
|
||||
self.rt_username = credentials.get("requesttracker_username")
|
||||
self.rt_password = credentials.get("requesttracker_password")
|
||||
self.rt_base_url = credentials.get("requesttracker_base_url")
|
||||
return None
|
||||
|
||||
# This does not include RT file attachments yet.
|
||||
def _process_tickets(
|
||||
self, start: datetime, end: datetime
|
||||
) -> GenerateDocumentsOutput:
|
||||
if any([self.rt_username, self.rt_password, self.rt_base_url]) is None:
|
||||
raise ConnectorMissingCredentialError("requesttracker")
|
||||
|
||||
Rt0 = Rt(
|
||||
f"{self.rt_base_url}/REST/1.0/",
|
||||
self.rt_username,
|
||||
self.rt_password,
|
||||
)
|
||||
|
||||
Rt0.login()
|
||||
|
||||
d0 = start.strftime("%Y-%m-%d %H:%M:%S")
|
||||
d1 = end.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
tickets = Rt0.search(
|
||||
Queue=ALL_QUEUES,
|
||||
raw_query=f"Updated > '{d0}' AND Updated < '{d1}'",
|
||||
)
|
||||
|
||||
doc_batch: List[Document] = []
|
||||
|
||||
for ticket in tickets:
|
||||
ticket_keys_to_omit = ["id", "Subject"]
|
||||
tid: int = int(ticket["numerical_id"])
|
||||
ticketLink: str = f"{self.rt_base_url}/Ticket/Display.html?id={tid}"
|
||||
logger.info(f"Processing ticket {tid}")
|
||||
doc = Document(
|
||||
id=ticket["id"],
|
||||
# Will add title to the first section later in processing
|
||||
sections=[Section(link=ticketLink, text="")]
|
||||
+ self.build_doc_sections_from_txn(Rt0, tid),
|
||||
source=DocumentSource.REQUESTTRACKER,
|
||||
semantic_identifier=ticket["Subject"],
|
||||
metadata={
|
||||
key: value
|
||||
for key, value in ticket.items()
|
||||
if key not in ticket_keys_to_omit
|
||||
},
|
||||
)
|
||||
|
||||
doc_batch.append(doc)
|
||||
|
||||
if len(doc_batch) >= self.batch_size:
|
||||
yield doc_batch
|
||||
doc_batch = []
|
||||
|
||||
if doc_batch:
|
||||
yield doc_batch
|
||||
|
||||
def poll_source(
|
||||
self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch
|
||||
) -> GenerateDocumentsOutput:
|
||||
# Keep query short, only look behind 1 day at maximum
|
||||
one_day_ago: float = end - (24 * 60 * 60)
|
||||
_start: float = start if start > one_day_ago else one_day_ago
|
||||
start_datetime = datetime.fromtimestamp(_start, tz=timezone.utc)
|
||||
end_datetime = datetime.fromtimestamp(end, tz=timezone.utc)
|
||||
yield from self._process_tickets(start_datetime, end_datetime)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import time
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
logger.setLevel(LOG_LVL_DEBUG)
|
||||
rt_connector = RequestTrackerConnector()
|
||||
rt_connector.load_credentials(
|
||||
{
|
||||
"requesttracker_username": os.getenv("RT_USERNAME"),
|
||||
"requesttracker_password": os.getenv("RT_PASSWORD"),
|
||||
"requesttracker_base_url": os.getenv("RT_BASE_URL"),
|
||||
}
|
||||
)
|
||||
|
||||
current = time.time()
|
||||
one_day_ago = current - (24 * 60 * 60) # 1 days
|
||||
latest_docs = rt_connector.poll_source(one_day_ago, current)
|
||||
|
||||
for doc in latest_docs:
|
||||
print(doc)
|
||||
|
||||
@@ -25,7 +25,6 @@ from danswer.connectors.models import Section
|
||||
from danswer.file_processing.extract_file_text import extract_file_text
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
|
||||
@@ -20,13 +20,10 @@ from danswer.connectors.models import Document
|
||||
from danswer.connectors.models import Section
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
# Fairly generous retry because it's not understood why occasionally GraphQL requests fail even with timeout > 1 min
|
||||
SLAB_GRAPHQL_MAX_TRIES = 10
|
||||
SLAB_API_URL = "https://api.slab.com/v1/graphql"
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def run_graphql_request(
|
||||
|
||||
@@ -441,7 +441,6 @@ if __name__ == "__main__":
|
||||
|
||||
current = time.time()
|
||||
one_day_ago = current - 24 * 60 * 60 # 1 day
|
||||
|
||||
document_batches = connector.poll_source(one_day_ago, current)
|
||||
|
||||
print(next(document_batches))
|
||||
|
||||
@@ -16,7 +16,6 @@ from danswer.connectors.slack.connector import filter_channels
|
||||
from danswer.connectors.slack.utils import get_message_link
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
|
||||
@@ -57,10 +57,7 @@ async def get_user_count() -> int:
|
||||
|
||||
# Need to override this because FastAPI Users doesn't give flexibility for backend field creation logic in OAuth flow
|
||||
class SQLAlchemyUserAdminDB(SQLAlchemyUserDatabase):
|
||||
async def create(
|
||||
self,
|
||||
create_dict: Dict[str, Any],
|
||||
) -> UP:
|
||||
async def create(self, create_dict: Dict[str, Any]) -> UP:
|
||||
user_count = await get_user_count()
|
||||
if user_count == 0 or create_dict["email"] in get_default_admin_user_emails():
|
||||
create_dict["role"] = UserRole.ADMIN
|
||||
|
||||
@@ -25,6 +25,7 @@ from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from danswer.configs.app_configs import LOG_POSTGRES_CONN_COUNTS
|
||||
from danswer.configs.app_configs import LOG_POSTGRES_LATENCY
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.app_configs import POSTGRES_API_SERVER_POOL_OVERFLOW
|
||||
from danswer.configs.app_configs import POSTGRES_API_SERVER_POOL_SIZE
|
||||
from danswer.configs.app_configs import POSTGRES_DB
|
||||
@@ -34,13 +35,12 @@ from danswer.configs.app_configs import POSTGRES_POOL_PRE_PING
|
||||
from danswer.configs.app_configs import POSTGRES_POOL_RECYCLE
|
||||
from danswer.configs.app_configs import POSTGRES_PORT
|
||||
from danswer.configs.app_configs import POSTGRES_USER
|
||||
from danswer.configs.app_configs import USER_AUTH_SECRET
|
||||
from danswer.configs.app_configs import SECRET_JWT_KEY
|
||||
from danswer.configs.constants import POSTGRES_UNKNOWN_APP_NAME
|
||||
from danswer.configs.constants import TENANT_ID_PREFIX
|
||||
from danswer.utils.logger import setup_logger
|
||||
from shared_configs.configs import CURRENT_TENANT_ID_CONTEXTVAR
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
from shared_configs.configs import POSTGRES_DEFAULT_SCHEMA
|
||||
from shared_configs.configs import TENANT_ID_PREFIX
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
@@ -263,20 +263,17 @@ def get_current_tenant_id(request: Request) -> str:
|
||||
CURRENT_TENANT_ID_CONTEXTVAR.set(tenant_id)
|
||||
return tenant_id
|
||||
|
||||
token = request.cookies.get("fastapiusersauth")
|
||||
token = request.cookies.get("tenant_details")
|
||||
if not token:
|
||||
current_value = CURRENT_TENANT_ID_CONTEXTVAR.get()
|
||||
# If no token is present, use the default schema or handle accordingly
|
||||
return current_value
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
USER_AUTH_SECRET,
|
||||
audience=["fastapi-users:auth"],
|
||||
algorithms=["HS256"],
|
||||
)
|
||||
tenant_id = payload.get("tenant_id", POSTGRES_DEFAULT_SCHEMA)
|
||||
payload = jwt.decode(token, SECRET_JWT_KEY, algorithms=["HS256"])
|
||||
tenant_id = payload.get("tenant_id")
|
||||
if not tenant_id:
|
||||
return CURRENT_TENANT_ID_CONTEXTVAR.get()
|
||||
if not is_valid_schema_name(tenant_id):
|
||||
raise HTTPException(status_code=400, detail="Invalid tenant ID format")
|
||||
CURRENT_TENANT_ID_CONTEXTVAR.set(tenant_id)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.constants import KV_REINDEX_KEY
|
||||
from danswer.db.connector_credential_pair import get_connector_credential_pairs
|
||||
from danswer.db.connector_credential_pair import resync_cc_pair
|
||||
@@ -14,7 +15,6 @@ from danswer.db.search_settings import get_secondary_search_settings
|
||||
from danswer.db.search_settings import update_search_settings_status
|
||||
from danswer.key_value_store.factory import get_kv_store
|
||||
from danswer.utils.logger import setup_logger
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.db.search_settings import get_current_search_settings
|
||||
from danswer.document_index.interfaces import DocumentIndex
|
||||
from danswer.document_index.vespa.index import VespaIndex
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
|
||||
def get_default_document_index(
|
||||
|
||||
@@ -17,6 +17,7 @@ import httpx # type: ignore
|
||||
import requests # type: ignore
|
||||
|
||||
from danswer.configs.app_configs import DOCUMENT_INDEX_NAME
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.chat_configs import DOC_TIME_DECAY
|
||||
from danswer.configs.chat_configs import NUM_RETURNED_HITS
|
||||
from danswer.configs.chat_configs import TITLE_CONTENT_RATIO
|
||||
@@ -72,7 +73,6 @@ from danswer.search.models import IndexFilters
|
||||
from danswer.search.models import InferenceChunkUncleaned
|
||||
from danswer.utils.batching import batch_generator
|
||||
from danswer.utils.logger import setup_logger
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
from shared_configs.model_server_models import Embedding
|
||||
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ def _does_document_exist(
|
||||
chunk. This checks for whether the chunk exists already in the index"""
|
||||
doc_url = f"{DOCUMENT_ID_ENDPOINT.format(index_name=index_name)}/{doc_chunk_id}"
|
||||
doc_fetch_response = http_client.get(doc_url)
|
||||
|
||||
if doc_fetch_response.status_code == 404:
|
||||
return False
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ VESPA_APPLICATION_ENDPOINT = f"{VESPA_CONFIG_SERVER_URL}/application/v2"
|
||||
# main search application
|
||||
VESPA_APP_CONTAINER_URL = VESPA_CLOUD_URL or f"http://{VESPA_HOST}:{VESPA_PORT}"
|
||||
|
||||
|
||||
# danswer_chunk below is defined in vespa/app_configs/schemas/danswer_chunk.sd
|
||||
DOCUMENT_ID_ENDPOINT = (
|
||||
f"{VESPA_APP_CONTAINER_URL}/document/v1/default/{{index_name}}/docid"
|
||||
|
||||
@@ -8,6 +8,7 @@ from redis.client import Redis
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.db.engine import get_sqlalchemy_engine
|
||||
from danswer.db.engine import is_valid_schema_name
|
||||
from danswer.db.models import KVStore
|
||||
@@ -17,7 +18,6 @@ from danswer.key_value_store.interface import KvKeyNotFoundError
|
||||
from danswer.redis.redis_pool import get_redis_client
|
||||
from danswer.utils.logger import setup_logger
|
||||
from shared_configs.configs import CURRENT_TENANT_ID_CONTEXTVAR
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
from shared_configs.configs import POSTGRES_DEFAULT_SCHEMA
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
@@ -32,6 +32,7 @@ from danswer.configs.app_configs import APP_PORT
|
||||
from danswer.configs.app_configs import AUTH_TYPE
|
||||
from danswer.configs.app_configs import DISABLE_GENERATIVE_AI
|
||||
from danswer.configs.app_configs import LOG_ENDPOINT_LATENCY
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.app_configs import OAUTH_CLIENT_ID
|
||||
from danswer.configs.app_configs import OAUTH_CLIENT_SECRET
|
||||
from danswer.configs.app_configs import POSTGRES_API_SERVER_POOL_OVERFLOW
|
||||
@@ -93,7 +94,6 @@ from danswer.utils.variable_functionality import fetch_versioned_implementation
|
||||
from danswer.utils.variable_functionality import global_version
|
||||
from danswer.utils.variable_functionality import set_is_ee_based_on_env_variable
|
||||
from shared_configs.configs import CORS_ALLOWED_ORIGIN
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
from shared_configs.configs import SENTRY_DSN
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator:
|
||||
|
||||
# If we are multi-tenant, we need to only set up initial public tables
|
||||
with Session(engine) as db_session:
|
||||
setup_danswer(db_session, None)
|
||||
setup_danswer(db_session)
|
||||
else:
|
||||
setup_multitenant_danswer()
|
||||
|
||||
|
||||
@@ -129,19 +129,7 @@ def stream_answer_objects(
|
||||
|
||||
persona = temporary_persona if temporary_persona else chat_session.persona
|
||||
|
||||
try:
|
||||
llm, fast_llm = get_llms_for_persona(persona=persona)
|
||||
except ValueError as e:
|
||||
logger.error(
|
||||
f"Failed to initialize LLMs for persona '{persona.name}': {str(e)}"
|
||||
)
|
||||
if "No LLM provider" in str(e):
|
||||
raise ValueError(
|
||||
"Please configure a Generative AI model to use this feature."
|
||||
) from e
|
||||
raise ValueError(
|
||||
"Failed to initialize the AI model. Please check your configuration and try again."
|
||||
) from e
|
||||
llm, fast_llm = get_llms_for_persona(persona=persona)
|
||||
|
||||
llm_tokenizer = get_tokenizer(
|
||||
model_name=llm.config.model_name,
|
||||
|
||||
@@ -102,8 +102,6 @@ class TenantRedis(redis.Redis):
|
||||
"reacquire",
|
||||
"create_lock",
|
||||
"startswith",
|
||||
"sadd",
|
||||
"srem",
|
||||
] # Regular methods that need simple prefixing
|
||||
|
||||
if item == "scan_iter":
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.chat_configs import BASE_RECENCY_DECAY
|
||||
from danswer.configs.chat_configs import CONTEXT_CHUNKS_ABOVE
|
||||
from danswer.configs.chat_configs import CONTEXT_CHUNKS_BELOW
|
||||
@@ -30,7 +31,6 @@ from danswer.utils.logger import setup_logger
|
||||
from danswer.utils.threadpool_concurrency import FunctionCall
|
||||
from danswer.utils.threadpool_concurrency import run_functions_in_parallel
|
||||
from danswer.utils.timing import log_function_time
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
@@ -39,7 +39,6 @@ logger = setup_logger()
|
||||
|
||||
def _create_indexable_chunks(
|
||||
preprocessed_docs: list[dict],
|
||||
tenant_id: str | None,
|
||||
) -> tuple[list[Document], list[DocMetadataAwareIndexChunk]]:
|
||||
ids_to_documents = {}
|
||||
chunks = []
|
||||
@@ -81,7 +80,7 @@ def _create_indexable_chunks(
|
||||
mini_chunk_embeddings=[],
|
||||
),
|
||||
title_embedding=preprocessed_doc["title_embedding"],
|
||||
tenant_id=tenant_id,
|
||||
tenant_id=None,
|
||||
access=default_public_access,
|
||||
document_sets=set(),
|
||||
boost=DEFAULT_BOOST,
|
||||
@@ -91,7 +90,7 @@ def _create_indexable_chunks(
|
||||
return list(ids_to_documents.values()), chunks
|
||||
|
||||
|
||||
def seed_initial_documents(db_session: Session, tenant_id: str | None) -> None:
|
||||
def seed_initial_documents(db_session: Session) -> None:
|
||||
"""
|
||||
Seed initial documents so users don't have an empty index to start
|
||||
|
||||
@@ -178,7 +177,7 @@ def seed_initial_documents(db_session: Session, tenant_id: str | None) -> None:
|
||||
)
|
||||
processed_docs = json.load(open(initial_docs_path))
|
||||
|
||||
docs, chunks = _create_indexable_chunks(processed_docs, tenant_id)
|
||||
docs, chunks = _create_indexable_chunks(processed_docs)
|
||||
|
||||
index_doc_batch_prepare(
|
||||
document_batch=docs,
|
||||
@@ -199,7 +198,6 @@ def seed_initial_documents(db_session: Session, tenant_id: str | None) -> None:
|
||||
|
||||
# Retries here because the index may take a few seconds to become ready
|
||||
# as we just sent over the Vespa schema and there is a slight delay
|
||||
|
||||
index_with_retries = retry_builder()(document_index.index)
|
||||
index_with_retries(chunks=chunks)
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ from danswer.file_store.file_store import get_default_file_store
|
||||
from danswer.file_store.models import ChatFileType
|
||||
from danswer.llm.answering.prompts.utils import build_dummy_prompt
|
||||
from danswer.server.features.persona.models import CreatePersonaRequest
|
||||
from danswer.server.features.persona.models import ImageGenerationToolStatus
|
||||
from danswer.server.features.persona.models import PersonaSharedNotificationData
|
||||
from danswer.server.features.persona.models import PersonaSnapshot
|
||||
from danswer.server.features.persona.models import PromptTemplateResponse
|
||||
@@ -228,16 +227,6 @@ def delete_persona(
|
||||
)
|
||||
|
||||
|
||||
@basic_router.get("/image-generation-tool")
|
||||
def get_image_generation_tool(
|
||||
_: User
|
||||
| None = Depends(current_user), # User param not used but kept for consistency
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> ImageGenerationToolStatus: # Use bool instead of str for boolean values
|
||||
is_available = is_image_generation_available(db_session=db_session)
|
||||
return ImageGenerationToolStatus(is_available=is_available)
|
||||
|
||||
|
||||
@basic_router.get("")
|
||||
def list_personas(
|
||||
user: User | None = Depends(current_user),
|
||||
|
||||
@@ -124,7 +124,3 @@ class PromptTemplateResponse(BaseModel):
|
||||
|
||||
class PersonaSharedNotificationData(BaseModel):
|
||||
persona_id: int
|
||||
|
||||
|
||||
class ImageGenerationToolStatus(BaseModel):
|
||||
is_available: bool
|
||||
|
||||
@@ -57,7 +57,6 @@ class UserInfo(BaseModel):
|
||||
oidc_expiry: datetime | None = None
|
||||
current_token_created_at: datetime | None = None
|
||||
current_token_expiry_length: int | None = None
|
||||
organization_name: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_model(
|
||||
@@ -65,7 +64,6 @@ class UserInfo(BaseModel):
|
||||
user: User,
|
||||
current_token_created_at: datetime | None = None,
|
||||
expiry_length: int | None = None,
|
||||
organization_name: str | None = None,
|
||||
) -> "UserInfo":
|
||||
return cls(
|
||||
id=str(user.id),
|
||||
@@ -82,7 +80,6 @@ class UserInfo(BaseModel):
|
||||
visible_assistants=user.visible_assistants,
|
||||
)
|
||||
),
|
||||
organization_name=organization_name,
|
||||
# set to None if TRACK_EXTERNAL_IDP_EXPIRY is False so that we avoid cases
|
||||
# where they previously had this set + used OIDC, and now they switched to
|
||||
# basic auth are now constantly getting redirected back to the login page
|
||||
|
||||
@@ -30,10 +30,10 @@ from danswer.auth.schemas import UserStatus
|
||||
from danswer.auth.users import current_admin_user
|
||||
from danswer.auth.users import current_curator_or_admin_user
|
||||
from danswer.auth.users import current_user
|
||||
from danswer.auth.users import get_tenant_id_for_email
|
||||
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 MULTI_TENANT
|
||||
from danswer.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS
|
||||
from danswer.configs.app_configs import VALID_EMAIL_DOMAINS
|
||||
from danswer.configs.constants import AuthType
|
||||
@@ -66,7 +66,6 @@ from ee.danswer.db.user_group import remove_curator_status__no_commit
|
||||
from ee.danswer.server.tenants.billing import register_tenant_users
|
||||
from ee.danswer.server.tenants.provisioning import add_users_to_tenant
|
||||
from ee.danswer.server.tenants.provisioning import remove_users_from_tenant
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
@@ -494,13 +493,10 @@ def verify_user_logged_in(
|
||||
token_created_at = (
|
||||
None if MULTI_TENANT else get_current_token_creation(user, db_session)
|
||||
)
|
||||
organization_name = get_tenant_id_for_email(user.email)
|
||||
|
||||
user_info = UserInfo.from_model(
|
||||
user,
|
||||
current_token_created_at=token_created_at,
|
||||
expiry_length=SESSION_EXPIRE_TIME_SECONDS,
|
||||
organization_name=organization_name,
|
||||
)
|
||||
|
||||
return user_info
|
||||
|
||||
@@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
|
||||
from danswer.chat.load_yamls import load_chat_yamls
|
||||
from danswer.configs.app_configs import DISABLE_INDEX_UPDATE_ON_SWAP
|
||||
from danswer.configs.app_configs import MANAGED_VESPA
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.constants import KV_REINDEX_KEY
|
||||
from danswer.configs.constants import KV_SEARCH_SETTINGS
|
||||
from danswer.configs.model_configs import FAST_GEN_AI_MODEL_VERSION
|
||||
@@ -51,7 +52,6 @@ from danswer.utils.logger import setup_logger
|
||||
from shared_configs.configs import ALT_INDEX_SUFFIX
|
||||
from shared_configs.configs import MODEL_SERVER_HOST
|
||||
from shared_configs.configs import MODEL_SERVER_PORT
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
from shared_configs.configs import SUPPORTED_EMBEDDING_MODELS
|
||||
from shared_configs.model_server_models import SupportedEmbeddingModel
|
||||
|
||||
@@ -59,7 +59,7 @@ from shared_configs.model_server_models import SupportedEmbeddingModel
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def setup_danswer(db_session: Session, tenant_id: str | None) -> None:
|
||||
def setup_danswer(db_session: Session) -> None:
|
||||
"""
|
||||
Setup Danswer for a particular tenant. In the Single Tenant case, it will set it up for the default schema
|
||||
on server startup. In the MT case, it will be called when the tenant is created.
|
||||
@@ -148,7 +148,7 @@ def setup_danswer(db_session: Session, tenant_id: str | None) -> None:
|
||||
# update multipass indexing setting based on GPU availability
|
||||
update_default_multipass_indexing(db_session)
|
||||
|
||||
seed_initial_documents(db_session, tenant_id)
|
||||
seed_initial_documents(db_session)
|
||||
|
||||
|
||||
def translate_saved_search_settings(db_session: Session) -> None:
|
||||
|
||||
@@ -4,14 +4,10 @@ from collections.abc import MutableMapping
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from typing import Any
|
||||
|
||||
from shared_configs.configs import CURRENT_TENANT_ID_CONTEXTVAR
|
||||
from shared_configs.configs import DEV_LOGGING_ENABLED
|
||||
from shared_configs.configs import LOG_FILE_NAME
|
||||
from shared_configs.configs import LOG_LEVEL
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
from shared_configs.configs import POSTGRES_DEFAULT_SCHEMA
|
||||
from shared_configs.configs import SLACK_CHANNEL_ID
|
||||
from shared_configs.configs import TENANT_ID_PREFIX
|
||||
|
||||
|
||||
logging.addLevelName(logging.INFO + 5, "NOTICE")
|
||||
@@ -70,18 +66,6 @@ class DanswerLoggingAdapter(logging.LoggerAdapter):
|
||||
if cc_pair_id is not None:
|
||||
msg = f"[CC Pair: {cc_pair_id}] {msg}"
|
||||
|
||||
# Add tenant information if it differs from default
|
||||
# This will always be the case for authenticated API requests
|
||||
if MULTI_TENANT:
|
||||
tenant_id = CURRENT_TENANT_ID_CONTEXTVAR.get()
|
||||
if tenant_id != POSTGRES_DEFAULT_SCHEMA:
|
||||
# Strip tenant_ prefix and take first 8 chars for cleaner logs
|
||||
tenant_display = tenant_id.removeprefix(TENANT_ID_PREFIX)
|
||||
short_tenant = (
|
||||
tenant_display[:8] if len(tenant_display) > 8 else tenant_display
|
||||
)
|
||||
msg = f"[t:{short_tenant}] {msg}"
|
||||
|
||||
# For Slack Bot, logs the channel relevant to the request
|
||||
channel_id = self.extra.get(SLACK_CHANNEL_ID) if self.extra else None
|
||||
if channel_id:
|
||||
|
||||
@@ -1,78 +1,39 @@
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Set
|
||||
from urllib.parse import urljoin
|
||||
from datetime import datetime
|
||||
from urllib import robotparser
|
||||
|
||||
import requests
|
||||
from usp.tree import sitemap_tree_for_homepage # type: ignore
|
||||
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def _get_sitemap_locations_from_robots(base_url: str) -> Set[str]:
|
||||
"""Extract sitemap URLs from robots.txt"""
|
||||
sitemap_urls: set = set()
|
||||
try:
|
||||
robots_url = urljoin(base_url, "/robots.txt")
|
||||
resp = requests.get(robots_url, timeout=10)
|
||||
if resp.status_code == 200:
|
||||
for line in resp.text.splitlines():
|
||||
if line.lower().startswith("sitemap:"):
|
||||
sitemap_url = line.split(":", 1)[1].strip()
|
||||
sitemap_urls.add(sitemap_url)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching robots.txt: {e}")
|
||||
return sitemap_urls
|
||||
def test_url(rp: robotparser.RobotFileParser | None, url: str) -> bool:
|
||||
if not rp:
|
||||
return True
|
||||
else:
|
||||
return rp.can_fetch("*", url)
|
||||
|
||||
|
||||
def _extract_urls_from_sitemap(sitemap_url: str) -> Set[str]:
|
||||
"""Extract URLs from a sitemap XML file"""
|
||||
urls: set[str] = set()
|
||||
try:
|
||||
resp = requests.get(sitemap_url, timeout=10)
|
||||
if resp.status_code != 200:
|
||||
return urls
|
||||
|
||||
root = ET.fromstring(resp.content)
|
||||
|
||||
# Handle both regular sitemaps and sitemap indexes
|
||||
# Remove namespace for easier parsing
|
||||
namespace = re.match(r"\{.*\}", root.tag)
|
||||
ns = namespace.group(0) if namespace else ""
|
||||
|
||||
if root.tag == f"{ns}sitemapindex":
|
||||
# This is a sitemap index
|
||||
for sitemap in root.findall(f".//{ns}loc"):
|
||||
if sitemap.text:
|
||||
sub_urls = _extract_urls_from_sitemap(sitemap.text)
|
||||
urls.update(sub_urls)
|
||||
else:
|
||||
# This is a regular sitemap
|
||||
for url in root.findall(f".//{ns}loc"):
|
||||
if url.text:
|
||||
urls.add(url.text)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing sitemap {sitemap_url}: {e}")
|
||||
|
||||
return urls
|
||||
def init_robots_txt(site: str) -> robotparser.RobotFileParser:
|
||||
ts = datetime.now().timestamp()
|
||||
robots_url = f"{site}/robots.txt?ts={ts}"
|
||||
rp = robotparser.RobotFileParser()
|
||||
rp.set_url(robots_url)
|
||||
rp.read()
|
||||
return rp
|
||||
|
||||
|
||||
def list_pages_for_site(site: str) -> list[str]:
|
||||
"""Get list of pages from a site's sitemaps"""
|
||||
site = site.rstrip("/")
|
||||
all_urls = set()
|
||||
rp: robotparser.RobotFileParser | None = None
|
||||
try:
|
||||
rp = init_robots_txt(site)
|
||||
except Exception:
|
||||
logger.warning("Failed to load robots.txt")
|
||||
|
||||
# Try both common sitemap locations
|
||||
sitemap_paths = ["/sitemap.xml", "/sitemap_index.xml"]
|
||||
for path in sitemap_paths:
|
||||
sitemap_url = urljoin(site, path)
|
||||
all_urls.update(_extract_urls_from_sitemap(sitemap_url))
|
||||
tree = sitemap_tree_for_homepage(site)
|
||||
|
||||
# Check robots.txt for additional sitemaps
|
||||
sitemap_locations = _get_sitemap_locations_from_robots(site)
|
||||
for sitemap_url in sitemap_locations:
|
||||
all_urls.update(_extract_urls_from_sitemap(sitemap_url))
|
||||
pages = [page.url for page in tree.all_pages() if test_url(rp, page.url)]
|
||||
pages = list(dict.fromkeys(pages))
|
||||
|
||||
return list(all_urls)
|
||||
return pages
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from danswer.background.celery.apps.primary import celery_app
|
||||
from danswer.background.task_utils import build_celery_task_wrapper
|
||||
from danswer.configs.app_configs import JOB_TIMEOUT
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.db.chat import delete_chat_sessions_older_than
|
||||
from danswer.db.engine import get_session_with_tenant
|
||||
from danswer.server.settings.store import load_settings
|
||||
@@ -29,7 +30,6 @@ from ee.danswer.external_permissions.permission_sync import (
|
||||
)
|
||||
from ee.danswer.server.reporting.usage_export_generation import create_new_usage_report
|
||||
from shared_configs.configs import CURRENT_TENANT_ID_CONTEXTVAR
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from danswer.auth.users import auth_backend
|
||||
from danswer.auth.users import create_danswer_oauth_router
|
||||
from danswer.auth.users import fastapi_users
|
||||
from danswer.configs.app_configs import AUTH_TYPE
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.app_configs import OAUTH_CLIENT_ID
|
||||
from danswer.configs.app_configs import OAUTH_CLIENT_SECRET
|
||||
from danswer.configs.app_configs import USER_AUTH_SECRET
|
||||
@@ -42,7 +43,6 @@ from ee.danswer.server.token_rate_limits.api import (
|
||||
)
|
||||
from ee.danswer.server.user_group.api import router as user_group_router
|
||||
from ee.danswer.utils.encryption import test_encryption
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi import Response
|
||||
|
||||
from danswer.configs.app_configs import USER_AUTH_SECRET
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.app_configs import SECRET_JWT_KEY
|
||||
from danswer.db.engine import is_valid_schema_name
|
||||
from shared_configs.configs import CURRENT_TENANT_ID_CONTEXTVAR
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
from shared_configs.configs import POSTGRES_DEFAULT_SCHEMA
|
||||
|
||||
|
||||
@@ -21,18 +21,15 @@ def add_tenant_id_middleware(app: FastAPI, logger: logging.LoggerAdapter) -> Non
|
||||
request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
||||
) -> Response:
|
||||
try:
|
||||
logger.info(f"Request route: {request.url.path}")
|
||||
if not MULTI_TENANT:
|
||||
tenant_id = POSTGRES_DEFAULT_SCHEMA
|
||||
else:
|
||||
token = request.cookies.get("fastapiusersauth")
|
||||
|
||||
token = request.cookies.get("tenant_details")
|
||||
if token:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
USER_AUTH_SECRET,
|
||||
audience=["fastapi-users:auth"],
|
||||
algorithms=["HS256"],
|
||||
token, SECRET_JWT_KEY, algorithms=["HS256"]
|
||||
)
|
||||
tenant_id = payload.get("tenant_id", POSTGRES_DEFAULT_SCHEMA)
|
||||
if not is_valid_schema_name(tenant_id):
|
||||
@@ -52,6 +49,8 @@ def add_tenant_id_middleware(app: FastAPI, logger: logging.LoggerAdapter) -> Non
|
||||
tenant_id = POSTGRES_DEFAULT_SCHEMA
|
||||
|
||||
CURRENT_TENANT_ID_CONTEXTVAR.set(tenant_id)
|
||||
logger.info(f"Middleware set current_tenant_id to: {tenant_id}")
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from fastapi import HTTPException
|
||||
|
||||
from danswer.auth.users import current_admin_user
|
||||
from danswer.auth.users import User
|
||||
from danswer.configs.app_configs import MULTI_TENANT
|
||||
from danswer.configs.app_configs import WEB_DOMAIN
|
||||
from danswer.db.engine import get_session_with_tenant
|
||||
from danswer.db.notification import create_notification
|
||||
@@ -24,7 +25,6 @@ from ee.danswer.server.tenants.provisioning import ensure_schema_exists
|
||||
from ee.danswer.server.tenants.provisioning import run_alembic_migrations
|
||||
from ee.danswer.server.tenants.provisioning import user_owns_a_tenant
|
||||
from shared_configs.configs import CURRENT_TENANT_ID_CONTEXTVAR
|
||||
from shared_configs.configs import MULTI_TENANT
|
||||
|
||||
|
||||
stripe.api_key = STRIPE_SECRET_KEY
|
||||
@@ -59,7 +59,7 @@ def create_tenant(
|
||||
run_alembic_migrations(tenant_id)
|
||||
|
||||
with get_session_with_tenant(tenant_id) as db_session:
|
||||
setup_danswer(db_session, tenant_id)
|
||||
setup_danswer(db_session)
|
||||
|
||||
add_users_to_tenant([email], tenant_id)
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ requests==2.32.2
|
||||
requests-oauthlib==1.3.1
|
||||
retry==0.9.2 # This pulls in py which is in CVE-2022-42969, must remove py from image
|
||||
rfc3986==1.5.0
|
||||
rt==3.1.2
|
||||
simple-salesforce==1.12.6
|
||||
slack-sdk==3.20.2
|
||||
SQLAlchemy[mypy]==2.0.15
|
||||
@@ -78,6 +79,7 @@ asana==5.0.8
|
||||
zenpy==2.0.41
|
||||
dropbox==11.36.2
|
||||
boto3-stubs[s3]==1.34.133
|
||||
ultimate_sitemap_parser==0.5
|
||||
stripe==10.12.0
|
||||
urllib3==2.2.3
|
||||
mistune==0.8.4
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# This file is purely for development use, not included in any builds
|
||||
import os
|
||||
import sys
|
||||
from time import sleep
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# makes it so `PYTHONPATH=.` is not required when running this script
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -17,58 +15,22 @@ from danswer.utils.logger import setup_logger # noqa: E402
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def wipe_vespa_index() -> bool:
|
||||
"""
|
||||
Wipes the Vespa index by deleting all documents.
|
||||
"""
|
||||
def wipe_vespa_index() -> None:
|
||||
continuation = None
|
||||
should_continue = True
|
||||
RETRIES = 3
|
||||
|
||||
while should_continue:
|
||||
params = {"selection": "true", "cluster": DOCUMENT_INDEX_NAME}
|
||||
if continuation:
|
||||
params["continuation"] = continuation
|
||||
params = {**params, "continuation": continuation}
|
||||
response = requests.delete(DOCUMENT_ID_ENDPOINT, params=params)
|
||||
response.raise_for_status()
|
||||
|
||||
for attempt in range(RETRIES):
|
||||
try:
|
||||
response = requests.delete(DOCUMENT_ID_ENDPOINT, params=params)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
print(response_json)
|
||||
|
||||
response_json = response.json()
|
||||
logger.info(f"Response: {response_json}")
|
||||
|
||||
continuation = response_json.get("continuation")
|
||||
should_continue = bool(continuation)
|
||||
break # Exit the retry loop if the request is successful
|
||||
|
||||
except RequestException:
|
||||
logger.exception("Request failed")
|
||||
sleep(2**attempt) # Exponential backoff
|
||||
else:
|
||||
logger.error(f"Max retries ({RETRIES}) exceeded. Exiting.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
Main function to execute the script.
|
||||
"""
|
||||
try:
|
||||
succeeded = wipe_vespa_index()
|
||||
except Exception:
|
||||
logger.exception("wipe_vespa_index exceptioned.")
|
||||
return 1
|
||||
|
||||
if not succeeded:
|
||||
logger.info("Vespa index wipe failed.")
|
||||
return 0
|
||||
|
||||
logger.info("Vespa index wiped successfully.")
|
||||
return 1
|
||||
continuation = response_json.get("continuation")
|
||||
should_continue = bool(continuation)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
wipe_vespa_index()
|
||||
|
||||
@@ -129,17 +129,12 @@ else:
|
||||
CORS_ALLOWED_ORIGIN = ["*"]
|
||||
|
||||
|
||||
# Multi-tenancy configuration
|
||||
MULTI_TENANT = os.environ.get("MULTI_TENANT", "").lower() == "true"
|
||||
|
||||
POSTGRES_DEFAULT_SCHEMA = os.environ.get("POSTGRES_DEFAULT_SCHEMA") or "public"
|
||||
|
||||
CURRENT_TENANT_ID_CONTEXTVAR = contextvars.ContextVar(
|
||||
"current_tenant_id", default=POSTGRES_DEFAULT_SCHEMA
|
||||
)
|
||||
|
||||
# Prefix used for all tenant ids
|
||||
TENANT_ID_PREFIX = "tenant_"
|
||||
|
||||
SUPPORTED_EMBEDDING_MODELS = [
|
||||
# Cloud-based models
|
||||
|
||||
@@ -70,14 +70,18 @@ class UserManager:
|
||||
|
||||
cookies = response.cookies.get_dict()
|
||||
session_cookie = cookies.get("fastapiusersauth")
|
||||
tenant_details_cookie = cookies.get("tenant_details")
|
||||
|
||||
if not session_cookie:
|
||||
raise Exception("Failed to login")
|
||||
|
||||
print(f"Logged in as {test_user.email}")
|
||||
|
||||
# Set cookies in the headers
|
||||
test_user.headers["Cookie"] = f"fastapiusersauth={session_cookie}; "
|
||||
# Set both cookies in the headers
|
||||
test_user.headers["Cookie"] = (
|
||||
f"fastapiusersauth={session_cookie}; "
|
||||
f"tenant_details={tenant_details_cookie}"
|
||||
)
|
||||
return test_user
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -110,7 +110,7 @@ def test_web_pruning(reset: None, vespa_client: vespa_fixture) -> None:
|
||||
test_filename = os.path.realpath(__file__)
|
||||
test_directory = os.path.dirname(test_filename)
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
port = 8889
|
||||
port = 8888
|
||||
|
||||
website_src = os.path.join(test_directory, "website")
|
||||
website_tgt = os.path.join(temp_dir, "website")
|
||||
|
||||
@@ -9,15 +9,12 @@ from pytest_mock import MockFixture
|
||||
|
||||
from danswer.connectors.mediawiki import wiki
|
||||
|
||||
# These tests are disabled for now
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def site() -> pywikibot.Site:
|
||||
return pywikibot.Site("en", "wikipedia")
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Test disabled")
|
||||
def test_pywikibot_timestamp_to_utc_datetime() -> None:
|
||||
timestamp_without_tzinfo = pywikibot.Timestamp(2023, 12, 27, 15, 38, 49)
|
||||
timestamp_min_timezone = timestamp_without_tzinfo.astimezone(datetime.timezone.min)
|
||||
@@ -83,7 +80,6 @@ class MockPage(pywikibot.Page):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Test disabled")
|
||||
def test_get_doc_from_page(site: pywikibot.Site) -> None:
|
||||
test_page = MockPage(site, "Test Page", _has_categories=True)
|
||||
doc = wiki.get_doc_from_page(test_page, site, wiki.DocumentSource.MEDIAWIKI)
|
||||
@@ -107,7 +103,6 @@ def test_get_doc_from_page(site: pywikibot.Site) -> None:
|
||||
assert doc.id == f"MEDIAWIKI_{test_page.pageid}_{test_page.full_url()}"
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Test disabled")
|
||||
def test_mediawiki_connector_recurse_depth() -> None:
|
||||
"""Test that the recurse_depth parameter is parsed correctly.
|
||||
|
||||
@@ -137,7 +132,6 @@ def test_mediawiki_connector_recurse_depth() -> None:
|
||||
assert connector.recurse_depth == recurse_depth
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="Test disabled")
|
||||
def test_load_from_state_calls_poll_source_with_nones(mocker: MockFixture) -> None:
|
||||
connector = wiki.MediaWikiConnector("wikipedia.org", [], [], 0, "test")
|
||||
poll_source = mocker.patch.object(connector, "poll_source")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine AS base
|
||||
FROM --platform=linux/amd64 node:20-alpine AS base
|
||||
|
||||
LABEL com.danswer.maintainer="founders@danswer.ai"
|
||||
LABEL com.danswer.description="This image is the web/frontend container of Danswer which \
|
||||
@@ -66,10 +66,7 @@ ARG NEXT_PUBLIC_POSTHOG_HOST
|
||||
ENV NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
|
||||
ENV NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST}
|
||||
|
||||
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||
ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
||||
|
||||
RUN npx next build
|
||||
RUN npx next build --no-lint
|
||||
|
||||
# Step 2. Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
@@ -134,8 +131,6 @@ ARG NEXT_PUBLIC_POSTHOG_KEY
|
||||
ARG NEXT_PUBLIC_POSTHOG_HOST
|
||||
ENV NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
|
||||
ENV NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST}
|
||||
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||
ENV NEXT_PUBLIC_SENTRY_DSN=${NEXT_PUBLIC_SENTRY_DSN}
|
||||
|
||||
|
||||
# Note: Don't expose ports here, Compose will handle that for us if necessary.
|
||||
|
||||
@@ -21,9 +21,10 @@ const nextConfig = {
|
||||
// - With both configured: Only unhandled errors are captured (no performance/session tracking)
|
||||
|
||||
// Determine if Sentry should be enabled
|
||||
const sentryEnabled = Boolean(
|
||||
process.env.SENTRY_AUTH_TOKEN && process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
);
|
||||
const sentryEnabled = false;
|
||||
// Boolean(
|
||||
// process.env.SENTRY_AUTH_TOKEN && process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
// );
|
||||
|
||||
// Sentry webpack plugin options
|
||||
const sentryWebpackPluginOptions = {
|
||||
|
||||
BIN
web/public/RequestTracker.png
Normal file
BIN
web/public/RequestTracker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -45,7 +45,12 @@ import { FullLLMProvider } from "../configuration/llm/interfaces";
|
||||
import CollapsibleSection from "./CollapsibleSection";
|
||||
import { SuccessfulPersonaUpdateRedirectType } from "./enums";
|
||||
import { Persona, StarterMessage } from "./interfaces";
|
||||
import { buildFinalPrompt, createPersona, updatePersona } from "./lib";
|
||||
import {
|
||||
buildFinalPrompt,
|
||||
createPersona,
|
||||
providersContainImageGeneratingSupport,
|
||||
updatePersona,
|
||||
} from "./lib";
|
||||
import { Popover } from "@/components/popover/Popover";
|
||||
import {
|
||||
CameraIcon,
|
||||
@@ -101,7 +106,7 @@ export function AssistantEditor({
|
||||
shouldAddAssistantToUserPreferences?: boolean;
|
||||
admin?: boolean;
|
||||
}) {
|
||||
const { refreshAssistants, isImageGenerationAvailable } = useAssistants();
|
||||
const { refreshAssistants } = useAssistants();
|
||||
const router = useRouter();
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
@@ -133,13 +138,42 @@ export function AssistantEditor({
|
||||
|
||||
const [isIconDropdownOpen, setIsIconDropdownOpen] = useState(false);
|
||||
|
||||
const [finalPrompt, setFinalPrompt] = useState<string | null>("");
|
||||
const [finalPromptError, setFinalPromptError] = useState<string>("");
|
||||
const [removePersonaImage, setRemovePersonaImage] = useState(false);
|
||||
|
||||
const triggerFinalPromptUpdate = async (
|
||||
systemPrompt: string,
|
||||
taskPrompt: string,
|
||||
retrievalDisabled: boolean
|
||||
) => {
|
||||
const response = await buildFinalPrompt(
|
||||
systemPrompt,
|
||||
taskPrompt,
|
||||
retrievalDisabled
|
||||
);
|
||||
if (response.ok) {
|
||||
setFinalPrompt((await response.json()).final_prompt_template);
|
||||
}
|
||||
};
|
||||
|
||||
const isUpdate = existingPersona !== undefined && existingPersona !== null;
|
||||
const existingPrompt = existingPersona?.prompts[0] ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (isUpdate && existingPrompt) {
|
||||
triggerFinalPromptUpdate(
|
||||
existingPrompt.system_prompt,
|
||||
existingPrompt.task_prompt,
|
||||
existingPersona.num_chunks === 0
|
||||
);
|
||||
}
|
||||
}, [isUpdate, existingPrompt, existingPersona?.num_chunks]);
|
||||
|
||||
const defaultProvider = llmProviders.find(
|
||||
(llmProvider) => llmProvider.is_default_provider
|
||||
);
|
||||
const defaultProviderName = defaultProvider?.provider;
|
||||
const defaultModelName = defaultProvider?.default_model_name;
|
||||
const providerDisplayNameToProviderName = new Map<string, string>();
|
||||
llmProviders.forEach((llmProvider) => {
|
||||
@@ -280,6 +314,14 @@ export function AssistantEditor({
|
||||
}
|
||||
)}
|
||||
onSubmit={async (values, formikHelpers) => {
|
||||
if (finalPromptError) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: "Cannot submit while there are errors in the form",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
values.llm_model_provider_override &&
|
||||
!values.llm_model_version_override
|
||||
@@ -600,7 +642,13 @@ export function AssistantEditor({
|
||||
placeholder="e.g. 'You are a professional email writing assistant that always uses a polite enthusiastic tone, emphasizes action items, and leaves blanks for the human to fill in when you have unknowns'"
|
||||
onChange={(e) => {
|
||||
setFieldValue("system_prompt", e.target.value);
|
||||
triggerFinalPromptUpdate(
|
||||
e.target.value,
|
||||
values.task_prompt,
|
||||
searchToolEnabled()
|
||||
);
|
||||
}}
|
||||
error={finalPromptError}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -727,8 +775,7 @@ export function AssistantEditor({
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={`w-fit ${
|
||||
!currentLLMSupportsImageOutput ||
|
||||
!isImageGenerationAvailable
|
||||
!currentLLMSupportsImageOutput
|
||||
? "opacity-70 cursor-not-allowed"
|
||||
: ""
|
||||
}`}
|
||||
@@ -740,14 +787,11 @@ export function AssistantEditor({
|
||||
onChange={() => {
|
||||
toggleToolInValues(imageGenerationTool.id);
|
||||
}}
|
||||
disabled={
|
||||
!currentLLMSupportsImageOutput ||
|
||||
!isImageGenerationAvailable
|
||||
}
|
||||
disabled={!currentLLMSupportsImageOutput}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{!currentLLMSupportsImageOutput ? (
|
||||
{!currentLLMSupportsImageOutput && (
|
||||
<TooltipContent side="top" align="center">
|
||||
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
|
||||
To use Image Generation, select GPT-4o or another
|
||||
@@ -755,15 +799,6 @@ export function AssistantEditor({
|
||||
this Assistant.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
) : (
|
||||
!isImageGenerationAvailable && (
|
||||
<TooltipContent side="top" align="center">
|
||||
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
|
||||
Image Generation requires an OpenAI or Azure
|
||||
Dalle configuration.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
)
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -989,6 +1024,11 @@ export function AssistantEditor({
|
||||
placeholder="e.g. 'Remember to reference all of the points mentioned in my message to you and focus on identifying action items that can move things forward'"
|
||||
onChange={(e) => {
|
||||
setFieldValue("task_prompt", e.target.value);
|
||||
triggerFinalPromptUpdate(
|
||||
values.system_prompt,
|
||||
e.target.value,
|
||||
searchToolEnabled()
|
||||
);
|
||||
}}
|
||||
explanationText="Learn about prompting in our docs!"
|
||||
explanationLink="https://docs.danswer.dev/guides/assistants"
|
||||
@@ -1002,10 +1042,6 @@ export function AssistantEditor({
|
||||
Starter Messages (Optional){" "}
|
||||
</div>
|
||||
</div>
|
||||
<SubLabel>
|
||||
Add pre-defined messages to help users get started. Only
|
||||
the first 4 will be displayed.
|
||||
</SubLabel>
|
||||
<FieldArray
|
||||
name="starter_messages"
|
||||
render={(
|
||||
|
||||
@@ -8,47 +8,12 @@ import { credentialTemplates } from "@/lib/connectors/credentials";
|
||||
import Link from "next/link";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { useContext } from "react";
|
||||
import { User } from "@/lib/types";
|
||||
|
||||
function BackButton({
|
||||
isAdmin,
|
||||
isCurator,
|
||||
user,
|
||||
}: {
|
||||
isAdmin: boolean;
|
||||
isCurator: boolean;
|
||||
user: User | null;
|
||||
}) {
|
||||
const buttonText = isAdmin ? "Admin Page" : "Curator Page";
|
||||
|
||||
if (!isAdmin && !isCurator) {
|
||||
console.error(
|
||||
`User is neither admin nor curator, defaulting to curator view. Found user:\n ${JSON.stringify(
|
||||
user,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-3 mt-6 flex-col flex items-center">
|
||||
<Link
|
||||
href={"/admin/add-connector"}
|
||||
className="w-full p-2 bg-white border-border border rounded items-center hover:bg-background-200 cursor-pointer transition-all duration-150 flex gap-x-2"
|
||||
>
|
||||
<SettingsIcon className="flex-none " />
|
||||
<p className="my-auto flex items-center text-sm">{buttonText}</p>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Sidebar() {
|
||||
const { formStep, setFormStep, connector, allowAdvanced, allowCreate } =
|
||||
useFormContext();
|
||||
const combinedSettings = useContext(SettingsContext);
|
||||
const { isCurator, isAdmin, user } = useUser();
|
||||
const { isLoadingUser, isAdmin } = useUser();
|
||||
if (!combinedSettings) {
|
||||
return null;
|
||||
}
|
||||
@@ -90,7 +55,17 @@ export default function Sidebar() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BackButton isAdmin={isAdmin} isCurator={isCurator} user={user} />
|
||||
<div className="mx-3 mt-6 gap-y-1 flex-col flex gap-x-1.5 items-center items-center">
|
||||
<Link
|
||||
href={"/admin/add-connector"}
|
||||
className="w-full p-2 bg-white border-border border rounded items-center hover:bg-background-200 cursor-pointer transition-all duration-150 flex gap-x-2"
|
||||
>
|
||||
<SettingsIcon className="flex-none " />
|
||||
<p className="my-auto flex items-center text-sm">
|
||||
{isAdmin ? "Admin Page" : "Curator Page"}
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="h-full flex">
|
||||
<div className="mx-auto w-full max-w-2xl px-4 py-8">
|
||||
|
||||
@@ -24,8 +24,8 @@ export default async function GalleryPage({
|
||||
chatSessions,
|
||||
folders,
|
||||
openedFolders,
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
toggleSidebar,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
|
||||
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import WrappedAssistantsMine from "./WrappedAssistantsMine";
|
||||
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
import { AssistantsProvider } from "@/components/context/AssistantsContext";
|
||||
|
||||
export default async function GalleryPage({
|
||||
searchParams,
|
||||
@@ -24,8 +24,8 @@ export default async function GalleryPage({
|
||||
chatSessions,
|
||||
folders,
|
||||
openedFolders,
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
toggleSidebar,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,7 @@ export const POST = async (request: NextRequest) => {
|
||||
|
||||
// Delete cookies only if cloud is enabled (jwt auth)
|
||||
if (NEXT_PUBLIC_CLOUD_ENABLED) {
|
||||
const cookiesToDelete = ["fastapiusersauth"];
|
||||
const cookiesToDelete = ["fastapiusersauth", "tenant_details"];
|
||||
const cookieOptions = {
|
||||
path: "/",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
|
||||
@@ -1,44 +1,96 @@
|
||||
import { getSourceMetadataForSources } from "@/lib/sources";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
||||
import { useState } from "react";
|
||||
import { DisplayAssistantCard } from "@/components/assistants/AssistantCards";
|
||||
import { Divider } from "@tremor/react";
|
||||
import { FiBookmark, FiInfo } from "react-icons/fi";
|
||||
import { HoverPopup } from "@/components/HoverPopup";
|
||||
|
||||
export function ChatIntro({ selectedPersona }: { selectedPersona: Persona }) {
|
||||
const [hoveredAssistant, setHoveredAssistant] = useState(false);
|
||||
export function ChatIntro({
|
||||
availableSources,
|
||||
selectedPersona,
|
||||
}: {
|
||||
availableSources: ValidSources[];
|
||||
selectedPersona: Persona;
|
||||
}) {
|
||||
const availableSourceMetadata = getSourceMetadataForSources(availableSources);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mobile:w-[90%] mobile:px-4 w-message-xs 2xl:w-message-sm 3xl:w-message">
|
||||
<div className="relative flex w-fit mx-auto justify-center">
|
||||
<div className="absolute z-10 -left-20 top-1/2 -translate-y-1/2">
|
||||
<div className="relative">
|
||||
<div
|
||||
onMouseEnter={() => setHoveredAssistant(true)}
|
||||
onMouseLeave={() => setHoveredAssistant(false)}
|
||||
className="p-4 scale-[.8] cursor-pointer border-dashed rounded-full flex border border-border border-2 border-dashed"
|
||||
style={{
|
||||
borderStyle: "dashed",
|
||||
borderWidth: "1.5px",
|
||||
borderSpacing: "4px",
|
||||
}}
|
||||
>
|
||||
<AssistantIcon
|
||||
disableToolip
|
||||
size={"large"}
|
||||
assistant={selectedPersona}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute right-full mr-2 w-[300px] top-0">
|
||||
{hoveredAssistant && (
|
||||
<DisplayAssistantCard selectedPersona={selectedPersona} />
|
||||
)}
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<div className="mobile:w-[90%] mobile:px-4 w-message-xs 2xl:w-message-sm 3xl:w-message">
|
||||
<div className="flex">
|
||||
<div className="mx-auto">
|
||||
<div className="m-auto text-3xl font-strong font-bold text-strong w-fit">
|
||||
{selectedPersona?.name || "How can I help you today?"}
|
||||
</div>
|
||||
{selectedPersona && (
|
||||
<div className="mt-1">{selectedPersona.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-3xl line-clamp-2 text-text-800 font-base font-semibold text-strong">
|
||||
{selectedPersona?.name || "How can I help you today?"}
|
||||
</div>
|
||||
{selectedPersona && selectedPersona.num_chunks !== 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<div>
|
||||
{selectedPersona.document_sets.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="font-bold mb-1 mt-4 text-emphasis">
|
||||
Knowledge Sets:{" "}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedPersona.document_sets.map((documentSet) => (
|
||||
<div key={documentSet.id} className="w-fit">
|
||||
<HoverPopup
|
||||
mainContent={
|
||||
<span className="flex w-fit p-1 rounded border border-border text-xs font-medium cursor-default">
|
||||
<div className="mr-1 my-auto">
|
||||
<FiBookmark />
|
||||
</div>
|
||||
{documentSet.name}
|
||||
</span>
|
||||
}
|
||||
popupContent={
|
||||
<div className="flex py-1 w-96">
|
||||
<FiInfo className="my-auto mr-2" />
|
||||
<div className="text-sm">
|
||||
{documentSet.description}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
direction="top"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{availableSources.length > 0 && (
|
||||
<div className="mt-1">
|
||||
<p className="font-bold mb-1 mt-4 text-emphasis">
|
||||
Connected Sources:{" "}
|
||||
</p>
|
||||
<div className={`flex flex-wrap gap-2`}>
|
||||
{availableSourceMetadata.map((sourceMetadata) => (
|
||||
<span
|
||||
key={sourceMetadata.internalName}
|
||||
className="flex w-fit p-1 rounded border border-border text-xs font-medium cursor-default"
|
||||
>
|
||||
<div className="mr-1 my-auto">
|
||||
{sourceMetadata.icon({})}
|
||||
</div>
|
||||
<div className="my-auto">
|
||||
{sourceMetadata.displayName}
|
||||
</div>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -103,7 +103,6 @@ import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
|
||||
import BlurBackground from "./shared_chat_search/BlurBackground";
|
||||
import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { Divider } from "@tremor/react";
|
||||
|
||||
const TEMP_USER_MESSAGE_ID = -1;
|
||||
const TEMP_ASSISTANT_MESSAGE_ID = -2;
|
||||
@@ -136,40 +135,12 @@ export function ChatPage({
|
||||
|
||||
const { assistants: availableAssistants, finalAssistants } = useAssistants();
|
||||
|
||||
const [showApiKeyModal, setShowApiKeyModal] = useState(
|
||||
!shouldShowWelcomeModal
|
||||
);
|
||||
const [showApiKeyModal, setShowApiKeyModal] = useState(true);
|
||||
|
||||
const { user, isAdmin, isLoadingUser } = useUser();
|
||||
|
||||
const existingChatIdRaw = searchParams.get("chatId");
|
||||
const [sendOnLoad, setSendOnLoad] = useState<string | null>(
|
||||
searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)
|
||||
);
|
||||
|
||||
const currentPersonaId = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
|
||||
const modelVersionFromSearchParams = searchParams.get(
|
||||
SEARCH_PARAM_NAMES.STRUCTURED_MODEL
|
||||
);
|
||||
|
||||
// Effect to handle sendOnLoad
|
||||
useEffect(() => {
|
||||
if (sendOnLoad) {
|
||||
const newSearchParams = new URLSearchParams(searchParams.toString());
|
||||
newSearchParams.delete(SEARCH_PARAM_NAMES.SEND_ON_LOAD);
|
||||
|
||||
// Update the URL without the send-on-load parameter
|
||||
router.replace(`?${newSearchParams.toString()}`, { scroll: false });
|
||||
|
||||
// Update our local state to reflect the change
|
||||
setSendOnLoad(null);
|
||||
|
||||
// If there's a message, submit it
|
||||
if (message) {
|
||||
onSubmit({ messageOverride: message });
|
||||
}
|
||||
}
|
||||
}, [sendOnLoad, searchParams, router]);
|
||||
|
||||
const existingChatSessionId = existingChatIdRaw ? existingChatIdRaw : null;
|
||||
|
||||
@@ -225,7 +196,7 @@ export function ChatPage({
|
||||
};
|
||||
|
||||
const llmOverrideManager = useLlmOverride(
|
||||
modelVersionFromSearchParams || (user?.preferences.default_model ?? null),
|
||||
user?.preferences.default_model ?? null,
|
||||
selectedChatSession,
|
||||
defaultTemperature
|
||||
);
|
||||
@@ -742,6 +713,12 @@ export function ChatPage({
|
||||
}, [liveAssistant]);
|
||||
|
||||
const filterManager = useFilters();
|
||||
const [finalAvailableSources, finalAvailableDocumentSets] =
|
||||
computeAvailableFilters({
|
||||
selectedPersona: selectedAssistant,
|
||||
availableSources,
|
||||
availableDocumentSets,
|
||||
});
|
||||
|
||||
const [currentFeedback, setCurrentFeedback] = useState<
|
||||
[FeedbackType, number] | null
|
||||
@@ -1876,9 +1853,6 @@ export function ChatPage({
|
||||
|
||||
{sharedChatSession && (
|
||||
<ShareChatSessionModal
|
||||
assistantId={liveAssistant?.id}
|
||||
message={message}
|
||||
modelOverride={llmOverrideManager.llmOverride}
|
||||
chatSessionId={sharedChatSession.id}
|
||||
existingSharedStatus={sharedChatSession.shared_status}
|
||||
onClose={() => setSharedChatSession(null)}
|
||||
@@ -1893,9 +1867,6 @@ export function ChatPage({
|
||||
)}
|
||||
{sharingModalVisible && chatSessionIdRef.current !== null && (
|
||||
<ShareChatSessionModal
|
||||
message={message}
|
||||
assistantId={liveAssistant?.id}
|
||||
modelOverride={llmOverrideManager.llmOverride}
|
||||
chatSessionId={chatSessionIdRef.current}
|
||||
existingSharedStatus={chatSessionSharedStatus}
|
||||
onClose={() => setSharingModalVisible(false)}
|
||||
@@ -1997,7 +1968,7 @@ export function ChatPage({
|
||||
{...getRootProps()}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-full flex flex-col overflow-y-auto include-scrollbar overflow-x-hidden relative`}
|
||||
className={`w-full h-full flex flex-col overflow-y-auto include-scrollbar overflow-x-hidden relative`}
|
||||
ref={scrollableDivRef}
|
||||
>
|
||||
{/* ChatBanner is a custom banner that displays a admin-specified message at
|
||||
@@ -2007,51 +1978,11 @@ export function ChatPage({
|
||||
!isFetchingChatMessages &&
|
||||
currentSessionChatState == "input" &&
|
||||
!loadingError && (
|
||||
<div className="h-full flex flex-col justify-center items-center">
|
||||
<ChatIntro selectedPersona={liveAssistant} />
|
||||
|
||||
<div
|
||||
key={-4}
|
||||
className={`
|
||||
mx-auto
|
||||
px-4
|
||||
w-full
|
||||
max-w-[750px]
|
||||
flex
|
||||
flex-wrap
|
||||
justify-center
|
||||
mt-2
|
||||
h-40
|
||||
items-start
|
||||
mb-6`}
|
||||
>
|
||||
{currentPersona?.starter_messages &&
|
||||
currentPersona.starter_messages.length >
|
||||
0 && (
|
||||
<>
|
||||
<Divider className="mx-2" />
|
||||
|
||||
{currentPersona.starter_messages
|
||||
.slice(0, 4)
|
||||
.map((starterMessage, i) => (
|
||||
<div key={i} className="w-1/2">
|
||||
<StarterMessage
|
||||
starterMessage={starterMessage}
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
messageOverride:
|
||||
starterMessage.message,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ChatIntro
|
||||
availableSources={finalAvailableSources}
|
||||
selectedPersona={liveAssistant}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={
|
||||
"-ml-4 w-full mx-auto " +
|
||||
@@ -2418,6 +2349,45 @@ export function ChatPage({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{currentPersona &&
|
||||
currentPersona.starter_messages &&
|
||||
currentPersona.starter_messages.length > 0 &&
|
||||
selectedAssistant &&
|
||||
messageHistory.length === 0 &&
|
||||
!isFetchingChatMessages && (
|
||||
<div
|
||||
key={-4}
|
||||
className={`
|
||||
mx-auto
|
||||
px-4
|
||||
w-searchbar-xs
|
||||
2xl:w-searchbar-sm
|
||||
3xl:w-searchbar
|
||||
grid
|
||||
gap-4
|
||||
grid-cols-1
|
||||
grid-rows-1
|
||||
mt-4
|
||||
md:grid-cols-2
|
||||
mb-6`}
|
||||
>
|
||||
{currentPersona.starter_messages.map(
|
||||
(starterMessage, i) => (
|
||||
<div key={i} className="w-full">
|
||||
<StarterMessage
|
||||
starterMessage={starterMessage}
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
messageOverride:
|
||||
starterMessage.message,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Some padding at the bottom so the search bar has space at the bottom to not cover the last message*/}
|
||||
<div ref={endPaddingRef} className="h-[95px]" />
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
import { StarterMessage as StarterMessageType } from "../admin/assistants/interfaces";
|
||||
import { StarterMessage } from "../admin/assistants/interfaces";
|
||||
|
||||
export function StarterMessage({
|
||||
starterMessage,
|
||||
onClick,
|
||||
}: {
|
||||
starterMessage: StarterMessageType;
|
||||
starterMessage: StarterMessage;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="mb-4 mx-2 group relative overflow-hidden rounded-xl border border-border bg-gradient-to-br from-white to-background p-4 shadow-sm transition-all duration-300 hover:shadow-md hover:scale-[1.005] cursor-pointer"
|
||||
className={
|
||||
"py-2 px-3 rounded border border-border bg-white cursor-pointer hover:bg-hover-light h-full"
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-100 to-purple-100 opacity-0 group-hover:opacity-20 transition-opacity duration-300" />
|
||||
<h3
|
||||
className="text-base flex items-center font-medium text-text-800 group-hover:text-text-900 transition-colors duration-300
|
||||
line-clamp-2 gap-x-2 overflow-hidden"
|
||||
>
|
||||
{starterMessage.name}
|
||||
</h3>
|
||||
<div className={`overflow-hidden transition-all duration-300 max-h-20}`}>
|
||||
<p className="text-sm text-text-600 mt-2">
|
||||
{starterMessage.description}
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-medium text-emphasis">{starterMessage.name}</p>
|
||||
<p className="text-subtle text-sm">{starterMessage.description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,6 @@ import { Spinner } from "@/components/Spinner";
|
||||
import { ChatSessionSharedStatus } from "../interfaces";
|
||||
import { FiCopy } from "react-icons/fi";
|
||||
import { CopyButton } from "@/components/CopyButton";
|
||||
import { SEARCH_PARAM_NAMES } from "../searchParams";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { destructureValue, structureValue } from "@/lib/llm/utils";
|
||||
import { LlmOverride } from "@/lib/hooks";
|
||||
|
||||
function buildShareLink(chatSessionId: string) {
|
||||
const baseUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
@@ -30,34 +26,6 @@ async function generateShareLink(chatSessionId: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function generateCloneLink(
|
||||
message?: string,
|
||||
assistantId?: number,
|
||||
modelOverride?: LlmOverride
|
||||
) {
|
||||
const baseUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const model = modelOverride
|
||||
? structureValue(
|
||||
modelOverride.name,
|
||||
modelOverride.provider,
|
||||
modelOverride.modelName
|
||||
)
|
||||
: null;
|
||||
return `${baseUrl}/chat${
|
||||
message
|
||||
? `?${SEARCH_PARAM_NAMES.USER_PROMPT}=${encodeURIComponent(message)}`
|
||||
: ""
|
||||
}${
|
||||
assistantId
|
||||
? `${message ? "&" : "?"}${SEARCH_PARAM_NAMES.PERSONA_ID}=${assistantId}`
|
||||
: ""
|
||||
}${
|
||||
model
|
||||
? `${message || assistantId ? "&" : "?"}${SEARCH_PARAM_NAMES.STRUCTURED_MODEL}=${encodeURIComponent(model)}`
|
||||
: ""
|
||||
}${message ? `&${SEARCH_PARAM_NAMES.SEND_ON_LOAD}=true` : ""}`;
|
||||
}
|
||||
|
||||
async function deleteShareLink(chatSessionId: string) {
|
||||
const response = await fetch(`/api/chat/chat-session/${chatSessionId}`, {
|
||||
method: "PATCH",
|
||||
@@ -75,162 +43,117 @@ export function ShareChatSessionModal({
|
||||
existingSharedStatus,
|
||||
onShare,
|
||||
onClose,
|
||||
message,
|
||||
assistantId,
|
||||
modelOverride,
|
||||
}: {
|
||||
chatSessionId: string;
|
||||
existingSharedStatus: ChatSessionSharedStatus;
|
||||
onShare?: (shared: boolean) => void;
|
||||
onClose: () => void;
|
||||
message?: string;
|
||||
assistantId?: number;
|
||||
modelOverride?: LlmOverride;
|
||||
}) {
|
||||
const [linkGenerating, setLinkGenerating] = useState(false);
|
||||
const [shareLink, setShareLink] = useState<string>(
|
||||
existingSharedStatus === ChatSessionSharedStatus.Public
|
||||
? buildShareLink(chatSessionId)
|
||||
: ""
|
||||
);
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<Modal onOutsideClick={onClose} width="max-w-3xl">
|
||||
<>
|
||||
<div className="flex mb-4">
|
||||
<h2 className="text-2xl text-emphasis font-bold flex my-auto">
|
||||
Share link to Chat
|
||||
</h2>
|
||||
</div>
|
||||
<Modal onOutsideClick={onClose} width="max-w-3xl">
|
||||
<>
|
||||
<div className="flex mb-4">
|
||||
<h2 className="text-2xl text-emphasis font-bold flex my-auto">
|
||||
Share link to Chat
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-2">
|
||||
{shareLink ? (
|
||||
<div>
|
||||
<Text>
|
||||
This chat session is currently shared. Anyone at your
|
||||
organization can view the message history using the following
|
||||
link:
|
||||
</Text>
|
||||
{linkGenerating && <Spinner />}
|
||||
|
||||
<div className="flex mt-2">
|
||||
<CopyButton content={shareLink} />
|
||||
<a
|
||||
href={shareLink}
|
||||
target="_blank"
|
||||
className="underline text-link mt-1 ml-1 text-sm my-auto"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{shareLink}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex mt-2">
|
||||
{shareLink ? (
|
||||
<div>
|
||||
<Text>
|
||||
This chat session is currently shared. Anyone at your
|
||||
organization can view the message history using the following
|
||||
link:
|
||||
</Text>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Text className="mb-4">
|
||||
Click the button below to make the chat private again.
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const success = await deleteShareLink(chatSessionId);
|
||||
if (success) {
|
||||
setShareLink("");
|
||||
onShare && onShare(false);
|
||||
} else {
|
||||
alert("Failed to delete share link");
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
color="red"
|
||||
<div className="flex mt-2">
|
||||
<CopyButton content={shareLink} />
|
||||
<a
|
||||
href={shareLink}
|
||||
target="_blank"
|
||||
className="underline text-link mt-1 ml-1 text-sm my-auto"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Delete Share Link
|
||||
</Button>
|
||||
{shareLink}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Callout title="Warning" color="yellow" className="mb-4">
|
||||
Ensure that all content in the chat is safe to share with the
|
||||
whole organization. The content of the retrieved documents
|
||||
will not be visible, but the names of cited documents as well
|
||||
as the AI and human messages will be visible.
|
||||
</Callout>
|
||||
<div className="flex w-full justify-between">
|
||||
<Button
|
||||
icon={FiCopy}
|
||||
onClick={async () => {
|
||||
// NOTE: for "insecure" non-https setup, the `navigator.clipboard.writeText` may fail
|
||||
// as the browser may not allow the clipboard to be accessed.
|
||||
try {
|
||||
const shareLink =
|
||||
await generateShareLink(chatSessionId);
|
||||
if (!shareLink) {
|
||||
alert("Failed to generate share link");
|
||||
} else {
|
||||
setShareLink(shareLink);
|
||||
onShare && onShare(true);
|
||||
navigator.clipboard.writeText(shareLink);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
color="green"
|
||||
>
|
||||
Generate and Copy Share Link
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider className="my-4" />
|
||||
<div className="mb-4">
|
||||
<Callout title="Clone Chat" color="blue">
|
||||
Generate a link to clone this chat session with the current query.
|
||||
This allows others to start a new chat with the same initial
|
||||
message and settings.
|
||||
</Callout>
|
||||
</div>
|
||||
<div className="flex w-full justify-between">
|
||||
<Button
|
||||
icon={FiCopy}
|
||||
onClick={async () => {
|
||||
// NOTE: for "insecure" non-https setup, the `navigator.clipboard.writeText` may fail
|
||||
// as the browser may not allow the clipboard to be accessed.
|
||||
try {
|
||||
const cloneLink = await generateCloneLink(
|
||||
message,
|
||||
assistantId,
|
||||
modelOverride
|
||||
);
|
||||
if (!cloneLink) {
|
||||
setPopup({
|
||||
message: "Failed to generate clone link",
|
||||
type: "error",
|
||||
});
|
||||
<Divider />
|
||||
|
||||
<Text className="mb-4">
|
||||
Click the button below to make the chat private again.
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setLinkGenerating(true);
|
||||
|
||||
const success = await deleteShareLink(chatSessionId);
|
||||
if (success) {
|
||||
setShareLink("");
|
||||
onShare && onShare(false);
|
||||
} else {
|
||||
navigator.clipboard.writeText(cloneLink);
|
||||
setPopup({
|
||||
message: "Link copied to clipboard!",
|
||||
type: "success",
|
||||
});
|
||||
alert("Failed to delete share link");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Failed to generate or copy link.");
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
color="blue"
|
||||
>
|
||||
Generate and Copy Clone Link
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
setLinkGenerating(false);
|
||||
}}
|
||||
size="xs"
|
||||
color="red"
|
||||
>
|
||||
Delete Share Link
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Callout title="Warning" color="yellow" className="mb-4">
|
||||
Ensure that all content in the chat is safe to share with the
|
||||
whole organization. The content of the retrieved documents will
|
||||
not be visible, but the names of cited documents as well as the
|
||||
AI and human messages will be visible.
|
||||
</Callout>
|
||||
|
||||
<Button
|
||||
icon={FiCopy}
|
||||
onClick={async () => {
|
||||
setLinkGenerating(true);
|
||||
|
||||
// NOTE: for "inescure" non-https setup, the `navigator.clipboard.writeText` may fail
|
||||
// as the browser may not allow the clipboard to be accessed.
|
||||
try {
|
||||
const shareLink = await generateShareLink(chatSessionId);
|
||||
if (!shareLink) {
|
||||
alert("Failed to generate share link");
|
||||
} else {
|
||||
setShareLink(shareLink);
|
||||
onShare && onShare(true);
|
||||
navigator.clipboard.writeText(shareLink);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
setLinkGenerating(false);
|
||||
}}
|
||||
size="xs"
|
||||
color="green"
|
||||
>
|
||||
Generate and Copy Share Link
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrap
|
||||
import { ChatProvider } from "@/components/context/ChatContext";
|
||||
import { fetchChatData } from "@/lib/chat/fetchChatData";
|
||||
import WrappedChat from "./WrappedChat";
|
||||
import { AssistantsProvider } from "@/components/context/AssistantsContext";
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
|
||||
@@ -9,7 +9,6 @@ export const SEARCH_PARAM_NAMES = {
|
||||
TEMPERATURE: "temperature",
|
||||
MODEL_VERSION: "model-version",
|
||||
SYSTEM_PROMPT: "system-prompt",
|
||||
STRUCTURED_MODEL: "structured-model",
|
||||
// user message
|
||||
USER_PROMPT: "user-prompt",
|
||||
SUBMIT_ON_LOAD: "submit-on-load",
|
||||
@@ -17,7 +16,6 @@ export const SEARCH_PARAM_NAMES = {
|
||||
TITLE: "title",
|
||||
// for seeding chats
|
||||
SEEDED: "seeded",
|
||||
SEND_ON_LOAD: "send-on-load",
|
||||
};
|
||||
|
||||
export function shouldSubmitOnLoad(searchParams: ReadonlyURLSearchParams) {
|
||||
|
||||
@@ -19,16 +19,15 @@ import { fetchAssistantData } from "@/lib/chat/fetchAssistantdata";
|
||||
import { AppProvider } from "@/components/context/AppProvider";
|
||||
import { PHProvider } from "./providers";
|
||||
import { default as dynamicImport } from "next/dynamic";
|
||||
import { getCurrentUserSS } from "@/lib/userSS";
|
||||
|
||||
const PostHogPageView = dynamicImport(() => import("./PostHogPageView"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
display: "swap",
|
||||
preload: false, // Add this to prevent build-time font loading issues
|
||||
adjustFontFallback: true, // Add this to ensure smooth fallback
|
||||
});
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
@@ -58,11 +57,7 @@ export default async function RootLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [combinedSettings, assistantsData, user] = await Promise.all([
|
||||
fetchSettingsSS(),
|
||||
fetchAssistantData(),
|
||||
getCurrentUserSS(),
|
||||
]);
|
||||
const combinedSettings = await fetchSettingsSS();
|
||||
|
||||
const productGating =
|
||||
combinedSettings?.settings.product_gating ?? GatingType.NONE;
|
||||
@@ -170,12 +165,11 @@ export default async function RootLayout({
|
||||
);
|
||||
}
|
||||
|
||||
const { assistants, hasAnyConnectors, hasImageCompatibleModel } =
|
||||
assistantsData;
|
||||
const data = await fetchAssistantData();
|
||||
const { assistants, hasAnyConnectors, hasImageCompatibleModel } = data;
|
||||
|
||||
return getPageContent(
|
||||
<AppProvider
|
||||
user={user}
|
||||
settings={combinedSettings}
|
||||
assistants={assistants}
|
||||
hasAnyConnectors={hasAnyConnectors}
|
||||
|
||||
@@ -10,10 +10,16 @@ import { CCPairBasicInfo, DocumentSet, Tag, User } from "@/lib/types";
|
||||
import { cookies } from "next/headers";
|
||||
import { SearchType } from "@/lib/search/interfaces";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import {
|
||||
WelcomeModal,
|
||||
hasCompletedWelcomeFlowSS,
|
||||
} from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import { personaComparator } from "../admin/assistants/lib";
|
||||
import { FullEmbeddingModelResponse } from "@/components/embedding/interfaces";
|
||||
import { NoSourcesModal } from "@/components/initialSetup/search/NoSourcesModal";
|
||||
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
|
||||
import { ChatPopup } from "../chat/ChatPopup";
|
||||
import {
|
||||
FetchAssistantsResponse,
|
||||
@@ -32,10 +38,6 @@ import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { LLMProviderDescriptor } from "../admin/configuration/llm/interfaces";
|
||||
import { AssistantsProvider } from "@/components/context/AssistantsContext";
|
||||
import { headers } from "next/headers";
|
||||
import {
|
||||
hasCompletedWelcomeFlowSS,
|
||||
WelcomeModal,
|
||||
} from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
|
||||
export default async function Home({
|
||||
searchParams,
|
||||
@@ -168,6 +170,14 @@ export default async function Home({
|
||||
ccPairs.length === 0 &&
|
||||
!shouldShowWelcomeModal;
|
||||
|
||||
const shouldDisplaySourcesIncompleteModal =
|
||||
!ccPairs.some(
|
||||
(ccPair) => ccPair.has_successful_run && ccPair.docs_indexed > 0
|
||||
) &&
|
||||
!shouldDisplayNoSourcesModal &&
|
||||
!shouldShowWelcomeModal &&
|
||||
(!user || user.role == "admin");
|
||||
|
||||
const sidebarToggled = cookies().get(SIDEBAR_TOGGLED_COOKIE_NAME);
|
||||
const agenticSearchToggle = cookies().get(AGENTIC_SEARCH_TYPE_COOKIE_NAME);
|
||||
|
||||
@@ -182,8 +192,12 @@ export default async function Home({
|
||||
return (
|
||||
<>
|
||||
<HealthCheckBanner />
|
||||
<InstantSSRAutoRefresh />
|
||||
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
|
||||
<InstantSSRAutoRefresh />
|
||||
{shouldDisplayNoSourcesModal && <NoSourcesModal />}
|
||||
{shouldDisplaySourcesIncompleteModal && (
|
||||
<NoCompleteSourcesModal ccPairs={ccPairs} />
|
||||
)}
|
||||
{/* ChatPopup is a custom popup that displays a admin-specified message on initial user visit.
|
||||
Only used in the EE version of the app. */}
|
||||
<ChatPopup />
|
||||
|
||||
@@ -115,63 +115,3 @@ export function DraggableAssistantCard(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DisplayAssistantCard({
|
||||
selectedPersona,
|
||||
}: {
|
||||
selectedPersona: Persona;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-4 bg-white/90 backdrop-blur-sm rounded-lg shadow-md border border-border/50 max-w-md w-full mx-auto transition-all duration-300 ease-in-out hover:shadow-lg">
|
||||
<div className="flex items-center mb-3">
|
||||
<AssistantIcon
|
||||
disableToolip
|
||||
size="medium"
|
||||
assistant={selectedPersona}
|
||||
/>
|
||||
<h2 className="ml-3 text-xl font-semibold text-text-900">
|
||||
{selectedPersona.name}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-text-600 mb-3 leading-relaxed">
|
||||
{selectedPersona.description}
|
||||
</p>
|
||||
{selectedPersona.tools.length > 0 ||
|
||||
selectedPersona.llm_relevance_filter ||
|
||||
selectedPersona.llm_filter_extraction ? (
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-medium text-text-900">Capabilities:</h3>
|
||||
<ul className="space-y-.5">
|
||||
{/* display all tools */}
|
||||
{selectedPersona.tools.map((tool, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="flex items-center text-sm text-text-700"
|
||||
>
|
||||
<span className="mr-2 text-text-500 opacity-70">•</span>{" "}
|
||||
{tool.display_name}
|
||||
</li>
|
||||
))}
|
||||
{/* Built in capabilities */}
|
||||
{selectedPersona.llm_relevance_filter && (
|
||||
<li className="flex items-center text-sm text-text-700">
|
||||
<span className="mr-2 text-text-500 opacity-70">•</span>{" "}
|
||||
Advanced Relevance Filtering
|
||||
</li>
|
||||
)}
|
||||
{selectedPersona.llm_filter_extraction && (
|
||||
<li className="flex items-center text-sm text-text-700">
|
||||
<span className="mr-2 text-text-500 opacity-70">•</span> Smart
|
||||
Information Extraction
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-600 italic">
|
||||
No specific capabilities listed for this assistant.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,38 +23,22 @@ export function AssistantIcon({
|
||||
assistant,
|
||||
size,
|
||||
border,
|
||||
disableToolip,
|
||||
}: {
|
||||
assistant: Persona;
|
||||
size?: "small" | "medium" | "large" | "header";
|
||||
size?: "small" | "medium" | "large";
|
||||
border?: boolean;
|
||||
disableToolip?: boolean;
|
||||
}) {
|
||||
const color = darkerGenerateColorFromId(assistant.id.toString());
|
||||
|
||||
return (
|
||||
<CustomTooltip
|
||||
disabled={disableToolip}
|
||||
showTick
|
||||
line
|
||||
wrap
|
||||
content={assistant.description}
|
||||
>
|
||||
<CustomTooltip showTick line wrap content={assistant.description}>
|
||||
{
|
||||
// Prioritization order: image, graph, defaults
|
||||
assistant.uploaded_image_id ? (
|
||||
<img
|
||||
alt={assistant.name}
|
||||
className={`object-cover object-center rounded-sm overflow-hidden transition-opacity duration-300 opacity-100
|
||||
${
|
||||
size === "large"
|
||||
? "w-10 h-10"
|
||||
: size === "header"
|
||||
? "w-14 h-14"
|
||||
: size === "medium"
|
||||
? "w-8 h-8"
|
||||
: "w-6 h-6"
|
||||
}`}
|
||||
${size === "large" ? "w-8 h-8" : "w-6 h-6"}`}
|
||||
src={buildImgUrl(assistant.uploaded_image_id)}
|
||||
loading="lazy"
|
||||
/>
|
||||
@@ -62,36 +46,20 @@ export function AssistantIcon({
|
||||
<div
|
||||
className={`flex-none
|
||||
${border && "ring ring-[1px] ring-border-strong "}
|
||||
${
|
||||
size === "large"
|
||||
? "w-10 h-10"
|
||||
: size === "header"
|
||||
? "w-14 h-14"
|
||||
: size === "medium"
|
||||
? "w-8 h-8"
|
||||
: "w-6 h-6"
|
||||
} `}
|
||||
${size === "large" ? "w-10 h-10" : "w-6 h-6"} `}
|
||||
>
|
||||
{createSVG(
|
||||
{ encodedGrid: assistant.icon_shape, filledSquares: 0 },
|
||||
assistant.icon_color,
|
||||
size === "large"
|
||||
? 40
|
||||
: size === "header"
|
||||
? 56
|
||||
: size === "medium"
|
||||
? 32
|
||||
: 24
|
||||
size == "large" ? 36 : 24
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`flex-none rounded-sm overflow-hidden
|
||||
${border && "border border-.5 border-border-strong "}
|
||||
${size === "large" ? "w-10 h-10" : ""}
|
||||
${size === "header" ? "w-14 h-14" : ""}
|
||||
${size === "medium" ? "w-8 h-8" : ""}
|
||||
${!size || size === "small" ? "w-6 h-6" : ""} `}
|
||||
${size === "large" && "w-12 h-12"}
|
||||
${(!size || size === "small") && "w-6 h-6"} `}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2,41 +2,26 @@ import { useProviderStatus } from "./ProviderContext";
|
||||
|
||||
export default function CredentialNotConfigured({
|
||||
showConfigureAPIKey,
|
||||
noSources,
|
||||
}: {
|
||||
showConfigureAPIKey: () => void;
|
||||
noSources?: boolean;
|
||||
}) {
|
||||
const { shouldShowConfigurationNeeded } = useProviderStatus();
|
||||
|
||||
if (!shouldShowConfigurationNeeded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{noSources ? (
|
||||
<p className="text-base text-center w-full text-subtle">
|
||||
You have not yet added any sources. Please add{" "}
|
||||
<a
|
||||
href="/admin/add-connector"
|
||||
className="text-link hover:underline cursor-pointer"
|
||||
>
|
||||
a source
|
||||
</a>{" "}
|
||||
to continue.
|
||||
</p>
|
||||
) : (
|
||||
shouldShowConfigurationNeeded && (
|
||||
<p className="text-base text-center w-full text-subtle">
|
||||
Please note that you have not yet configured an LLM provider. You
|
||||
can configure one{" "}
|
||||
<button
|
||||
onClick={showConfigureAPIKey}
|
||||
className="text-link hover:underline cursor-pointer"
|
||||
>
|
||||
here
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
<p className="text-base text-center w-full text-subtle">
|
||||
Please note that you have not yet configured an LLM provider. You can
|
||||
configure one{" "}
|
||||
<button
|
||||
onClick={showConfigureAPIKey}
|
||||
className="text-link hover:underline cursor-pointer"
|
||||
>
|
||||
here
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,9 @@ import { ProviderContextProvider } from "../chat_search/ProviderContext";
|
||||
import { SettingsProvider } from "../settings/SettingsProvider";
|
||||
import { AssistantsProvider } from "./AssistantsContext";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { User } from "@/lib/types";
|
||||
|
||||
interface AppProviderProps {
|
||||
children: React.ReactNode;
|
||||
user: User | null;
|
||||
settings: CombinedSettings;
|
||||
assistants: Persona[];
|
||||
hasAnyConnectors: boolean;
|
||||
@@ -18,14 +16,13 @@ interface AppProviderProps {
|
||||
|
||||
export const AppProvider = ({
|
||||
children,
|
||||
user,
|
||||
settings,
|
||||
assistants,
|
||||
hasAnyConnectors,
|
||||
hasImageCompatibleModel,
|
||||
}: AppProviderProps) => {
|
||||
return (
|
||||
<UserProvider user={user}>
|
||||
<UserProvider>
|
||||
<ProviderContextProvider>
|
||||
<SettingsProvider settings={settings}>
|
||||
<AssistantsProvider
|
||||
|
||||
@@ -21,7 +21,6 @@ interface AssistantsContextProps {
|
||||
finalAssistants: Persona[];
|
||||
ownedButHiddenAssistants: Persona[];
|
||||
refreshAssistants: () => Promise<void>;
|
||||
isImageGenerationAvailable: boolean;
|
||||
|
||||
// Admin only
|
||||
editablePersonas: Persona[];
|
||||
@@ -48,54 +47,51 @@ export const AssistantsProvider: React.FC<{
|
||||
);
|
||||
const { user, isLoadingUser, isAdmin } = useUser();
|
||||
const [editablePersonas, setEditablePersonas] = useState<Persona[]>([]);
|
||||
const [allAssistants, setAllAssistants] = useState<Persona[]>([]);
|
||||
|
||||
const [isImageGenerationAvailable, setIsImageGenerationAvailable] =
|
||||
useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkImageGenerationAvailability = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/persona/image-generation-tool");
|
||||
if (response.ok) {
|
||||
const { is_available } = await response.json();
|
||||
setIsImageGenerationAvailable(is_available);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking image generation availability:", error);
|
||||
}
|
||||
};
|
||||
|
||||
checkImageGenerationAvailability();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPersonas = async () => {
|
||||
const fetchEditablePersonas = async () => {
|
||||
if (!isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [editableResponse, allResponse] = await Promise.all([
|
||||
fetch("/api/admin/persona?get_editable=true"),
|
||||
fetch("/api/admin/persona"),
|
||||
]);
|
||||
|
||||
if (editableResponse.ok) {
|
||||
const editablePersonas = await editableResponse.json();
|
||||
setEditablePersonas(editablePersonas);
|
||||
}
|
||||
|
||||
if (allResponse.ok) {
|
||||
const allPersonas = await allResponse.json();
|
||||
setAllAssistants(allPersonas);
|
||||
const response = await fetch("/api/admin/persona?get_editable=true");
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch editable personas");
|
||||
return;
|
||||
}
|
||||
const personas = await response.json();
|
||||
setEditablePersonas(personas);
|
||||
} catch (error) {
|
||||
console.error("Error fetching personas:", error);
|
||||
console.error("Error fetching editable personas:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPersonas();
|
||||
fetchEditablePersonas();
|
||||
}, [isAdmin]);
|
||||
|
||||
const [allAssistants, setAllAssistants] = useState<Persona[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAllAssistants = async () => {
|
||||
if (!isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/admin/persona");
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch all personas");
|
||||
return;
|
||||
}
|
||||
const personas = await response.json();
|
||||
setAllAssistants(personas);
|
||||
} catch (error) {
|
||||
console.error("Error fetching all personas:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAllAssistants();
|
||||
}, [isAdmin]);
|
||||
|
||||
const refreshAssistants = async () => {
|
||||
@@ -166,7 +162,6 @@ export const AssistantsProvider: React.FC<{
|
||||
refreshAssistants,
|
||||
editablePersonas,
|
||||
allAssistants,
|
||||
isImageGenerationAvailable,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -62,6 +62,7 @@ import OCIStorageSVG from "../../../public/OCI.svg";
|
||||
import googleCloudStorageIcon from "../../../public/GoogleCloudStorage.png";
|
||||
import guruIcon from "../../../public/Guru.svg";
|
||||
import gongIcon from "../../../public/Gong.png";
|
||||
import requestTrackerIcon from "../../../public/RequestTracker.png";
|
||||
import zulipIcon from "../../../public/Zulip.png";
|
||||
import linearIcon from "../../../public/Linear.png";
|
||||
import hubSpotIcon from "../../../public/HubSpot.png";
|
||||
@@ -1177,6 +1178,13 @@ export const GuruIcon = ({
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => <LogoIcon size={size} className={className} src={guruIcon} />;
|
||||
|
||||
export const RequestTrackerIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => (
|
||||
<LogoIcon size={size} className={className} src={requestTrackerIcon} />
|
||||
);
|
||||
|
||||
export const SalesforceIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
|
||||
55
web/src/components/initialSetup/search/NoSourcesModal.tsx
Normal file
55
web/src/components/initialSetup/search/NoSourcesModal.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Divider, Text } from "@tremor/react";
|
||||
import { Modal } from "../../Modal";
|
||||
import Link from "next/link";
|
||||
import { FiMessageSquare, FiShare2 } from "react-icons/fi";
|
||||
import { useContext, useState } from "react";
|
||||
import { SettingsContext } from "@/components/settings/SettingsProvider";
|
||||
|
||||
export function NoSourcesModal() {
|
||||
const settings = useContext(SettingsContext);
|
||||
const [isHidden, setIsHidden] = useState(
|
||||
!settings?.settings.search_page_enabled
|
||||
);
|
||||
|
||||
if (isHidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
width="max-w-3xl w-full"
|
||||
title="🧐 No sources connected"
|
||||
onOutsideClick={() => setIsHidden(true)}
|
||||
>
|
||||
<div className="text-base">
|
||||
<div>
|
||||
<Text>
|
||||
Before using Search you'll need to connect at least one source.
|
||||
Without any connected knowledge sources, there isn't anything
|
||||
to search over.
|
||||
</Text>
|
||||
<Link href="/admin/add-connector">
|
||||
<Button className="mt-3" size="xs" icon={FiShare2}>
|
||||
Connect a Source!
|
||||
</Button>
|
||||
</Link>
|
||||
<Divider />
|
||||
<div>
|
||||
<Text>
|
||||
Or, if you're looking for a pure ChatGPT-like experience
|
||||
without any organization specific knowledge, then you can head
|
||||
over to the Chat page and start chatting with Danswer right away!
|
||||
</Text>
|
||||
<Link href="/chat">
|
||||
<Button className="mt-3" size="xs" icon={FiMessageSquare}>
|
||||
Start Chatting!
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -58,10 +58,6 @@ export function _WelcomeModal({ user }: { user: User | null }) {
|
||||
{popup}
|
||||
|
||||
<Modal
|
||||
onOutsideClick={() => {
|
||||
setWelcomeFlowComplete();
|
||||
router.refresh();
|
||||
}}
|
||||
title={"Welcome to Danswer!"}
|
||||
width="w-full max-h-[900px] overflow-y-scroll max-w-3xl"
|
||||
>
|
||||
|
||||
@@ -26,17 +26,18 @@ export const ApiKeyModal = ({
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
title="Configure a Generative AI Model"
|
||||
title="Set an API Key!"
|
||||
width="max-w-3xl w-full"
|
||||
onOutsideClick={() => hide()}
|
||||
>
|
||||
<>
|
||||
<div className="mb-5 text-sm text-gray-700">
|
||||
Please provide an API Key – you can always change this or switch
|
||||
models later.
|
||||
Please provide an API Key below in order to start using Danswer – you
|
||||
can always change this later.
|
||||
<br />
|
||||
If you would rather look around first, you can{" "}
|
||||
If you'd rather look around first, you can
|
||||
<strong onClick={() => hide()} className="text-link cursor-pointer">
|
||||
{" "}
|
||||
skip this step
|
||||
</strong>
|
||||
.
|
||||
|
||||
@@ -44,7 +44,6 @@ import UnconfiguredProviderText from "../chat_search/UnconfiguredProviderText";
|
||||
import { DateRangePickerValue } from "@tremor/react";
|
||||
import { Tag } from "@/lib/types";
|
||||
import { isEqual } from "lodash";
|
||||
import { WelcomeModal } from "../initialSetup/welcome/WelcomeModalWrapper";
|
||||
|
||||
export type searchState =
|
||||
| "input"
|
||||
@@ -784,7 +783,6 @@ export const SearchSection = ({
|
||||
</div>
|
||||
|
||||
<UnconfiguredProviderText
|
||||
noSources={shouldDisplayNoSources}
|
||||
showConfigureAPIKey={() => setShowApiKeyModal(true)}
|
||||
/>
|
||||
|
||||
|
||||
@@ -124,9 +124,9 @@ export const CustomTooltip = ({
|
||||
!disabled &&
|
||||
createPortal(
|
||||
<div
|
||||
className={`min-w-8 fixed z-[1000] ${
|
||||
citation ? "max-w-[350px]" : "w-40"
|
||||
} ${large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"}
|
||||
className={`min-w-8 fixed z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${
|
||||
large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"
|
||||
}
|
||||
transform -translate-x-1/2 text-sm
|
||||
${
|
||||
light
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||
import { User, UserRole } from "@/lib/types";
|
||||
import { getCurrentUser } from "@/lib/user";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
interface UserContextType {
|
||||
user: User | null;
|
||||
@@ -15,39 +14,20 @@ interface UserContextType {
|
||||
|
||||
const UserContext = createContext<UserContextType | undefined>(undefined);
|
||||
|
||||
export function UserProvider({
|
||||
children,
|
||||
user,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
user: User | null;
|
||||
}) {
|
||||
const [upToDateUser, setUpToDateUser] = useState<User | null>(user);
|
||||
const [isLoadingUser, setIsLoadingUser] = useState(false);
|
||||
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
if (!posthog) return;
|
||||
|
||||
if (user?.id) {
|
||||
const identifyData: Record<string, any> = {
|
||||
email: user.email,
|
||||
};
|
||||
if (user.organization_name) {
|
||||
identifyData.organization_name = user.organization_name;
|
||||
}
|
||||
posthog.identify(user.id, identifyData);
|
||||
} else {
|
||||
posthog.reset();
|
||||
}
|
||||
}, [posthog, user]);
|
||||
export function UserProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoadingUser, setIsLoadingUser] = useState(true);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [isCurator, setIsCurator] = useState(false);
|
||||
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
setIsLoadingUser(true);
|
||||
const currentUser = await getCurrentUser();
|
||||
setUpToDateUser(currentUser);
|
||||
const user = await getCurrentUser();
|
||||
setUser(user);
|
||||
setIsAdmin(user?.role === UserRole.ADMIN);
|
||||
setIsCurator(
|
||||
user?.role === UserRole.CURATOR || user?.role == UserRole.GLOBAL_CURATOR
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching current user:", error);
|
||||
} finally {
|
||||
@@ -55,19 +35,17 @@ export function UserProvider({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUser();
|
||||
}, []);
|
||||
|
||||
const refreshUser = async () => {
|
||||
await fetchUser();
|
||||
};
|
||||
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
user: upToDateUser,
|
||||
isLoadingUser,
|
||||
refreshUser,
|
||||
isAdmin: upToDateUser?.role === UserRole.ADMIN,
|
||||
isCurator: upToDateUser?.role === UserRole.CURATOR,
|
||||
}}
|
||||
value={{ user, isLoadingUser, isAdmin, refreshUser, isCurator }}
|
||||
>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
|
||||
@@ -552,6 +552,11 @@ For example, specifying .*-support.* as a "channel" will cause the connector to
|
||||
],
|
||||
advanced_values: [],
|
||||
},
|
||||
requesttracker: {
|
||||
description: "Configure HubSpot connector",
|
||||
values: [],
|
||||
advanced_values: [],
|
||||
},
|
||||
hubspot: {
|
||||
description: "Configure HubSpot connector",
|
||||
values: [],
|
||||
@@ -1111,6 +1116,8 @@ export interface NotionConfig {
|
||||
|
||||
export interface HubSpotConfig {}
|
||||
|
||||
export interface RequestTrackerConfig {}
|
||||
|
||||
export interface Document360Config {
|
||||
workspace: string;
|
||||
categories?: string[];
|
||||
|
||||
@@ -106,6 +106,12 @@ export interface HubSpotCredentialJson {
|
||||
hubspot_access_token: string;
|
||||
}
|
||||
|
||||
export interface RequestTrackerCredentialJson {
|
||||
requesttracker_username: string;
|
||||
requesttracker_password: string;
|
||||
requesttracker_base_url: string;
|
||||
}
|
||||
|
||||
export interface Document360CredentialJson {
|
||||
portal_id: string;
|
||||
document360_api_token: string;
|
||||
@@ -218,6 +224,11 @@ export const credentialTemplates: Record<ValidSources, any> = {
|
||||
portal_id: "",
|
||||
document360_api_token: "",
|
||||
} as Document360CredentialJson,
|
||||
requesttracker: {
|
||||
requesttracker_username: "",
|
||||
requesttracker_password: "",
|
||||
requesttracker_base_url: "",
|
||||
} as RequestTrackerCredentialJson,
|
||||
loopio: {
|
||||
loopio_subdomain: "",
|
||||
loopio_client_id: "",
|
||||
@@ -360,6 +371,12 @@ export const credentialDisplayNames: Record<string, string> = {
|
||||
|
||||
// HubSpot
|
||||
hubspot_access_token: "HubSpot Access Token",
|
||||
|
||||
// Request Tracker
|
||||
requesttracker_username: "Request Tracker Username",
|
||||
requesttracker_password: "Request Tracker Password",
|
||||
requesttracker_base_url: "Request Tracker Base URL",
|
||||
|
||||
// Document360
|
||||
portal_id: "Document360 Portal ID",
|
||||
document360_api_token: "Document360 API Token",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
LoopioIcon,
|
||||
NotionIcon,
|
||||
ProductboardIcon,
|
||||
RequestTrackerIcon,
|
||||
R2Icon,
|
||||
SalesforceIcon,
|
||||
SharepointIcon,
|
||||
@@ -242,6 +243,12 @@ const SOURCE_METADATA_MAP: SourceMap = {
|
||||
category: SourceCategory.Wiki,
|
||||
docs: "https://docs.danswer.dev/connectors/mediawiki",
|
||||
},
|
||||
requesttracker: {
|
||||
icon: RequestTrackerIcon,
|
||||
displayName: "Request Tracker",
|
||||
category: SourceCategory.CustomerSupport,
|
||||
docs: "https://docs.danswer.dev/connectors/requesttracker",
|
||||
},
|
||||
clickup: {
|
||||
icon: ClickupIcon,
|
||||
displayName: "Clickup",
|
||||
|
||||
@@ -42,7 +42,6 @@ export interface User {
|
||||
current_token_created_at?: Date;
|
||||
current_token_expiry_length?: number;
|
||||
oidc_expiry?: Date;
|
||||
organization_name: string | null;
|
||||
}
|
||||
|
||||
export interface MinimalUserSnapshot {
|
||||
@@ -242,6 +241,7 @@ const validSources = [
|
||||
"linear",
|
||||
"hubspot",
|
||||
"document360",
|
||||
"requesttracker",
|
||||
"file",
|
||||
"google_sites",
|
||||
"loopio",
|
||||
|
||||
@@ -12,9 +12,7 @@ const eePaths = [
|
||||
"/admin/whitelabeling/:path*",
|
||||
"/admin/performance/custom-analytics/:path*",
|
||||
"/admin/standard-answer/:path*",
|
||||
...(process.env.NEXT_PUBLIC_CLOUD_ENABLED
|
||||
? ["/admin/cloud-settings/:path*"]
|
||||
: []),
|
||||
"/admin/cloud-settings/:path*",
|
||||
];
|
||||
|
||||
// removes the "/:path*" from the end
|
||||
@@ -46,5 +44,14 @@ export async function middleware(request: NextRequest) {
|
||||
|
||||
// Specify the paths that the middleware should run for
|
||||
export const config = {
|
||||
matcher: eePaths,
|
||||
matcher: [
|
||||
"/admin/groups/:path*",
|
||||
"/admin/api-key/:path*",
|
||||
"/admin/performance/usage/:path*",
|
||||
"/admin/performance/query-history/:path*",
|
||||
"/admin/whitelabeling/:path*",
|
||||
"/admin/performance/custom-analytics/:path*",
|
||||
"/admin/standard-answer/:path*",
|
||||
"/admin/cloud-settings/:path*",
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user