From d2b58bdb4039209be27f180d6f25004f82fb5b61 Mon Sep 17 00:00:00 2001 From: Art Matsak Date: Tue, 28 May 2024 17:39:02 +0200 Subject: [PATCH 01/44] Fix DISABLE_LLM_CHOOSE_SEARCH being ignored (#1523) --- backend/danswer/secondary_llm_flows/choose_search.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/danswer/secondary_llm_flows/choose_search.py b/backend/danswer/secondary_llm_flows/choose_search.py index df3597d5641..5016cf055bc 100644 --- a/backend/danswer/secondary_llm_flows/choose_search.py +++ b/backend/danswer/secondary_llm_flows/choose_search.py @@ -3,6 +3,7 @@ from langchain.schema import SystemMessage from danswer.chat.chat_utils import combine_message_chain +from danswer.configs.chat_configs import DISABLE_LLM_CHOOSE_SEARCH from danswer.configs.model_configs import GEN_AI_HISTORY_CUTOFF from danswer.db.models import ChatMessage from danswer.llm.answering.models import PreviousMessage @@ -26,8 +27,8 @@ def check_if_need_search_multi_message( history: list[ChatMessage], llm: LLM, ) -> bool: - # Always start with a retrieval - if not history: + # Retrieve on start or when choosing is globally disabled + if not history or DISABLE_LLM_CHOOSE_SEARCH: return True prompt_msgs: list[BaseMessage] = [SystemMessage(content=REQUIRE_SEARCH_SYSTEM_MSG)] @@ -65,6 +66,10 @@ def _get_search_messages( return messages + # Choosing is globally disabled, use search + if DISABLE_LLM_CHOOSE_SEARCH: + return True + history_str = combine_message_chain( messages=history, token_limit=GEN_AI_HISTORY_CUTOFF ) From 44d57f1b53dce2a9a19a8f0a361c0289f3f9d4c6 Mon Sep 17 00:00:00 2001 From: Yuhong Sun Date: Tue, 28 May 2024 11:36:02 -0700 Subject: [PATCH 02/44] Reenable force search (#1531) --- backend/danswer/chat/process_message.py | 2 ++ backend/danswer/chat/prompts.yaml | 2 +- backend/danswer/prompts/prompt_utils.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/danswer/chat/process_message.py b/backend/danswer/chat/process_message.py index 06ac84fa061..93c07185544 100644 --- a/backend/danswer/chat/process_message.py +++ b/backend/danswer/chat/process_message.py @@ -14,6 +14,7 @@ from danswer.chat.models import QADocsResponse from danswer.chat.models import StreamingError from danswer.configs.chat_configs import CHAT_TARGET_CHUNK_PERCENTAGE +from danswer.configs.chat_configs import DISABLE_LLM_CHOOSE_SEARCH from danswer.configs.chat_configs import MAX_CHUNKS_FED_TO_CHAT from danswer.configs.constants import MessageType from danswer.db.chat import attach_files_to_chat_message @@ -134,6 +135,7 @@ def _check_should_force_search( and new_msg_req.retrieval_options.run_search == OptionalSearchSetting.ALWAYS ) or new_msg_req.search_doc_ids + or DISABLE_LLM_CHOOSE_SEARCH ): args = ( {"query": new_msg_req.query_override} diff --git a/backend/danswer/chat/prompts.yaml b/backend/danswer/chat/prompts.yaml index cdf7a1da591..474327f046c 100644 --- a/backend/danswer/chat/prompts.yaml +++ b/backend/danswer/chat/prompts.yaml @@ -22,7 +22,7 @@ prompts: I have not read or seen any of the documents and do not want to read them. - If there are no relevant documents, refer to the chat history and existing knowledge. + If there are no relevant documents, refer to the chat history and your internal knowledge. # Inject a statement at the end of system prompt to inform the LLM of the current date/time # If the DANSWER_DATETIME_REPLACEMENT is set, the date/time is inserted there instead # Format looks like: "October 16, 2023 14:30" diff --git a/backend/danswer/prompts/prompt_utils.py b/backend/danswer/prompts/prompt_utils.py index 1e9316d2b71..9dc939eb170 100644 --- a/backend/danswer/prompts/prompt_utils.py +++ b/backend/danswer/prompts/prompt_utils.py @@ -39,7 +39,7 @@ def add_time_to_system_prompt(system_prompt: str) -> str: if DANSWER_DATETIME_REPLACEMENT in system_prompt: return system_prompt.replace( DANSWER_DATETIME_REPLACEMENT, - get_current_llm_day_time(full_sentence=False, include_day_of_week=False), + get_current_llm_day_time(full_sentence=False, include_day_of_week=True), ) if system_prompt: From fbdf8822994d826e166227c4e5aaad9e2303cec3 Mon Sep 17 00:00:00 2001 From: Matthieu Boret Date: Wed, 22 May 2024 12:30:02 +0200 Subject: [PATCH 03/44] fix sharepoint connector missing objects --- backend/danswer/connectors/sharepoint/connector.py | 2 +- backend/requirements/default.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/danswer/connectors/sharepoint/connector.py b/backend/danswer/connectors/sharepoint/connector.py index d0e98f2fd61..0c7497d0ceb 100644 --- a/backend/danswer/connectors/sharepoint/connector.py +++ b/backend/danswer/connectors/sharepoint/connector.py @@ -107,7 +107,7 @@ def get_all_driveitem_objects( site_list_objects = site_object.lists.get().execute_query() for site_list_object in site_list_objects: try: - query = site_list_object.drive.root.get_files(True) + query = site_list_object.drive.root.get_files(True, 1000) if filter_str: query = query.filter(filter_str) driveitems = query.execute_query() diff --git a/backend/requirements/default.txt b/backend/requirements/default.txt index 865fab2382b..6052624ad20 100644 --- a/backend/requirements/default.txt +++ b/backend/requirements/default.txt @@ -33,7 +33,7 @@ llama-index==0.9.45 Mako==1.2.4 msal==1.26.0 nltk==3.8.1 -Office365-REST-Python-Client==2.5.4 +Office365-REST-Python-Client==2.5.9 oauthlib==3.2.2 openai==1.14.3 openpyxl==3.1.2 From b690ae05b42634946de3861b2fb13d9f805635af Mon Sep 17 00:00:00 2001 From: Weves Date: Sun, 26 May 2024 18:27:05 -0700 Subject: [PATCH 04/44] Add assistant gallery --- ...902_add_chosen_assistants_to_user_table.py | 27 ++ backend/danswer/auth/noauth_user.py | 40 ++ backend/danswer/db/chat.py | 20 + backend/danswer/db/models.py | 13 + backend/danswer/db/persona.py | 34 ++ backend/danswer/server/auth_check.py | 2 +- .../danswer/server/features/persona/api.py | 22 ++ .../danswer/server/features/persona/models.py | 7 +- backend/danswer/server/manage/models.py | 21 + backend/danswer/server/manage/users.py | 80 +++- .../app/admin/assistants/AssistantEditor.tsx | 39 +- web/src/app/admin/assistants/interfaces.ts | 2 +- .../app/assistants/AssistantSharedStatus.tsx | 62 +++ .../app/assistants/AssistantsPageTitle.tsx | 18 + web/src/app/assistants/LargeBackButton.tsx | 16 + web/src/app/assistants/NavigationButton.tsx | 25 ++ web/src/app/assistants/ToolsDisplay.tsx | 31 ++ web/src/app/assistants/edit/[id]/page.tsx | 10 +- .../assistants/gallery/AssistantsGallery.tsx | 205 ++++++++++ web/src/app/assistants/gallery/page.tsx | 81 ++++ .../assistants/mine/AssistantSharingModal.tsx | 227 +++++++++++ .../app/assistants/mine/AssistantsList.tsx | 367 ++++++++++++++++++ web/src/app/assistants/mine/page.tsx | 82 ++++ web/src/app/assistants/new/page.tsx | 11 +- web/src/app/chat/ChatIntro.tsx | 252 ++++-------- web/src/app/chat/ChatPage.tsx | 27 +- web/src/app/chat/ChatPersonaSelector.tsx | 4 +- .../modal/configuration/AssistantsTab.tsx | 95 +++-- .../configuration/ConfigurationModal.tsx | 2 +- web/src/app/chat/page.tsx | 185 +-------- .../app/chat/sessionSidebar/AssistantsTab.tsx | 3 +- .../app/chat/sessionSidebar/ChatSidebar.tsx | 11 +- web/src/components/Bubble.tsx | 11 +- web/src/components/Dropdown.tsx | 18 +- web/src/components/UserDropdown.tsx | 26 +- .../components/assistants/AssistantIcon.tsx | 28 ++ web/src/components/popover/DefaultPopover.tsx | 59 +++ web/src/lib/assistants/checkOwnership.ts | 19 + web/src/lib/assistants/orderAssistants.ts | 28 ++ web/src/lib/assistants/shareAssistant.ts | 59 +++ .../assistants/updateAssistantPreferences.ts | 62 +++ .../lib/chat/fetchAssistantsGalleryData.ts | 0 web/src/lib/chat/fetchChatData.ts | 202 ++++++++++ web/src/lib/types.ts | 5 + web/src/lib/user.ts | 6 +- web/src/lib/userSS.ts | 2 +- 46 files changed, 2072 insertions(+), 474 deletions(-) create mode 100644 backend/alembic/versions/a3bfd0d64902_add_chosen_assistants_to_user_table.py create mode 100644 backend/danswer/auth/noauth_user.py create mode 100644 web/src/app/assistants/AssistantSharedStatus.tsx create mode 100644 web/src/app/assistants/AssistantsPageTitle.tsx create mode 100644 web/src/app/assistants/LargeBackButton.tsx create mode 100644 web/src/app/assistants/NavigationButton.tsx create mode 100644 web/src/app/assistants/ToolsDisplay.tsx create mode 100644 web/src/app/assistants/gallery/AssistantsGallery.tsx create mode 100644 web/src/app/assistants/gallery/page.tsx create mode 100644 web/src/app/assistants/mine/AssistantSharingModal.tsx create mode 100644 web/src/app/assistants/mine/AssistantsList.tsx create mode 100644 web/src/app/assistants/mine/page.tsx create mode 100644 web/src/components/assistants/AssistantIcon.tsx create mode 100644 web/src/components/popover/DefaultPopover.tsx create mode 100644 web/src/lib/assistants/checkOwnership.ts create mode 100644 web/src/lib/assistants/orderAssistants.ts create mode 100644 web/src/lib/assistants/shareAssistant.ts create mode 100644 web/src/lib/assistants/updateAssistantPreferences.ts create mode 100644 web/src/lib/chat/fetchAssistantsGalleryData.ts create mode 100644 web/src/lib/chat/fetchChatData.ts diff --git a/backend/alembic/versions/a3bfd0d64902_add_chosen_assistants_to_user_table.py b/backend/alembic/versions/a3bfd0d64902_add_chosen_assistants_to_user_table.py new file mode 100644 index 00000000000..289bc8e6258 --- /dev/null +++ b/backend/alembic/versions/a3bfd0d64902_add_chosen_assistants_to_user_table.py @@ -0,0 +1,27 @@ +"""Add chosen_assistants to User table + +Revision ID: a3bfd0d64902 +Revises: ec85f2b3c544 +Create Date: 2024-05-26 17:22:24.834741 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "a3bfd0d64902" +down_revision = "ec85f2b3c544" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "user", + sa.Column("chosen_assistants", postgresql.ARRAY(sa.Integer()), nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("user", "chosen_assistants") diff --git a/backend/danswer/auth/noauth_user.py b/backend/danswer/auth/noauth_user.py new file mode 100644 index 00000000000..4744c4a6488 --- /dev/null +++ b/backend/danswer/auth/noauth_user.py @@ -0,0 +1,40 @@ +from collections.abc import Mapping +from typing import Any +from typing import cast + +from danswer.auth.schemas import UserRole +from danswer.dynamic_configs.store import ConfigNotFoundError +from danswer.dynamic_configs.store import DynamicConfigStore +from danswer.server.manage.models import UserInfo +from danswer.server.manage.models import UserPreferences + + +NO_AUTH_USER_PREFERENCES_KEY = "no_auth_user_preferences" + + +def set_no_auth_user_preferences( + store: DynamicConfigStore, preferences: UserPreferences +) -> None: + store.store(NO_AUTH_USER_PREFERENCES_KEY, preferences.dict()) + + +def load_no_auth_user_preferences(store: DynamicConfigStore) -> UserPreferences: + try: + preferences_data = cast( + Mapping[str, Any], store.load(NO_AUTH_USER_PREFERENCES_KEY) + ) + return UserPreferences(**preferences_data) + except ConfigNotFoundError: + return UserPreferences(chosen_assistants=None) + + +def fetch_no_auth_user(store: DynamicConfigStore) -> UserInfo: + return UserInfo( + id="__no_auth_user__", + email="anonymous@danswer.ai", + is_active=True, + is_superuser=False, + is_verified=True, + role=UserRole.ADMIN, + preferences=load_no_auth_user_preferences(store), + ) diff --git a/backend/danswer/db/chat.py b/backend/danswer/db/chat.py index b4a0b9bf29d..f0361da30d4 100644 --- a/backend/danswer/db/chat.py +++ b/backend/danswer/db/chat.py @@ -2,6 +2,7 @@ from functools import lru_cache from uuid import UUID +from fastapi import HTTPException from sqlalchemy import delete from sqlalchemy import func from sqlalchemy import not_ @@ -398,6 +399,23 @@ def get_persona_by_id( return persona +def check_user_can_edit_persona(user: User | None, persona: Persona) -> None: + # if user is None, assume that no-auth is turned on + if user is None: + return + + # admins can edit everything + if user.role == UserRole.ADMIN: + return + + # otherwise, make sure user owns persona + if persona.user_id != user.id: + raise HTTPException( + status_code=403, + detail=f"User not authorized to edit persona with ID {persona.id}", + ) + + def get_prompts_by_ids(prompt_ids: list[int], db_session: Session) -> Sequence[Prompt]: """Unsafe, can fetch prompts from all users""" if not prompt_ids: @@ -543,6 +561,8 @@ def upsert_persona( if not default_persona and persona.default_persona: raise ValueError("Cannot update default persona with non-default.") + check_user_can_edit_persona(user=user, persona=persona) + persona.name = name persona.description = description persona.num_chunks = num_chunks diff --git a/backend/danswer/db/models.py b/backend/danswer/db/models.py index a77223394f7..3b4b67f0947 100644 --- a/backend/danswer/db/models.py +++ b/backend/danswer/db/models.py @@ -108,6 +108,19 @@ class User(SQLAlchemyBaseUserTableUUID, Base): role: Mapped[UserRole] = mapped_column( Enum(UserRole, native_enum=False, default=UserRole.BASIC) ) + + """ + Preferences probably should be in a separate table at some point, but for now + putting here for simpicity + """ + + # if specified, controls the assistants that are shown to the user + their order + # if not specified, all assistants are shown + chosen_assistants: Mapped[list[int]] = mapped_column( + postgresql.ARRAY(Integer), nullable=True + ) + + # relationships credentials: Mapped[list["Credential"]] = relationship( "Credential", back_populates="user", lazy="joined" ) diff --git a/backend/danswer/db/persona.py b/backend/danswer/db/persona.py index 6fa65135dca..db543e0b97f 100644 --- a/backend/danswer/db/persona.py +++ b/backend/danswer/db/persona.py @@ -4,6 +4,7 @@ from sqlalchemy import select from sqlalchemy.orm import Session +from danswer.db.chat import check_user_can_edit_persona from danswer.db.chat import get_prompts_by_ids from danswer.db.chat import upsert_persona from danswer.db.document_set import get_document_sets_by_ids @@ -97,5 +98,38 @@ def create_update_persona( return PersonaSnapshot.from_model(persona) +def update_persona_shared_users( + persona_id: int, + user_ids: list[UUID], + user: User | None, + db_session: Session, +) -> None: + """Simplified version of `create_update_persona` which only touches the + accessibility rather than any of the logic (e.g. prompt, connected data sources, + etc.).""" + persona = fetch_persona_by_id(db_session=db_session, persona_id=persona_id) + if not persona: + raise HTTPException( + status_code=404, detail=f"Persona with ID {persona_id} not found" + ) + + check_user_can_edit_persona(user=user, persona=persona) + + if persona.is_public: + raise HTTPException(status_code=400, detail="Cannot share public persona") + + versioned_make_persona_private = fetch_versioned_implementation( + "danswer.db.persona", "make_persona_private" + ) + + # Privatize Persona + versioned_make_persona_private( + persona_id=persona_id, + user_ids=user_ids, + group_ids=None, + db_session=db_session, + ) + + def fetch_persona_by_id(db_session: Session, persona_id: int) -> Persona | None: return db_session.scalar(select(Persona).where(Persona.id == persona_id)) diff --git a/backend/danswer/server/auth_check.py b/backend/danswer/server/auth_check.py index 9fbc973bdc6..53ef572daa3 100644 --- a/backend/danswer/server/auth_check.py +++ b/backend/danswer/server/auth_check.py @@ -17,7 +17,7 @@ ("/docs/oauth2-redirect", {"GET", "HEAD"}), ("/redoc", {"GET", "HEAD"}), # should always be callable, will just return 401 if not authenticated - ("/manage/me", {"GET"}), + ("/me", {"GET"}), # just returns 200 to validate that the server is up ("/health", {"GET"}), # just returns auth type, needs to be accessible before the user is logged diff --git a/backend/danswer/server/features/persona/api.py b/backend/danswer/server/features/persona/api.py index 65303e4b782..f69550fcdda 100644 --- a/backend/danswer/server/features/persona/api.py +++ b/backend/danswer/server/features/persona/api.py @@ -1,3 +1,5 @@ +from uuid import UUID + from fastapi import APIRouter from fastapi import Depends from pydantic import BaseModel @@ -14,6 +16,7 @@ from danswer.db.engine import get_session from danswer.db.models import User from danswer.db.persona import create_update_persona +from danswer.db.persona import update_persona_shared_users 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 PersonaSnapshot @@ -119,6 +122,25 @@ def update_persona( ) +class PersonaShareRequest(BaseModel): + user_ids: list[UUID] + + +@basic_router.patch("/{persona_id}/share") +def share_persona( + persona_id: int, + persona_share_request: PersonaShareRequest, + user: User | None = Depends(current_user), + db_session: Session = Depends(get_session), +) -> None: + update_persona_shared_users( + persona_id=persona_id, + user_ids=persona_share_request.user_ids, + user=user, + db_session=db_session, + ) + + @basic_router.delete("/{persona_id}") def delete_persona( persona_id: int, diff --git a/backend/danswer/server/features/persona/models.py b/backend/danswer/server/features/persona/models.py index 46ccba760aa..aee39e72af0 100644 --- a/backend/danswer/server/features/persona/models.py +++ b/backend/danswer/server/features/persona/models.py @@ -53,7 +53,7 @@ class PersonaSnapshot(BaseModel): prompts: list[PromptSnapshot] tools: list[ToolSnapshot] document_sets: list[DocumentSet] - users: list[UUID] + users: list[MinimalUserSnapshot] groups: list[int] @classmethod @@ -92,7 +92,10 @@ def from_model( DocumentSet.from_model(document_set_model) for document_set_model in persona.document_sets ], - users=[user.id for user in persona.users], + users=[ + MinimalUserSnapshot(id=user.id, email=user.email) + for user in persona.users + ], groups=[user_group.id for user_group in persona.groups], ) diff --git a/backend/danswer/server/manage/models.py b/backend/danswer/server/manage/models.py index df0f2ec477c..8797913f44f 100644 --- a/backend/danswer/server/manage/models.py +++ b/backend/danswer/server/manage/models.py @@ -1,4 +1,5 @@ from typing import Any +from typing import TYPE_CHECKING from pydantic import BaseModel from pydantic import root_validator @@ -14,6 +15,9 @@ from danswer.indexing.models import EmbeddingModelDetail from danswer.server.features.persona.models import PersonaSnapshot +if TYPE_CHECKING: + from danswer.db.models import User as UserModel + class VersionResponse(BaseModel): backend_version: str @@ -26,6 +30,10 @@ class AuthTypeResponse(BaseModel): requires_verification: bool +class UserPreferences(BaseModel): + chosen_assistants: list[int] | None + + class UserInfo(BaseModel): id: str email: str @@ -33,6 +41,19 @@ class UserInfo(BaseModel): is_superuser: bool is_verified: bool role: UserRole + preferences: UserPreferences + + @classmethod + def from_model(cls, user: "UserModel") -> "UserInfo": + return cls( + id=str(user.id), + email=user.email, + is_active=user.is_active, + is_superuser=user.is_superuser, + is_verified=user.is_verified, + role=user.role, + preferences=(UserPreferences(chosen_assistants=user.chosen_assistants)), + ) class UserByEmail(BaseModel): diff --git a/backend/danswer/server/manage/users.py b/backend/danswer/server/manage/users.py index 7258a8564f3..a643f4e752f 100644 --- a/backend/danswer/server/manage/users.py +++ b/backend/danswer/server/manage/users.py @@ -2,26 +2,33 @@ from fastapi import Depends from fastapi import HTTPException from fastapi import status +from pydantic import BaseModel +from sqlalchemy import update from sqlalchemy.orm import Session +from danswer.auth.noauth_user import fetch_no_auth_user +from danswer.auth.noauth_user import set_no_auth_user_preferences from danswer.auth.schemas import UserRead from danswer.auth.schemas import UserRole from danswer.auth.users import current_admin_user from danswer.auth.users import current_user -from danswer.auth.users import get_display_email from danswer.auth.users import optional_user +from danswer.configs.app_configs import AUTH_TYPE +from danswer.configs.constants import AuthType from danswer.db.engine import get_session from danswer.db.models import User from danswer.db.users import get_user_by_email from danswer.db.users import list_users +from danswer.dynamic_configs.factory import get_dynamic_config_store from danswer.server.manage.models import UserByEmail from danswer.server.manage.models import UserInfo from danswer.server.manage.models import UserRoleResponse +from danswer.server.models import MinimalUserSnapshot -router = APIRouter(prefix="/manage") +router = APIRouter() -@router.patch("/promote-user-to-admin") +@router.patch("/manage/promote-user-to-admin") def promote_admin( user_email: UserByEmail, _: User = Depends(current_admin_user), @@ -38,7 +45,7 @@ def promote_admin( db_session.commit() -@router.patch("/demote-admin-to-basic") +@router.patch("/manage/demote-admin-to-basic") async def demote_admin( user_email: UserByEmail, user: User = Depends(current_admin_user), @@ -60,7 +67,7 @@ async def demote_admin( db_session.commit() -@router.get("/users") +@router.get("/manage/users") def list_all_users( _: User | None = Depends(current_admin_user), db_session: Session = Depends(get_session), @@ -69,7 +76,19 @@ def list_all_users( return [UserRead.from_orm(user) for user in users] -@router.get("/get-user-role", response_model=UserRoleResponse) +"""Endpoints for all""" + + +@router.get("/users") +def list_all_users_basic_info( + _: User | None = Depends(current_user), + db_session: Session = Depends(get_session), +) -> list[MinimalUserSnapshot]: + users = list_users(db_session) + return [MinimalUserSnapshot(id=user.id, email=user.email) for user in users] + + +@router.get("/get-user-role") async def get_user_role(user: User = Depends(current_user)) -> UserRoleResponse: if user is None: raise ValueError("Invalid or missing user.") @@ -77,20 +96,53 @@ async def get_user_role(user: User = Depends(current_user)) -> UserRoleResponse: @router.get("/me") -def verify_user_logged_in(user: User | None = Depends(optional_user)) -> UserInfo: +def verify_user_logged_in( + user: User | None = Depends(optional_user), +) -> UserInfo: # NOTE: this does not use `current_user` / `current_admin_user` because we don't want # to enforce user verification here - the frontend always wants to get the info about # the current user regardless of if they are currently verified if user is None: + # if auth type is disabled, return a dummy user with preferences from + # the key-value store + if AUTH_TYPE == AuthType.DISABLED: + store = get_dynamic_config_store() + return fetch_no_auth_user(store) + raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="User Not Authenticated" ) - return UserInfo( - id=str(user.id), - email=get_display_email(user.email, space_less=True), - is_active=user.is_active, - is_superuser=user.is_superuser, - is_verified=user.is_verified, - role=user.role, + return UserInfo.from_model(user) + + +"""APIs to adjust user preferences""" + + +class ChosenAssistantsRequest(BaseModel): + chosen_assistants: list[int] + + +@router.patch("/user/assistant-list") +def update_user_assistant_list( + request: ChosenAssistantsRequest, + user: User | None = Depends(current_user), + db_session: Session = Depends(get_session), +) -> None: + if user is None: + if AUTH_TYPE == AuthType.DISABLED: + store = get_dynamic_config_store() + + no_auth_user = fetch_no_auth_user(store) + no_auth_user.preferences.chosen_assistants = request.chosen_assistants + set_no_auth_user_preferences(store, no_auth_user.preferences) + return + else: + raise RuntimeError("This should never happen") + + db_session.execute( + update(User) + .where(User.id == user.id) # type: ignore + .values(chosen_assistants=request.chosen_assistants) ) + db_session.commit() diff --git a/web/src/app/admin/assistants/AssistantEditor.tsx b/web/src/app/admin/assistants/AssistantEditor.tsx index 8c1f01973b2..f792ebce751 100644 --- a/web/src/app/admin/assistants/AssistantEditor.tsx +++ b/web/src/app/admin/assistants/AssistantEditor.tsx @@ -34,6 +34,8 @@ import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelec import { FullLLMProvider } from "../models/llm/interfaces"; import { Option } from "@/components/Dropdown"; import { ToolSnapshot } from "@/lib/tools/interfaces"; +import { checkUserIsNoAuthUser } from "@/lib/user"; +import { addAssistantToList } from "@/lib/assistants/updateAssistantPreferences"; function findSearchTool(tools: ToolSnapshot[]) { return tools.find((tool) => tool.in_code_tool_id === "SearchTool"); @@ -44,8 +46,6 @@ function findImageGenerationTool(tools: ToolSnapshot[]) { } function checkLLMSupportsImageGeneration(provider: string, model: string) { - console.log(provider); - console.log(model); return provider === "openai" && model === "gpt-4-turbo"; } @@ -68,6 +68,7 @@ export function AssistantEditor({ redirectType, llmProviders, tools, + shouldAddAssistantToUserPreferences, }: { existingPersona?: Persona | null; ccPairs: CCPairBasicInfo[]; @@ -77,6 +78,7 @@ export function AssistantEditor({ redirectType: SuccessfulPersonaUpdateRedirectType; llmProviders: FullLLMProvider[]; tools: ToolSnapshot[]; + shouldAddAssistantToUserPreferences?: boolean; }) { const router = useRouter(); const { popup, setPopup } = usePopup(); @@ -288,7 +290,8 @@ export function AssistantEditor({ existingPromptId: existingPrompt?.id, ...values, num_chunks: numChunks, - users: user ? [user.id] : undefined, + users: + user && !checkUserIsNoAuthUser(user.id) ? [user.id] : undefined, groups, tool_ids: tools, }); @@ -296,7 +299,8 @@ export function AssistantEditor({ [promptResponse, personaResponse] = await createPersona({ ...values, num_chunks: numChunks, - users: user ? [user.id] : undefined, + users: + user && !checkUserIsNoAuthUser(user.id) ? [user.id] : undefined, groups, tool_ids: tools, }); @@ -319,12 +323,33 @@ export function AssistantEditor({ }); formikHelpers.setSubmitting(false); } else { + const assistant = await personaResponse.json(); + const assistantId = assistant.id; + if ( + shouldAddAssistantToUserPreferences && + user?.preferences?.chosen_assistants + ) { + const success = await addAssistantToList( + assistantId, + user.preferences.chosen_assistants + ); + if (success) { + setPopup({ + message: `"${assistant.name}" has been added to your list.`, + type: "success", + }); + router.refresh(); + } else { + setPopup({ + message: `"${assistant.name}" could not be added to your list.`, + type: "error", + }); + } + } router.push( redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN ? `/admin/assistants?u=${Date.now()}` - : `/chat?assistantId=${ - ((await personaResponse.json()) as Persona).id - }` + : `/chat?assistantId=${assistantId}` ); } }} diff --git a/web/src/app/admin/assistants/interfaces.ts b/web/src/app/admin/assistants/interfaces.ts index fada33191f9..0a06ac4cc82 100644 --- a/web/src/app/admin/assistants/interfaces.ts +++ b/web/src/app/admin/assistants/interfaces.ts @@ -36,6 +36,6 @@ export interface Persona { llm_model_version_override?: string; starter_messages: StarterMessage[] | null; default_persona: boolean; - users: string[]; + users: MinimalUserSnapshot[]; groups: number[]; } diff --git a/web/src/app/assistants/AssistantSharedStatus.tsx b/web/src/app/assistants/AssistantSharedStatus.tsx new file mode 100644 index 00000000000..c5127c87e91 --- /dev/null +++ b/web/src/app/assistants/AssistantSharedStatus.tsx @@ -0,0 +1,62 @@ +import { User } from "@/lib/types"; +import { Persona } from "../admin/assistants/interfaces"; +import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership"; +import { FiLock, FiUnlock } from "react-icons/fi"; + +export function AssistantSharedStatusDisplay({ + assistant, + user, +}: { + assistant: Persona; + user: User | null; +}) { + const isOwnedByUser = checkUserOwnsAssistant(user, assistant); + + const assistantSharedUsersWithoutOwner = assistant.users?.filter( + (u) => u.id !== assistant.owner?.id + ); + + if (assistant.is_public) { + return ( +
+ + Public +
+ ); + } + + if (assistantSharedUsersWithoutOwner.length > 0) { + return ( +
+ + {isOwnedByUser ? ( + `Shared with: ${ + assistantSharedUsersWithoutOwner.length <= 4 + ? assistantSharedUsersWithoutOwner.map((u) => u.email).join(", ") + : `${assistantSharedUsersWithoutOwner + .slice(0, 4) + .map((u) => u.email) + .join(", ")} and ${assistant.users.length - 4} others...` + }` + ) : ( +
+ {assistant.owner ? ( +
+ Shared with you by {assistant.owner?.email} +
+ ) : ( + "Shared with you" + )} +
+ )} +
+ ); + } + + return ( +
+ + Private +
+ ); +} diff --git a/web/src/app/assistants/AssistantsPageTitle.tsx b/web/src/app/assistants/AssistantsPageTitle.tsx new file mode 100644 index 00000000000..5bcdea7ea66 --- /dev/null +++ b/web/src/app/assistants/AssistantsPageTitle.tsx @@ -0,0 +1,18 @@ +export function AssistantsPageTitle({ + children, +}: { + children: JSX.Element | string; +}) { + return ( +

+ {children} +

+ ); +} diff --git a/web/src/app/assistants/LargeBackButton.tsx b/web/src/app/assistants/LargeBackButton.tsx new file mode 100644 index 00000000000..3b52f36c569 --- /dev/null +++ b/web/src/app/assistants/LargeBackButton.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { FiChevronLeft } from "react-icons/fi"; + +export function LargeBackButton() { + const router = useRouter(); + return ( +
router.back()}> + +
+ ); +} diff --git a/web/src/app/assistants/NavigationButton.tsx b/web/src/app/assistants/NavigationButton.tsx new file mode 100644 index 00000000000..223c8fbbdf2 --- /dev/null +++ b/web/src/app/assistants/NavigationButton.tsx @@ -0,0 +1,25 @@ +export function NavigationButton({ + children, +}: { + children: JSX.Element | string; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/web/src/app/assistants/ToolsDisplay.tsx b/web/src/app/assistants/ToolsDisplay.tsx new file mode 100644 index 00000000000..c30025dc6e5 --- /dev/null +++ b/web/src/app/assistants/ToolsDisplay.tsx @@ -0,0 +1,31 @@ +import { Bubble } from "@/components/Bubble"; +import { ToolSnapshot } from "@/lib/tools/interfaces"; +import { FiImage, FiSearch } from "react-icons/fi"; + +export function ToolsDisplay({ tools }: { tools: ToolSnapshot[] }) { + return ( +
+ {tools.map((tool) => { + let toolName = tool.name; + let toolIcon = null; + + if (tool.name === "SearchTool") { + toolName = "Search"; + toolIcon = ; + } else if (tool.name === "ImageGenerationTool") { + toolName = "Image Generation"; + toolIcon = ; + } + + return ( + +
+ {toolIcon} + {toolName} +
+
+ ); + })} +
+ ); +} diff --git a/web/src/app/assistants/edit/[id]/page.tsx b/web/src/app/assistants/edit/[id]/page.tsx index db4a54d6780..2b56bb00e48 100644 --- a/web/src/app/assistants/edit/[id]/page.tsx +++ b/web/src/app/assistants/edit/[id]/page.tsx @@ -1,12 +1,11 @@ import { ErrorCallout } from "@/components/ErrorCallout"; import { Card, Text, Title } from "@tremor/react"; import { HeaderWrapper } from "@/components/header/HeaderWrapper"; -import { FiChevronLeft } from "react-icons/fi"; -import Link from "next/link"; import { AssistantEditor } from "@/app/admin/assistants/AssistantEditor"; import { SuccessfulPersonaUpdateRedirectType } from "@/app/admin/assistants/enums"; import { fetchAssistantEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS"; import { DeletePersonaButton } from "@/app/admin/assistants/[id]/DeletePersonaButton"; +import { LargeBackButton } from "../../LargeBackButton"; export default async function Page({ params }: { params: { id: string } }) { const [values, error] = await fetchAssistantEditorInfoSS(params.id); @@ -52,12 +51,7 @@ export default async function Page({ params }: { params: { id: string } }) {
- - - +

Edit Assistant

diff --git a/web/src/app/assistants/gallery/AssistantsGallery.tsx b/web/src/app/assistants/gallery/AssistantsGallery.tsx new file mode 100644 index 00000000000..4b9eaff718b --- /dev/null +++ b/web/src/app/assistants/gallery/AssistantsGallery.tsx @@ -0,0 +1,205 @@ +"use client"; + +import { Persona } from "@/app/admin/assistants/interfaces"; +import { AssistantIcon } from "@/components/assistants/AssistantIcon"; +import { User } from "@/lib/types"; +import { Button } from "@tremor/react"; +import Link from "next/link"; +import { useState } from "react"; +import { FiMinus, FiPlus, FiX } from "react-icons/fi"; +import { NavigationButton } from "../NavigationButton"; +import { AssistantsPageTitle } from "../AssistantsPageTitle"; +import { + addAssistantToList, + removeAssistantFromList, +} from "@/lib/assistants/updateAssistantPreferences"; +import { usePopup } from "@/components/admin/connectors/Popup"; +import { useRouter } from "next/navigation"; +import { ToolsDisplay } from "../ToolsDisplay"; + +export function AssistantsGallery({ + assistants, + user, +}: { + assistants: Persona[]; + user: User | null; +}) { + function filterAssistants(assistants: Persona[], query: string): Persona[] { + return assistants.filter( + (assistant) => + assistant.name.toLowerCase().includes(query.toLowerCase()) || + assistant.description.toLowerCase().includes(query.toLowerCase()) + ); + } + + const router = useRouter(); + + const [searchQuery, setSearchQuery] = useState(""); + const { popup, setPopup } = usePopup(); + + const allAssistantIds = assistants.map((assistant) => assistant.id); + const filteredAssistants = filterAssistants(assistants, searchQuery); + + return ( + <> + {popup} +
+ Assistant Gallery +
+ + View Your Assistants + +
+ +

+ Discover and create custom assistants that combine instructions, extra + knowledge, and any combination of tools. +

+ +
+ setSearchQuery(e.target.value)} + className=" + w-full + p-2 + border + border-gray-300 + rounded + focus:outline-none + focus:ring-2 + focus:ring-blue-500 + " + /> +
+
+ {filteredAssistants.map((assistant) => ( +
+
+ +

+ {assistant.name} +

+ {user && ( +
+ {!user.preferences?.chosen_assistants || + user.preferences?.chosen_assistants?.includes( + assistant.id + ) ? ( + + ) : ( + + )} +
+ )} +
+ {assistant.tools.length > 0 && ( + + )} +

{assistant.description}

+

+ Author: {assistant.owner?.email || "Danswer"} +

+
+ ))} +
+
+ + ); +} diff --git a/web/src/app/assistants/gallery/page.tsx b/web/src/app/assistants/gallery/page.tsx new file mode 100644 index 00000000000..c4b9f46f6d0 --- /dev/null +++ b/web/src/app/assistants/gallery/page.tsx @@ -0,0 +1,81 @@ +import { ChatSidebar } from "@/app/chat/sessionSidebar/ChatSidebar"; +import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; +import { UserDropdown } from "@/components/UserDropdown"; +import { ChatProvider } from "@/components/context/ChatContext"; +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 { AssistantsGallery } from "./AssistantsGallery"; + +export default async function GalleryPage({ + searchParams, +}: { + searchParams: { [key: string]: string }; +}) { + noStore(); + + const data = await fetchChatData(searchParams); + + if ("redirect" in data) { + redirect(data.redirect); + } + + const { + user, + chatSessions, + availableSources, + documentSets, + personas, + tags, + llmProviders, + folders, + openedFolders, + shouldShowWelcomeModal, + } = data; + + return ( + <> + + + {shouldShowWelcomeModal && } + + +
+ + +
+
+
+ +
+
+ +
+ +
+
+
+
+ + ); +} diff --git a/web/src/app/assistants/mine/AssistantSharingModal.tsx b/web/src/app/assistants/mine/AssistantSharingModal.tsx new file mode 100644 index 00000000000..0fa55fea424 --- /dev/null +++ b/web/src/app/assistants/mine/AssistantSharingModal.tsx @@ -0,0 +1,227 @@ +import { useState } from "react"; +import { Modal } from "@/components/Modal"; +import { MinimalUserSnapshot, User } from "@/lib/types"; +import { Button, Divider, Text } from "@tremor/react"; +import { FiPlus, FiX } from "react-icons/fi"; +import { Persona } from "@/app/admin/assistants/interfaces"; +import { SearchMultiSelectDropdown } from "@/components/Dropdown"; +import { UsersIcon } from "@/components/icons/icons"; +import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus"; +import { + addUsersToAssistantSharedList, + removeUsersFromAssistantSharedList, +} from "@/lib/assistants/shareAssistant"; +import { usePopup } from "@/components/admin/connectors/Popup"; +import { Bubble } from "@/components/Bubble"; +import { useRouter } from "next/navigation"; +import { AssistantIcon } from "@/components/assistants/AssistantIcon"; +import { Spinner } from "@/components/Spinner"; + +interface AssistantSharingModalProps { + assistant: Persona; + user: User | null; + allUsers: MinimalUserSnapshot[]; + show: boolean; + onClose: () => void; +} + +export function AssistantSharingModal({ + assistant, + user, + allUsers, + show, + onClose, +}: AssistantSharingModalProps) { + const router = useRouter(); + const { popup, setPopup } = usePopup(); + const [isUpdating, setIsUpdating] = useState(false); + const [selectedUsers, setSelectedUsers] = useState([]); + + const assistantName = assistant.name; + const sharedUsersWithoutOwner = assistant.users.filter( + (u) => u.id !== assistant.owner?.id + ); + + if (!show) { + return null; + } + + const handleShare = async () => { + setIsUpdating(true); + const startTime = Date.now(); + + const error = await addUsersToAssistantSharedList( + assistant, + selectedUsers.map((user) => user.id) + ); + router.refresh(); + + const elapsedTime = Date.now() - startTime; + const remainingTime = Math.max(0, 1000 - elapsedTime); + + setTimeout(() => { + setIsUpdating(false); + if (error) { + setPopup({ + message: `Failed to share assistant - ${error}`, + type: "error", + }); + } + }, remainingTime); + }; + + let sharedStatus = null; + if (assistant.is_public || !sharedUsersWithoutOwner.length) { + sharedStatus = ( + + ); + } else { + sharedStatus = ( +
+ Shared with:{" "} +
+ {sharedUsersWithoutOwner.map((u) => ( + { + setIsUpdating(true); + const startTime = Date.now(); + + const error = await removeUsersFromAssistantSharedList( + assistant, + [u.id] + ); + router.refresh(); + + const elapsedTime = Date.now() - startTime; + const remainingTime = Math.max(0, 1000 - elapsedTime); + + setTimeout(() => { + setIsUpdating(false); + if (error) { + setPopup({ + message: `Failed to remove assistant - ${error}`, + type: "error", + }); + } + }, remainingTime); + }} + > +
+ {u.email} +
+
+ ))} +
+
+ ); + } + + return ( + <> + {popup} + + {" "} +
{assistantName}
+
+ } + onOutsideClick={onClose} + > +
+ {isUpdating && } + + Control which other users should have access to this assistant. + + +
+

Current status:

+ {sharedStatus} +
+ +

Share Assistant:

+
+ + !selectedUsers.map((u2) => u2.id).includes(u1.id) && + !sharedUsersWithoutOwner + .map((u2) => u2.id) + .includes(u1.id) && + u1.id !== user?.id + ) + .map((user) => { + return { + name: user.email, + value: user.id, + }; + })} + onSelect={(option) => { + setSelectedUsers([ + ...Array.from( + new Set([ + ...selectedUsers, + { id: option.value as string, email: option.name }, + ]) + ), + ]); + }} + itemComponent={({ option }) => ( +
+ + {option.name} +
+ +
+
+ )} + /> +
+ {selectedUsers.length > 0 && + selectedUsers.map((selectedUser) => ( +
{ + setSelectedUsers( + selectedUsers.filter( + (user) => user.id !== selectedUser.id + ) + ); + }} + className={` + flex + rounded-lg + px-2 + py-1 + border + border-border + hover:bg-hover-light + cursor-pointer`} + > + {selectedUser.email} +
+ ))} +
+ + {selectedUsers.length > 0 && ( + + )} +
+
+ + + ); +} diff --git a/web/src/app/assistants/mine/AssistantsList.tsx b/web/src/app/assistants/mine/AssistantsList.tsx new file mode 100644 index 00000000000..34e792c1ed2 --- /dev/null +++ b/web/src/app/assistants/mine/AssistantsList.tsx @@ -0,0 +1,367 @@ +"use client"; + +import { useState } from "react"; +import { MinimalUserSnapshot, User } from "@/lib/types"; +import { Persona } from "@/app/admin/assistants/interfaces"; +import { Divider, Text } from "@tremor/react"; +import { + FiArrowDown, + FiArrowUp, + FiEdit2, + FiMoreHorizontal, + FiPlus, + FiSearch, + FiX, + FiShare2, +} from "react-icons/fi"; +import Link from "next/link"; +import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants"; +import { + addAssistantToList, + moveAssistantDown, + moveAssistantUp, + removeAssistantFromList, +} from "@/lib/assistants/updateAssistantPreferences"; +import { AssistantIcon } from "@/components/assistants/AssistantIcon"; +import { DefaultPopover } from "@/components/popover/DefaultPopover"; +import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup"; +import { useRouter } from "next/navigation"; +import { NavigationButton } from "../NavigationButton"; +import { AssistantsPageTitle } from "../AssistantsPageTitle"; +import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership"; +import { AssistantSharingModal } from "./AssistantSharingModal"; +import { AssistantSharedStatusDisplay } from "../AssistantSharedStatus"; +import useSWR from "swr"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ToolsDisplay } from "../ToolsDisplay"; + +function AssistantListItem({ + assistant, + user, + allAssistantIds, + allUsers, + isFirst, + isLast, + isVisible, + setPopup, +}: { + assistant: Persona; + user: User | null; + allUsers: MinimalUserSnapshot[]; + allAssistantIds: number[]; + isFirst: boolean; + isLast: boolean; + isVisible: boolean; + setPopup: (popupSpec: PopupSpec | null) => void; +}) { + const router = useRouter(); + const [showSharingModal, setShowSharingModal] = useState(false); + + const currentChosenAssistants = user?.preferences?.chosen_assistants; + const isOwnedByUser = checkUserOwnsAssistant(user, assistant); + + return ( + <> + { + setShowSharingModal(false); + router.refresh(); + }} + show={showSharingModal} + /> +
+
+
+ +

+ {assistant.name} +

+
+ {assistant.tools.length > 0 && ( + + )} +
{assistant.description}
+
+ +
+
+ {isOwnedByUser && ( +
+ {!assistant.is_public && ( +
setShowSharingModal(true)} + > + +
+ )} + + + +
+ )} + + +
+ } + side="bottom" + align="start" + sideOffset={5} + > + {[ + ...(!isFirst + ? [ +
{ + const success = await moveAssistantUp( + assistant.id, + currentChosenAssistants || allAssistantIds + ); + if (success) { + setPopup({ + message: `"${assistant.name}" has been moved up.`, + type: "success", + }); + router.refresh(); + } else { + setPopup({ + message: `"${assistant.name}" could not be moved up.`, + type: "error", + }); + } + }} + > + Move Up +
, + ] + : []), + ...(!isLast + ? [ +
{ + const success = await moveAssistantDown( + assistant.id, + currentChosenAssistants || allAssistantIds + ); + if (success) { + setPopup({ + message: `"${assistant.name}" has been moved down.`, + type: "success", + }); + router.refresh(); + } else { + setPopup({ + message: `"${assistant.name}" could not be moved down.`, + type: "error", + }); + } + }} + > + Move Down +
, + ] + : []), + isVisible ? ( +
{ + if ( + currentChosenAssistants && + currentChosenAssistants.length === 1 + ) { + setPopup({ + message: `Cannot remove "${assistant.name}" - you must have at least one assistant.`, + type: "error", + }); + return; + } + + const success = await removeAssistantFromList( + assistant.id, + currentChosenAssistants || allAssistantIds + ); + if (success) { + setPopup({ + message: `"${assistant.name}" has been removed from your list.`, + type: "success", + }); + router.refresh(); + } else { + setPopup({ + message: `"${assistant.name}" could not be removed from your list.`, + type: "error", + }); + } + }} + > + {isOwnedByUser ? "Hide" : "Remove"} +
+ ) : ( +
{ + const success = await addAssistantToList( + assistant.id, + currentChosenAssistants || allAssistantIds + ); + if (success) { + setPopup({ + message: `"${assistant.name}" has been added to your list.`, + type: "success", + }); + router.refresh(); + } else { + setPopup({ + message: `"${assistant.name}" could not be added to your list.`, + type: "error", + }); + } + }} + > + Add +
+ ), + ]} + +
+ + ); +} + +interface AssistantsListProps { + user: User | null; + assistants: Persona[]; +} + +export function AssistantsList({ user, assistants }: AssistantsListProps) { + const filteredAssistants = orderAssistantsForUser(assistants, user); + const ownedButHiddenAssistants = assistants.filter( + (assistant) => + checkUserOwnsAssistant(user, assistant) && + user?.preferences?.chosen_assistants && + !user?.preferences?.chosen_assistants?.includes(assistant.id) + ); + const allAssistantIds = assistants.map((assistant) => assistant.id); + + const { popup, setPopup } = usePopup(); + + const { data: users } = useSWR( + "/api/users", + errorHandlingFetcher + ); + + return ( + <> + {popup} +
+ My Assistants + +
+ + +
+ + Create New Assistant +
+
+ + + + +
+ + View Public and Shared Assistants +
+
+ +
+ +

+ Assistants allow you to customize your experience for a specific + purpose. Specifically, they combine instructions, extra knowledge, and + any combination of tools. +

+ + + +

Active Assistants

+ + + The order the assistants appear below will be the order they appear in + the Assistants dropdown. The first assistant listed will be your + default assistant when you start a new chat. + + +
+ {filteredAssistants.map((assistant, index) => ( + + ))} +
+ + {ownedButHiddenAssistants.length > 0 && ( + <> + + +

Your Hidden Assistants

+ + + Assistants you've created that aren't currently visible + in the Assistants selector. + + +
+ {ownedButHiddenAssistants.map((assistant, index) => ( + + ))} +
+ + )} +
+ + ); +} diff --git a/web/src/app/assistants/mine/page.tsx b/web/src/app/assistants/mine/page.tsx new file mode 100644 index 00000000000..3bbf83e1705 --- /dev/null +++ b/web/src/app/assistants/mine/page.tsx @@ -0,0 +1,82 @@ +import { ChatSidebar } from "@/app/chat/sessionSidebar/ChatSidebar"; +import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; +import { UserDropdown } from "@/components/UserDropdown"; +import { ChatProvider } from "@/components/context/ChatContext"; +import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper"; +import { ApiKeyModal } from "@/components/llm/ApiKeyModal"; +import { fetchChatData } from "@/lib/chat/fetchChatData"; +import { unstable_noStore as noStore } from "next/cache"; +import { redirect } from "next/navigation"; +import { AssistantsList } from "./AssistantsList"; + +export default async function GalleryPage({ + searchParams, +}: { + searchParams: { [key: string]: string }; +}) { + noStore(); + + const data = await fetchChatData(searchParams); + + if ("redirect" in data) { + redirect(data.redirect); + } + + const { + user, + chatSessions, + availableSources, + documentSets, + personas, + tags, + llmProviders, + folders, + openedFolders, + shouldShowWelcomeModal, + } = data; + + return ( + <> + + + {shouldShowWelcomeModal && } + + +
+ + +
+
+
+ +
+
+ +
+ +
+
+
+
+ + ); +} diff --git a/web/src/app/assistants/new/page.tsx b/web/src/app/assistants/new/page.tsx index 1e17e553b85..19e0abcaf71 100644 --- a/web/src/app/assistants/new/page.tsx +++ b/web/src/app/assistants/new/page.tsx @@ -1,11 +1,10 @@ import { Card } from "@tremor/react"; import { HeaderWrapper } from "@/components/header/HeaderWrapper"; -import { FiChevronLeft } from "react-icons/fi"; -import Link from "next/link"; import { AssistantEditor } from "@/app/admin/assistants/AssistantEditor"; import { SuccessfulPersonaUpdateRedirectType } from "@/app/admin/assistants/enums"; import { fetchAssistantEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS"; import { ErrorCallout } from "@/components/ErrorCallout"; +import { LargeBackButton } from "../LargeBackButton"; export default async function Page() { const [values, error] = await fetchAssistantEditorInfoSS(); @@ -27,6 +26,7 @@ export default async function Page() { {...values} defaultPublic={false} redirectType={SuccessfulPersonaUpdateRedirectType.CHAT} + shouldAddAssistantToUserPreferences={true} /> @@ -40,12 +40,7 @@ export default async function Page() {
- - - +

New Assistant

diff --git a/web/src/app/chat/ChatIntro.tsx b/web/src/app/chat/ChatIntro.tsx index 047ff54e476..fd40aec5c49 100644 --- a/web/src/app/chat/ChatIntro.tsx +++ b/web/src/app/chat/ChatIntro.tsx @@ -27,209 +27,97 @@ function HelperItemDisplay({ ); } -function AllPersonaOptionDisplay({ - availablePersonas, - handlePersonaSelect, - handleClose, -}: { - availablePersonas: Persona[]; - handlePersonaSelect: (persona: Persona) => void; - handleClose: () => void; -}) { - return ( - -
-
-

-
-
- -
-
- All Available Assistants -

- -
- -
-
-
- {availablePersonas.map((persona) => ( -
{ - handleClose(); - handlePersonaSelect(persona); - }} - > - -
- ))} -
-
-
- ); -} - export function ChatIntro({ availableSources, availablePersonas, selectedPersona, - handlePersonaSelect, }: { availableSources: ValidSources[]; availablePersonas: Persona[]; - selectedPersona?: Persona; - handlePersonaSelect: (persona: Persona) => void; + selectedPersona: Persona; }) { - const [isAllPersonaOptionVisible, setIsAllPersonaOptionVisible] = - useState(false); - const availableSourceMetadata = getSourceMetadataForSources(availableSources); return ( <> - {isAllPersonaOptionVisible && ( - setIsAllPersonaOptionVisible(false)} - availablePersonas={availablePersonas} - handlePersonaSelect={handlePersonaSelect} - /> - )}
- {selectedPersona ? ( -
-
-
-
- Logo -
-
- {selectedPersona?.name || "How can I help you today?"} -
- {selectedPersona && ( -
{selectedPersona.description}
- )} -
-
- - {selectedPersona && selectedPersona.num_chunks !== 0 && ( - <> - -
- {selectedPersona.document_sets.length > 0 && ( -
-

- Knowledge Sets:{" "} -

-
- {selectedPersona.document_sets.map((documentSet) => ( -
- -
- -
- {documentSet.name} - - } - popupContent={ -
- -
- {documentSet.description} -
-
- } - direction="top" - /> -
- ))} -
-
- )} - {availableSources.length > 0 && ( -
-

- Connected Sources:{" "} -

-
- {availableSourceMetadata.map((sourceMetadata) => ( - -
- {sourceMetadata.icon({})} -
-
- {sourceMetadata.displayName} -
-
- ))} -
-
- )} -
- - )} -
- ) : ( -
+
+
Logo
-
- -
-

- Which assistant do you want to chat with today?{" "} -

-

- Or ask a question immediately to use the{" "} - {availablePersonas[0]?.name} assistant. -

-
- {availablePersonas - .slice(0, MAX_PERSONAS_TO_DISPLAY) - .map((persona) => ( -
handlePersonaSelect(persona)} - > - -
- ))} +
+ {selectedPersona?.name || "How can I help you today?"}
- {availablePersonas.length > MAX_PERSONAS_TO_DISPLAY && ( -
-
setIsAllPersonaOptionVisible(true)} - className="text-sm flex mx-auto p-1 hover:bg-hover-light rounded cursor-default" - > - See more -
-
+ {selectedPersona && ( +
{selectedPersona.description}
)}
- )} + + {selectedPersona && selectedPersona.num_chunks !== 0 && ( + <> + +
+ {selectedPersona.document_sets.length > 0 && ( +
+

+ Knowledge Sets:{" "} +

+
+ {selectedPersona.document_sets.map((documentSet) => ( +
+ +
+ +
+ {documentSet.name} + + } + popupContent={ +
+ +
+ {documentSet.description} +
+
+ } + direction="top" + /> +
+ ))} +
+
+ )} + {availableSources.length > 0 && ( +
+

+ Connected Sources:{" "} +

+
+ {availableSourceMetadata.map((sourceMetadata) => ( + +
+ {sourceMetadata.icon({})} +
+
+ {sourceMetadata.displayName} +
+
+ ))} +
+
+ )} +
+ + )} +
); diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index bba5e841e5c..bab94e9a9cd 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -64,6 +64,7 @@ import { ConfigurationModal } from "./modal/configuration/ConfigurationModal"; import { useChatContext } from "@/components/context/ChatContext"; import { UserDropdown } from "@/components/UserDropdown"; import { v4 as uuidv4 } from "uuid"; +import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants"; const MAX_INPUT_HEIGHT = 200; const TEMP_USER_MESSAGE_ID = -1; @@ -90,6 +91,7 @@ export function ChatPage({ folders, openedFolders, } = useChatContext(); + const filteredAssistants = orderAssistantsForUser(availablePersonas, user); const router = useRouter(); const searchParams = useSearchParams(); @@ -156,7 +158,7 @@ export function ChatPage({ setIsFetchingChatMessages(false); if (defaultSelectedPersonaId !== undefined) { setSelectedPersona( - availablePersonas.find( + filteredAssistants.find( (persona) => persona.id === defaultSelectedPersonaId ) ); @@ -184,7 +186,7 @@ export function ChatPage({ ); const chatSession = (await response.json()) as BackendChatSession; setSelectedPersona( - availablePersonas.find( + filteredAssistants.find( (persona) => persona.id === chatSession.persona_id ) ); @@ -326,16 +328,16 @@ export function ChatPage({ const [selectedPersona, setSelectedPersona] = useState( existingChatSessionPersonaId !== undefined - ? availablePersonas.find( + ? filteredAssistants.find( (persona) => persona.id === existingChatSessionPersonaId ) : defaultSelectedPersonaId !== undefined - ? availablePersonas.find( + ? filteredAssistants.find( (persona) => persona.id === defaultSelectedPersonaId ) : undefined ); - const livePersona = selectedPersona || availablePersonas[0]; + const livePersona = selectedPersona || filteredAssistants[0]; const [chatSessionSharedStatus, setChatSessionSharedStatus] = useState(ChatSessionSharedStatus.Private); @@ -343,7 +345,7 @@ export function ChatPage({ useEffect(() => { if (messageHistory.length === 0 && chatSessionId === null) { setSelectedPersona( - availablePersonas.find( + filteredAssistants.find( (persona) => persona.id === defaultSelectedPersonaId ) ); @@ -903,7 +905,7 @@ export function ChatPage({
{ - setSelectedPersona(persona); - textAreaRef.current?.focus(); - router.push( - buildChatUrl(searchParams, null, persona.id) - ); - }} + availablePersonas={filteredAssistants} + selectedPersona={livePersona} /> )} diff --git a/web/src/app/chat/ChatPersonaSelector.tsx b/web/src/app/chat/ChatPersonaSelector.tsx index fbddafc1fa4..33ef61ef18f 100644 --- a/web/src/app/chat/ChatPersonaSelector.tsx +++ b/web/src/app/chat/ChatPersonaSelector.tsx @@ -2,8 +2,8 @@ import { Persona } from "@/app/admin/assistants/interfaces"; import { FiCheck, FiChevronDown, FiPlusSquare, FiEdit } from "react-icons/fi"; import { CustomDropdown, DefaultDropdownElement } from "@/components/Dropdown"; import { useRouter } from "next/navigation"; -import { Divider } from "@tremor/react"; import Link from "next/link"; +import { checkUserIdOwnsAssistant } from "@/lib/assistants/checkOwnership"; function PersonaItem({ id, @@ -95,7 +95,7 @@ export function ChatPersonaSelector({ > {personas.map((persona) => { const isSelected = persona.id === selectedPersonaId; - const isOwner = persona.owner?.id === userId; + const isOwner = checkUserIdOwnsAssistant(userId, persona); return ( void; +} + export function AssistantsTab({ selectedAssistant, onSelect, -}: { - selectedAssistant: Persona; - onSelect: (assistant: Persona) => void; -}) { +}: AssistantsTabProps) { const { availablePersonas, llmProviders } = useChatContext(); const [_, llmName] = getFinalLLM(llmProviders, null); return ( -
+ <>

Choose Assistant

-
+
{availablePersonas.map((assistant) => (
onSelect(assistant)} > -
{assistant.name}
-
+
+ +
+ {assistant.name} +
+
+ {assistant.tools.length > 0 && ( +
+ {assistant.tools.map((tool) => { + let toolName = tool.name; + let toolIcon = null; + + if (tool.name === "SearchTool") { + toolName = "Search"; + toolIcon = ; + } else if (tool.name === "ImageGenerationTool") { + toolName = "Image Generation"; + toolIcon = ; + } + + return ( + +
+ {toolIcon} + {toolName} +
+
+ ); + })} +
+ )} +
{assistant.description}
-
+
{assistant.document_sets.length > 0 && ( -
+

Document Sets:

{assistant.document_sets.map((set) => ( -
+
{set.name}
@@ -51,32 +88,6 @@ export function AssistantsTab({ ))}
)} - {assistant.tools.length > 0 && ( -
-

Tools:

- {assistant.tools.map((tool) => { - let toolName = tool.name; - let toolIcon = null; - - if (tool.name === "SearchTool") { - toolName = "Search"; - toolIcon = ; - } else if (tool.name === "ImageGenerationTool") { - toolName = "Image Generation"; - toolIcon = ; - } - - return ( - -
- {toolIcon} - {toolName} -
-
- ); - })} -
- )}
Default Model:{" "} {assistant.llm_model_version_override || llmName} @@ -85,6 +96,6 @@ export function AssistantsTab({
))}
-
+ ); } diff --git a/web/src/app/chat/modal/configuration/ConfigurationModal.tsx b/web/src/app/chat/modal/configuration/ConfigurationModal.tsx index ee75d88aa97..29b07c49494 100644 --- a/web/src/app/chat/modal/configuration/ConfigurationModal.tsx +++ b/web/src/app/chat/modal/configuration/ConfigurationModal.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useEffect } from "react"; import { Modal } from "../../../../components/Modal"; import { FilterManager, LlmOverrideManager } from "@/lib/hooks"; import { FiltersTab } from "./FiltersTab"; diff --git a/web/src/app/chat/page.tsx b/web/src/app/chat/page.tsx index f9623cb9c21..e0f42540882 100644 --- a/web/src/app/chat/page.tsx +++ b/web/src/app/chat/page.tsx @@ -1,37 +1,12 @@ -import { - AuthTypeMetadata, - getAuthTypeMetadataSS, - getCurrentUserSS, -} from "@/lib/userSS"; import { redirect } from "next/navigation"; -import { fetchSS } from "@/lib/utilsSS"; -import { - CCPairBasicInfo, - DocumentSet, - Tag, - User, - ValidSources, -} from "@/lib/types"; -import { ChatSession } from "./interfaces"; import { unstable_noStore as noStore } from "next/cache"; -import { Persona } from "../admin/assistants/interfaces"; import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; -import { - WelcomeModal, - hasCompletedWelcomeFlowSS, -} from "@/components/initialSetup/welcome/WelcomeModalWrapper"; +import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper"; import { ApiKeyModal } from "@/components/llm/ApiKeyModal"; -import { cookies } from "next/headers"; -import { DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME } from "@/components/resizable/contants"; -import { personaComparator } from "../admin/assistants/lib"; import { ChatPage } from "./ChatPage"; -import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels"; import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal"; -import { Settings } from "../admin/settings/interfaces"; -import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs"; -import { LLMProviderDescriptor } from "../admin/models/llm/interfaces"; -import { Folder } from "./folders/interfaces"; import { ChatProvider } from "@/components/context/ChatContext"; +import { fetchChatData } from "@/lib/chat/fetchChatData"; export default async function Page({ searchParams, @@ -40,146 +15,28 @@ export default async function Page({ }) { noStore(); - const tasks = [ - getAuthTypeMetadataSS(), - getCurrentUserSS(), - fetchSS("/manage/indexing-status"), - fetchSS("/manage/document-set"), - fetchSS("/persona?include_default=true"), - fetchSS("/chat/get-user-chat-sessions"), - fetchSS("/query/valid-tags"), - fetchLLMProvidersSS(), - fetchSS("/folder"), - ]; + const data = await fetchChatData(searchParams); - // catch cases where the backend is completely unreachable here - // without try / catch, will just raise an exception and the page - // will not render - let results: ( - | User - | Response - | AuthTypeMetadata - | FullEmbeddingModelResponse - | Settings - | LLMProviderDescriptor[] - | null - )[] = [null, null, null, null, null, null, null, null, null, null]; - try { - results = await Promise.all(tasks); - } catch (e) { - console.log(`Some fetch failed for the main search page - ${e}`); + if ("redirect" in data) { + redirect(data.redirect); } - const authTypeMetadata = results[0] as AuthTypeMetadata | null; - const user = results[1] as User | null; - const ccPairsResponse = results[2] as Response | null; - const documentSetsResponse = results[3] as Response | null; - const personasResponse = results[4] as Response | null; - const chatSessionsResponse = results[5] as Response | null; - const tagsResponse = results[6] as Response | null; - const llmProviders = (results[7] || []) as LLMProviderDescriptor[]; - const foldersResponse = results[8] as Response | null; // Handle folders result - const authDisabled = authTypeMetadata?.authType === "disabled"; - if (!authDisabled && !user) { - return redirect("/auth/login"); - } - - if (user && !user.is_verified && authTypeMetadata?.requiresVerification) { - return redirect("/auth/waiting-on-verification"); - } - - let ccPairs: CCPairBasicInfo[] = []; - if (ccPairsResponse?.ok) { - ccPairs = await ccPairsResponse.json(); - } else { - console.log(`Failed to fetch connectors - ${ccPairsResponse?.status}`); - } - const availableSources: ValidSources[] = []; - ccPairs.forEach((ccPair) => { - if (!availableSources.includes(ccPair.source)) { - availableSources.push(ccPair.source); - } - }); - - let chatSessions: ChatSession[] = []; - if (chatSessionsResponse?.ok) { - chatSessions = (await chatSessionsResponse.json()).sessions; - } else { - console.log( - `Failed to fetch chat sessions - ${chatSessionsResponse?.text()}` - ); - } - // Larger ID -> created later - chatSessions.sort((a, b) => (a.id > b.id ? -1 : 1)); - - let documentSets: DocumentSet[] = []; - if (documentSetsResponse?.ok) { - documentSets = await documentSetsResponse.json(); - } else { - console.log( - `Failed to fetch document sets - ${documentSetsResponse?.status}` - ); - } - - let personas: Persona[] = []; - if (personasResponse?.ok) { - personas = await personasResponse.json(); - } else { - console.log(`Failed to fetch personas - ${personasResponse?.status}`); - } - // remove those marked as hidden by an admin - personas = personas.filter((persona) => persona.is_visible); - // sort them in priority order - personas.sort(personaComparator); - - let tags: Tag[] = []; - if (tagsResponse?.ok) { - tags = (await tagsResponse.json()).tags; - } else { - console.log(`Failed to fetch tags - ${tagsResponse?.status}`); - } - - const defaultPersonaIdRaw = searchParams["assistantId"]; - const defaultPersonaId = defaultPersonaIdRaw - ? parseInt(defaultPersonaIdRaw) - : undefined; - - const documentSidebarCookieInitialWidth = cookies().get( - DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME - ); - const finalDocumentSidebarInitialWidth = documentSidebarCookieInitialWidth - ? parseInt(documentSidebarCookieInitialWidth.value) - : undefined; - - const hasAnyConnectors = ccPairs.length > 0; - const shouldShowWelcomeModal = - !hasCompletedWelcomeFlowSS() && - !hasAnyConnectors && - (!user || user.role === "admin"); - const shouldDisplaySourcesIncompleteModal = - hasAnyConnectors && - !shouldShowWelcomeModal && - !ccPairs.some( - (ccPair) => ccPair.has_successful_run && ccPair.docs_indexed > 0 - ); - - // if no connectors are setup, only show personas that are pure - // passthrough and don't do any retrieval - if (!hasAnyConnectors) { - personas = personas.filter((persona) => persona.num_chunks === 0); - } - - let folders: Folder[] = []; - if (foldersResponse?.ok) { - folders = (await foldersResponse.json()).folders as Folder[]; - } else { - console.log(`Failed to fetch folders - ${foldersResponse?.status}`); - } - - const openedFoldersCookie = cookies().get("openedFolders"); - const openedFolders = openedFoldersCookie - ? JSON.parse(openedFoldersCookie.value) - : {}; + const { + user, + chatSessions, + ccPairs, + availableSources, + documentSets, + personas, + tags, + llmProviders, + folders, + openedFolders, + defaultPersonaId, + finalDocumentSidebarInitialWidth, + shouldShowWelcomeModal, + shouldDisplaySourcesIncompleteModal, + } = data; return ( <> diff --git a/web/src/app/chat/sessionSidebar/AssistantsTab.tsx b/web/src/app/chat/sessionSidebar/AssistantsTab.tsx index 404491961da..fc2ca413b38 100644 --- a/web/src/app/chat/sessionSidebar/AssistantsTab.tsx +++ b/web/src/app/chat/sessionSidebar/AssistantsTab.tsx @@ -57,7 +57,8 @@ export function AssistantsTab({ const globalAssistants = personas.filter((persona) => persona.is_public); const personalAssistants = personas.filter( (persona) => - (!user || persona.users.includes(user.id)) && !persona.is_public + (!user || persona.users.some((u) => u.id === user.id)) && + !persona.is_public ); return ( diff --git a/web/src/app/chat/sessionSidebar/ChatSidebar.tsx b/web/src/app/chat/sessionSidebar/ChatSidebar.tsx index b84bd1270d2..b5fe5975bb8 100644 --- a/web/src/app/chat/sessionSidebar/ChatSidebar.tsx +++ b/web/src/app/chat/sessionSidebar/ChatSidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { FiEdit, FiFolderPlus, FiPlusSquare } from "react-icons/fi"; +import { FiBook, FiEdit, FiFolderPlus, FiPlusSquare } from "react-icons/fi"; import { useContext, useEffect, useRef, useState } from "react"; import Link from "next/link"; import Image from "next/image"; @@ -15,6 +15,7 @@ import { usePopup } from "@/components/admin/connectors/Popup"; import { SettingsContext } from "@/components/settings/SettingsProvider"; import React from "react"; +import { FaBrain } from "react-icons/fa"; export const ChatSidebar = ({ existingChats, @@ -120,6 +121,14 @@ export const ChatSidebar = ({
+ + +
+ Manage Assistants +
+
+ +
void; children: string | JSX.Element; showCheckbox?: boolean; + notSelectable?: boolean; }) { return (
diff --git a/web/src/components/Dropdown.tsx b/web/src/components/Dropdown.tsx index 29c50c263b5..e247234aced 100644 --- a/web/src/components/Dropdown.tsx +++ b/web/src/components/Dropdown.tsx @@ -101,22 +101,24 @@ export function SearchMultiSelectDropdown({ px-4 py-2 text-sm - bg-gray-700 + bg-background + border + border-border rounded-md shadow-sm - focus:ring focus:ring-offset-0 focus:ring-1 focus:ring-offset-gray-800 focus:ring-blue-800`} + `} onClick={(e) => e.stopPropagation()} />
@@ -129,9 +131,9 @@ export function SearchMultiSelectDropdown({ w-full rounded-md shadow-lg - bg-gray-700 - border-2 - border-gray-600 + bg-background + border + border-border max-h-80 overflow-y-auto overscroll-contain`} @@ -165,7 +167,7 @@ export function SearchMultiSelectDropdown({ ) : ( +
+ + ) : ( + <> + + See the Dropbox connector{" "} + + setup guide + {" "} + on the Danswer docs to obtain a Dropbox token. + + + + formBody={ + <> + + + } + validationSchema={Yup.object().shape({ + dropbox_access_token: Yup.string().required( + "Please enter your Dropbox API token" + ), + })} + initialValues={{ + dropbox_access_token: "", + }} + onSubmit={(isSuccess) => { + if (isSuccess) { + refreshCredentials(); + mutate("/api/manage/admin/connector/indexing-status"); + } + }} + /> + + + )} + + {dropboxConnectorIndexingStatuses.length > 0 && ( + <> + + Dropbox indexing status + + + The latest article changes are fetched every 10 minutes. + +
+ + connectorIndexingStatuses={dropboxConnectorIndexingStatuses} + liveCredential={dropboxCredential} + onCredentialLink={async (connectorId) => { + if (dropboxCredential) { + await linkCredential(connectorId, dropboxCredential.id); + mutate("/api/manage/admin/connector/indexing-status"); + } + }} + onUpdate={() => + mutate("/api/manage/admin/connector/indexing-status") + } + /> +
+ + )} + + {dropboxCredential && dropboxConnectorIndexingStatuses.length === 0 && ( + <> + +

Create Connection

+

+ Press connect below to start the connection to your Dropbox + instance. +

+ + nameBuilder={(values) => `Dropbox`} + ccPairNameBuilder={(values) => `Dropbox`} + source="dropbox" + inputType="poll" + formBody={<>} + validationSchema={Yup.object().shape({})} + initialValues={{}} + refreshFreq={10 * 60} // 10 minutes + credentialId={dropboxCredential.id} + /> +
+ + )} + + ); +}; + +export default function Page() { + return ( +
+
+ +
+ } title="Dropbox" /> +
+
+ ); +} diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 3a385c1afd4..1fec3bb2720 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -332,10 +332,10 @@ export function ChatPage({ (persona) => persona.id === existingChatSessionPersonaId ) : defaultSelectedPersonaId !== undefined - ? filteredAssistants.find( - (persona) => persona.id === defaultSelectedPersonaId - ) - : undefined + ? filteredAssistants.find( + (persona) => persona.id === defaultSelectedPersonaId + ) + : undefined ); const livePersona = selectedPersona || filteredAssistants[0] || availablePersonas[0]; diff --git a/web/src/app/chat/message/custom-code-styles.css b/web/src/app/chat/message/custom-code-styles.css index b7d419beba5..30dee55400c 100644 --- a/web/src/app/chat/message/custom-code-styles.css +++ b/web/src/app/chat/message/custom-code-styles.css @@ -21,9 +21,7 @@ pre[class*="language-"] { ::-webkit-scrollbar-thumb { background: #4b5563; /* Dark handle color */ border-radius: 10px; - transition: - background 0.3s ease, - box-shadow 0.3s ease; /* Smooth transition for hover effect */ + transition: background 0.3s ease, box-shadow 0.3s ease; /* Smooth transition for hover effect */ } ::-webkit-scrollbar-thumb:hover { diff --git a/web/src/app/chat/useDocumentSelection.ts b/web/src/app/chat/useDocumentSelection.ts index df33f13c3f6..9e5fa3d2c47 100644 --- a/web/src/app/chat/useDocumentSelection.ts +++ b/web/src/app/chat/useDocumentSelection.ts @@ -21,7 +21,7 @@ export function useDocumentSelection(): [ DanswerDocument[], (document: DanswerDocument) => void, () => void, - number, + number ] { const [selectedDocuments, setSelectedDocuments] = useState( [] diff --git a/web/src/components/Bubble.tsx b/web/src/components/Bubble.tsx index 1b559ec2407..8bf6f19771d 100644 --- a/web/src/components/Bubble.tsx +++ b/web/src/components/Bubble.tsx @@ -27,8 +27,8 @@ export function Bubble({ (notSelectable ? " bg-background cursor-default" : isSelected - ? " bg-hover cursor-pointer" - : " bg-background hover:bg-hover-light cursor-pointer") + ? " bg-hover cursor-pointer" + : " bg-background hover:bg-hover-light cursor-pointer") } onClick={onClick} > diff --git a/web/src/components/admin/connectors/table/ConnectorsTable.tsx b/web/src/components/admin/connectors/table/ConnectorsTable.tsx index be9448bac8e..1a84fdcd78c 100644 --- a/web/src/components/admin/connectors/table/ConnectorsTable.tsx +++ b/web/src/components/admin/connectors/table/ConnectorsTable.tsx @@ -87,7 +87,7 @@ export function StatusRow({ export interface ColumnSpecification< ConnectorConfigType, - ConnectorCredentialType, + ConnectorCredentialType > { header: string; key: string; @@ -101,7 +101,7 @@ export interface ColumnSpecification< export interface ConnectorsTableProps< ConnectorConfigType, - ConnectorCredentialType, + ConnectorCredentialType > { connectorIndexingStatuses: ConnectorIndexingStatus< ConnectorConfigType, diff --git a/web/src/components/admin/connectors/table/SingleUseConnectorsTable.tsx b/web/src/components/admin/connectors/table/SingleUseConnectorsTable.tsx index f1308a99504..47d84835799 100644 --- a/web/src/components/admin/connectors/table/SingleUseConnectorsTable.tsx +++ b/web/src/components/admin/connectors/table/SingleUseConnectorsTable.tsx @@ -46,7 +46,7 @@ const SingleUseConnectorStatus = ({ export function SingleUseConnectorsTable< ConnectorConfigType, - ConnectorCredentialType, + ConnectorCredentialType >({ connectorIndexingStatuses, liveCredential, diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 2a65feb08ac..2c9928e3144 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -51,6 +51,7 @@ import hubSpotIcon from "../../../public/HubSpot.png"; import document360Icon from "../../../public/Document360.png"; import googleSitesIcon from "../../../public/GoogleSites.png"; import zendeskIcon from "../../../public/Zendesk.svg"; +import dropboxIcon from "../../../public/Dropbox.png"; import sharepointIcon from "../../../public/Sharepoint.png"; import mediawikiIcon from "../../../public/MediaWiki.svg"; import wikipediaIcon from "../../../public/Wikipedia.svg"; @@ -604,6 +605,18 @@ export const ZendeskIcon = ({
); +export const DropboxIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => ( +
+ Logo +
+); + export const DiscourseIcon = ({ size = 16, className = defaultTailwindCSS, diff --git a/web/src/lib/assistants/fetchPersonaEditorInfoSS.ts b/web/src/lib/assistants/fetchPersonaEditorInfoSS.ts index b98564bee3d..17484cd1b3d 100644 --- a/web/src/lib/assistants/fetchPersonaEditorInfoSS.ts +++ b/web/src/lib/assistants/fetchPersonaEditorInfoSS.ts @@ -18,7 +18,7 @@ export async function fetchAssistantEditorInfoSS( existingPersona: Persona | null; tools: ToolSnapshot[]; }, - null, + null ] | [null, string] > { @@ -50,7 +50,7 @@ export async function fetchAssistantEditorInfoSS( Response, User | null, ToolSnapshot[] | null, - Response | null, + Response | null ]; if (!ccPairsInfoResponse.ok) { diff --git a/web/src/lib/sources.ts b/web/src/lib/sources.ts index 8e0820161fe..af173e614e3 100644 --- a/web/src/lib/sources.ts +++ b/web/src/lib/sources.ts @@ -4,6 +4,7 @@ import { ConfluenceIcon, DiscourseIcon, Document360Icon, + DropboxIcon, FileIcon, GithubIcon, GitlabIcon, @@ -153,6 +154,11 @@ const SOURCE_METADATA_MAP: SourceMap = { displayName: "Loopio", category: SourceCategory.AppConnection, }, + dropbox: { + icon: DropboxIcon, + displayName: "Dropbox", + category: SourceCategory.AppConnection, + }, sharepoint: { icon: SharepointIcon, displayName: "Sharepoint", diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index cd9ded58ade..55111b91d66 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -42,6 +42,7 @@ export type ValidSources = | "file" | "google_sites" | "loopio" + | "dropbox" | "sharepoint" | "zendesk" | "discourse" @@ -186,6 +187,8 @@ export interface GoogleSitesConfig { export interface ZendeskConfig {} +export interface DropboxConfig {} + export interface MediaWikiBaseConfig { connector_name: string; language_code: string; @@ -193,6 +196,7 @@ export interface MediaWikiBaseConfig { pages?: string[]; recurse_depth?: number; } + export interface MediaWikiConfig extends MediaWikiBaseConfig { hostname: string; } @@ -213,7 +217,7 @@ export interface IndexAttemptSnapshot { export interface ConnectorIndexingStatus< ConnectorConfigType, - ConnectorCredentialType, + ConnectorCredentialType > { cc_pair_id: number; name: string | null; @@ -357,6 +361,10 @@ export interface ZendeskCredentialJson { zendesk_token: string; } +export interface DropboxCredentialJson { + dropbox_access_token: string; +} + export interface SharepointCredentialJson { aad_client_id: string; aad_client_secret: string; From 41fbaf569854280526fa4e8a7b825f15eb094afe Mon Sep 17 00:00:00 2001 From: Weves Date: Wed, 5 Jun 2024 14:46:27 -0700 Subject: [PATCH 25/44] Fix prompt access --- backend/danswer/one_shot_answer/answer_question.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/danswer/one_shot_answer/answer_question.py b/backend/danswer/one_shot_answer/answer_question.py index 8d40f9abaa1..39e5d6c9472 100644 --- a/backend/danswer/one_shot_answer/answer_question.py +++ b/backend/danswer/one_shot_answer/answer_question.py @@ -129,8 +129,10 @@ def stream_answer_objects( prompt = None if query_req.prompt_id is not None: + # NOTE: let the user access any prompt as long as the Persona is shared + # with them prompt = get_prompt_by_id( - prompt_id=query_req.prompt_id, user=user, db_session=db_session + prompt_id=query_req.prompt_id, user=None, db_session=db_session ) if prompt is None: if not chat_session.persona.prompts: From adcbd354f426ea88b8474944b203c1b618b9cc44 Mon Sep 17 00:00:00 2001 From: Weves Date: Wed, 5 Jun 2024 14:06:43 -0700 Subject: [PATCH 26/44] Fix fast model update + slight reword on model selection --- backend/danswer/server/manage/llm/api.py | 6 +- backend/danswer/server/manage/llm/models.py | 2 +- .../llm/CustomLLMProviderUpdateForm.tsx | 104 ++++++------------ .../models/llm/LLMProviderUpdateForm.tsx | 98 ++++++----------- 4 files changed, 76 insertions(+), 134 deletions(-) diff --git a/backend/danswer/server/manage/llm/api.py b/backend/danswer/server/manage/llm/api.py index 7bc4efe63cc..6e21a29b12b 100644 --- a/backend/danswer/server/manage/llm/api.py +++ b/backend/danswer/server/manage/llm/api.py @@ -55,13 +55,13 @@ def test_llm_configuration( functions_with_args: list[tuple[Callable, tuple]] = [(test_llm, (llm,))] if ( - test_llm_request.default_fast_model_name - and test_llm_request.default_fast_model_name + test_llm_request.fast_default_model_name + and test_llm_request.fast_default_model_name != test_llm_request.default_model_name ): fast_llm = get_llm( provider=test_llm_request.provider, - model=test_llm_request.default_fast_model_name, + model=test_llm_request.fast_default_model_name, api_key=test_llm_request.api_key, api_base=test_llm_request.api_base, api_version=test_llm_request.api_version, diff --git a/backend/danswer/server/manage/llm/models.py b/backend/danswer/server/manage/llm/models.py index 0e791696a76..05a596ffd54 100644 --- a/backend/danswer/server/manage/llm/models.py +++ b/backend/danswer/server/manage/llm/models.py @@ -18,7 +18,7 @@ class TestLLMRequest(BaseModel): # model level default_model_name: str - default_fast_model_name: str | None = None + fast_default_model_name: str | None = None class LLMProviderDescriptor(BaseModel): diff --git a/web/src/app/admin/models/llm/CustomLLMProviderUpdateForm.tsx b/web/src/app/admin/models/llm/CustomLLMProviderUpdateForm.tsx index 7ada702031c..88b72f9411d 100644 --- a/web/src/app/admin/models/llm/CustomLLMProviderUpdateForm.tsx +++ b/web/src/app/admin/models/llm/CustomLLMProviderUpdateForm.tsx @@ -46,9 +46,6 @@ export function CustomLLMProviderUpdateForm({ const [isTesting, setIsTesting] = useState(false); const [testError, setTestError] = useState(""); - const [isTestSuccessful, setTestSuccessful] = useState( - existingLlmProvider ? true : false - ); // Define the initial values based on the provider's requirements const initialValues = { @@ -58,7 +55,7 @@ export function CustomLLMProviderUpdateForm({ api_base: existingLlmProvider?.api_base ?? "", api_version: existingLlmProvider?.api_version ?? "", default_model_name: existingLlmProvider?.default_model_name ?? null, - default_fast_model_name: + fast_default_model_name: existingLlmProvider?.fast_default_model_name ?? null, model_names: existingLlmProvider?.model_names ?? [], custom_config_list: existingLlmProvider?.custom_config @@ -66,10 +63,6 @@ export function CustomLLMProviderUpdateForm({ : [], }; - const [validatedConfig, setValidatedConfig] = useState( - existingLlmProvider ? initialValues : null - ); - // Setup validation schema if required const validationSchema = Yup.object({ name: Yup.string().required("Display Name is required"), @@ -79,7 +72,7 @@ export function CustomLLMProviderUpdateForm({ api_version: Yup.string(), model_names: Yup.array(Yup.string().required("Model name is required")), default_model_name: Yup.string().required("Model name is required"), - default_fast_model_name: Yup.string().nullable(), + fast_default_model_name: Yup.string().nullable(), custom_config_list: Yup.array(), }); @@ -87,20 +80,9 @@ export function CustomLLMProviderUpdateForm({ { - if (!isEqual(values, validatedConfig)) { - setTestSuccessful(false); - } - }} onSubmit={async (values, { setSubmitting }) => { setSubmitting(true); - if (!isTestSuccessful) { - setSubmitting(false); - return; - } - if (values.model_names.length === 0) { const fullErrorMsg = "At least one model name is required"; if (setPopup) { @@ -115,6 +97,29 @@ export function CustomLLMProviderUpdateForm({ return; } + // test the configuration + if (!isEqual(values, initialValues)) { + setIsTesting(true); + + const response = await fetch("/api/admin/llm/test", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + custom_config: customConfigProcessing(values.custom_config_list), + ...values, + }), + }); + setIsTesting(false); + + if (!response.ok) { + const errorMsg = (await response.json()).detail; + setTestError(errorMsg); + return; + } + } + const response = await fetch(LLM_PROVIDERS_ADMIN_URL, { method: "PUT", headers: { @@ -362,7 +367,7 @@ export function CustomLLMProviderUpdateForm({ /> {/* NOTE: this is above the test button to make sure it's visible */} - {!isTestSuccessful && testError && ( - {testError} - )} - {isTestSuccessful && ( - - Test successful! LLM provider is ready to go. - - )} + {testError && {testError}}
- {isTestSuccessful ? ( - - ) : ( - - )} + {existingLlmProvider && ( - ) : ( - - )} + {existingLlmProvider && (