diff --git a/.github/workflows/docker-build-push-web-container-on-tag.yml b/.github/workflows/docker-build-push-web-container-on-tag.yml index e5c4de0add6..8f0783f130a 100644 --- a/.github/workflows/docker-build-push-web-container-on-tag.yml +++ b/.github/workflows/docker-build-push-web-container-on-tag.yml @@ -5,40 +5,114 @@ on: tags: - '*' +env: + REGISTRY_IMAGE: danswer/danswer-web-server + jobs: - build-and-push: + build: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 steps: - - name: Checkout code - uses: actions/checkout@v2 - - - 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: 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=danswer/danswer-web-server:${{ github.ref_name }} + type=raw,value=danswer/danswer-web-server:latest + + - 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 }} + # 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 - - name: Web Image Docker Build and Push - uses: docker/build-push-action@v5 - with: - context: ./web - file: ./web/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: | - danswer/danswer-web-server:${{ github.ref_name }} - danswer/danswer-web-server:latest - build-args: | - DANSWER_VERSION=${{ github.ref_name }} - # needed due to weird interactions with the builds for different platforms - no-cache: true + 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 }} - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - image-ref: docker.io/danswer/danswer-web-server:${{ github.ref_name }} - severity: 'CRITICAL,HIGH' + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: docker.io/${{ env.REGISTRY_IMAGE }}:${{ github.ref_name }} + severity: 'CRITICAL,HIGH' diff --git a/.github/workflows/gar-build-push-backend-container-on-tag.yml b/.github/workflows/gar-build-push-backend-container-on-tag.yml index bd63a6c7487..0d10c5f89be 100644 --- a/.github/workflows/gar-build-push-backend-container-on-tag.yml +++ b/.github/workflows/gar-build-push-backend-container-on-tag.yml @@ -47,7 +47,7 @@ jobs: images: | us-docker.pkg.dev/${{ env.GarProjectID }}/${{ env.GarRepo }}/${{ env.GarImageName }}:${{ github.ref_name }} - - name: Web Image Docker Build and Push + - name: Backend Image Docker Build and Push uses: int128/kaniko-action@v1 with: context: ./backend diff --git a/.github/workflows/gar-build-push-model-server-container-on-tag.yml b/.github/workflows/gar-build-push-model-server-container-on-tag.yml index 2d6d8263ef0..29bf8d3434e 100644 --- a/.github/workflows/gar-build-push-model-server-container-on-tag.yml +++ b/.github/workflows/gar-build-push-model-server-container-on-tag.yml @@ -47,7 +47,7 @@ jobs: images: | us-docker.pkg.dev/${{ env.GarProjectID }}/${{ env.GarRepo }}/${{ env.GarImageName }}:${{ github.ref_name }} - - name: Web Image Docker Build and Push + - name: Model Server Image Docker Build and Push uses: int128/kaniko-action@v1 with: context: ./backend diff --git a/README.md b/README.md index edd8328c31e..658ee682382 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Documentation - + Slack 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..89439adb680 --- /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 = None +depends_on: None = 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/alembic/versions/b85f02ec1308_fix_file_type_migration.py b/backend/alembic/versions/b85f02ec1308_fix_file_type_migration.py new file mode 100644 index 00000000000..ac17670b703 --- /dev/null +++ b/backend/alembic/versions/b85f02ec1308_fix_file_type_migration.py @@ -0,0 +1,28 @@ +"""fix-file-type-migration + +Revision ID: b85f02ec1308 +Revises: a3bfd0d64902 +Create Date: 2024-05-31 18:09:26.658164 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "b85f02ec1308" +down_revision = "a3bfd0d64902" +branch_labels: None = None +depends_on: None = None + + +def upgrade() -> None: + op.execute( + """ + UPDATE file_store + SET file_origin = UPPER(file_origin) + """ + ) + + +def downgrade() -> None: + # Let's not break anything on purpose :) + pass 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/chat/load_yamls.py b/backend/danswer/chat/load_yamls.py index abb10461a9f..4036730a9ab 100644 --- a/backend/danswer/chat/load_yamls.py +++ b/backend/danswer/chat/load_yamls.py @@ -6,13 +6,13 @@ from danswer.configs.chat_configs import MAX_CHUNKS_FED_TO_CHAT from danswer.configs.chat_configs import PERSONAS_YAML from danswer.configs.chat_configs import PROMPTS_YAML -from danswer.db.chat import get_prompt_by_name -from danswer.db.chat import upsert_persona -from danswer.db.chat import upsert_prompt from danswer.db.document_set import get_or_create_document_set_by_name from danswer.db.engine import get_sqlalchemy_engine from danswer.db.models import DocumentSet as DocumentSetDBModel from danswer.db.models import Prompt as PromptDBModel +from danswer.db.persona import get_prompt_by_name +from danswer.db.persona import upsert_persona +from danswer.db.persona import upsert_prompt from danswer.search.enums import RecencyBiasSetting diff --git a/backend/danswer/chat/process_message.py b/backend/danswer/chat/process_message.py index 06ac84fa061..7ba7d991aa2 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 @@ -48,6 +49,8 @@ from danswer.search.enums import OptionalSearchSetting from danswer.search.retrieval.search_runner import inference_documents_from_ids from danswer.search.utils import chunks_or_sections_to_search_docs +from danswer.search.utils import dedupe_documents +from danswer.search.utils import drop_llm_indices from danswer.server.query_and_chat.models import ChatMessageDetail from danswer.server.query_and_chat.models import CreateChatMessageRequest from danswer.server.utils import get_json_line @@ -94,14 +97,21 @@ def _handle_search_tool_response_summary( packet: ToolResponse, db_session: Session, selected_search_docs: list[DbSearchDoc] | None, -) -> tuple[QADocsResponse, list[DbSearchDoc]]: + dedupe_docs: bool = False, +) -> tuple[QADocsResponse, list[DbSearchDoc], list[int] | None]: response_sumary = cast(SearchResponseSummary, packet.response) + dropped_inds = None if not selected_search_docs: top_docs = chunks_or_sections_to_search_docs(response_sumary.top_sections) + + deduped_docs = top_docs + if dedupe_docs: + deduped_docs, dropped_inds = dedupe_documents(top_docs) + reference_db_search_docs = [ - create_db_search_doc(server_search_doc=top_doc, db_session=db_session) - for top_doc in top_docs + create_db_search_doc(server_search_doc=doc, db_session=db_session) + for doc in deduped_docs ] else: reference_db_search_docs = selected_search_docs @@ -121,12 +131,17 @@ def _handle_search_tool_response_summary( recency_bias_multiplier=response_sumary.recency_bias_multiplier, ), reference_db_search_docs, + dropped_inds, ) def _check_should_force_search( new_msg_req: CreateChatMessageRequest, ) -> ForceUseTool | None: + # If files are already provided, don't run the search tool + if new_msg_req.file_descriptors: + return None + if ( new_msg_req.query_override or ( @@ -134,6 +149,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} @@ -267,8 +283,8 @@ def stream_chat_message_objects( "Be sure to update the chat pointers before calling this." ) - # Save now to save the latest chat message - db_session.commit() + # NOTE: do not commit user message - it will be committed when the + # assistant message is successfully generated else: # re-create linear history of messages final_msg, history_msgs = create_chat_chain( @@ -298,6 +314,7 @@ def stream_chat_message_objects( new_file.to_file_descriptor() for new_file in latest_query_files ], db_session=db_session, + commit=False, ) selected_db_search_docs = None @@ -356,7 +373,7 @@ def stream_chat_message_objects( # error=, # reference_docs=, db_session=db_session, - commit=True, + commit=False, ) if not final_msg.prompt: @@ -386,7 +403,7 @@ def stream_chat_message_objects( search_tool: SearchTool | None = None tools: list[Tool] = [] for tool_cls in persona_tool_classes: - if tool_cls.__name__ == SearchTool.__name__: + if tool_cls.__name__ == SearchTool.__name__ and not latest_query_files: search_tool = SearchTool( db_session=db_session, user=user, @@ -445,25 +462,44 @@ def stream_chat_message_objects( PreviousMessage.from_chat_message(msg, files) for msg in history_msgs ], tools=tools, - force_use_tool=_check_should_force_search(new_msg_req), + force_use_tool=( + _check_should_force_search(new_msg_req) if search_tool else None + ), ) reference_db_search_docs = None qa_docs_response = None ai_message_files = None # any files to associate with the AI message e.g. dall-e generated images + dropped_indices = None for packet in answer.processed_streamed_output: if isinstance(packet, ToolResponse): if packet.id == SEARCH_RESPONSE_SUMMARY_ID: ( qa_docs_response, reference_db_search_docs, + dropped_indices, ) = _handle_search_tool_response_summary( - packet, db_session, selected_db_search_docs + packet=packet, + db_session=db_session, + selected_search_docs=selected_db_search_docs, + # Deduping happens at the last step to avoid harming quality by dropping content early on + dedupe_docs=retrieval_options.dedupe_docs + if retrieval_options + else False, ) yield qa_docs_response elif packet.id == SECTION_RELEVANCE_LIST_ID: + chunk_indices = packet.response + + if reference_db_search_docs is not None and dropped_indices: + chunk_indices = drop_llm_indices( + llm_indices=chunk_indices, + search_docs=reference_db_search_docs, + dropped_indices=dropped_indices, + ) + yield LLMRelevanceFilterResponse( - relevant_chunk_indices=packet.response + relevant_chunk_indices=chunk_indices ) elif packet.id == IMAGE_GENERATION_RESPONSE_ID: img_generation_response = cast( @@ -485,11 +521,19 @@ def stream_chat_message_objects( yield cast(ChatPacket, packet) except Exception as e: - logger.exception(e) + logger.exception("Failed to process chat message") + + # Don't leak the API key + error_msg = str(e) + if llm.config.api_key and llm.config.api_key.lower() in error_msg.lower(): + error_msg = ( + f"LLM failed to respond. Invalid API " + f"key error from '{llm.config.model_provider}'." + ) - # Frontend will erase whatever answer and show this instead - # This will be the issue 99% of the time - yield StreamingError(error="LLM failed to respond, have you set your API key?") + yield StreamingError(error=error_msg) + # Cancel the transaction so that no messages are saved + db_session.rollback() return # Post-LLM answer processing @@ -513,6 +557,7 @@ def stream_chat_message_objects( citations=db_citations, error=None, ) + db_session.commit() # actually save user / assistant message msg_detail_response = translate_db_message_to_chat_message_detail( gen_ai_response_message diff --git a/backend/danswer/chat/prompts.yaml b/backend/danswer/chat/prompts.yaml index 190fcb3da61..899903c1299 100644 --- a/backend/danswer/chat/prompts.yaml +++ b/backend/danswer/chat/prompts.yaml @@ -20,9 +20,15 @@ prompts: # Task Prompt (as shown in UI) task: > If asked to find stuff give the exact find you got, if asked for links provide the links. + Make sure you pay attention to the timeframe of the data if specified in the query for example if asked for the latest ticket, it should be the earliest date you have in the knowledge base data set. + Do not use Knowledge base content older than 1 year in any response. - Ifspecified for a certain data set only pull information from that data set such as Jira + + If specified for a certain data set only pull information from that data set such as Jira. + + 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/configs/app_configs.py b/backend/danswer/configs/app_configs.py index e33962971ce..577ef2f0aea 100644 --- a/backend/danswer/configs/app_configs.py +++ b/backend/danswer/configs/app_configs.py @@ -251,6 +251,7 @@ LOG_VESPA_TIMING_INFORMATION = ( os.environ.get("LOG_VESPA_TIMING_INFORMATION", "").lower() == "true" ) +LOG_ENDPOINT_LATENCY = os.environ.get("LOG_ENDPOINT_LATENCY", "").lower() == "true" # Anonymous usage telemetry DISABLE_TELEMETRY = os.environ.get("DISABLE_TELEMETRY", "").lower() == "true" diff --git a/backend/danswer/configs/constants.py b/backend/danswer/configs/constants.py index b8acd3adff0..5f06a4acc07 100644 --- a/backend/danswer/configs/constants.py +++ b/backend/danswer/configs/constants.py @@ -93,6 +93,7 @@ class DocumentSource(str, Enum): GOOGLE_SITES = "google_sites" ZENDESK = "zendesk" LOOPIO = "loopio" + DROPBOX = "dropbox" SHAREPOINT = "sharepoint" DISCOURSE = "discourse" AXERO = "axero" diff --git a/backend/danswer/configs/danswerbot_configs.py b/backend/danswer/configs/danswerbot_configs.py index 1dc01bca1d2..5500d28c3ef 100644 --- a/backend/danswer/configs/danswerbot_configs.py +++ b/backend/danswer/configs/danswerbot_configs.py @@ -73,3 +73,7 @@ DANSWER_BOT_FEEDBACK_REMINDER = int( os.environ.get("DANSWER_BOT_FEEDBACK_REMINDER") or 0 ) +# Set to True to rephrase the Slack users messages +DANSWER_BOT_REPHRASE_MESSAGE = ( + os.environ.get("DANSWER_BOT_REPHRASE_MESSAGE", "").lower() == "true" +) diff --git a/backend/danswer/connectors/dropbox/__init__.py b/backend/danswer/connectors/dropbox/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/backend/danswer/connectors/dropbox/connector.py b/backend/danswer/connectors/dropbox/connector.py new file mode 100644 index 00000000000..2fd39948aaa --- /dev/null +++ b/backend/danswer/connectors/dropbox/connector.py @@ -0,0 +1,151 @@ +from datetime import timezone +from io import BytesIO +from typing import Any + +from dropbox import Dropbox # type: ignore +from dropbox.exceptions import ApiError # type:ignore +from dropbox.files import FileMetadata # type:ignore +from dropbox.files import FolderMetadata # type:ignore + +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 LoadConnector +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.file_processing.extract_file_text import extract_file_text +from danswer.utils.logger import setup_logger + + +logger = setup_logger() + + +class DropboxConnector(LoadConnector, PollConnector): + def __init__(self, batch_size: int = INDEX_BATCH_SIZE) -> None: + self.batch_size = batch_size + self.dropbox_client: Dropbox | None = None + + def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: + self.dropbox_client = Dropbox(credentials["dropbox_access_token"]) + return None + + def _download_file(self, path: str) -> bytes: + """Download a single file from Dropbox.""" + if self.dropbox_client is None: + raise ConnectorMissingCredentialError("Dropbox") + _, resp = self.dropbox_client.files_download(path) + return resp.content + + def _get_shared_link(self, path: str) -> str: + """Create a shared link for a file in Dropbox.""" + if self.dropbox_client is None: + raise ConnectorMissingCredentialError("Dropbox") + + try: + # Check if a shared link already exists + shared_links = self.dropbox_client.sharing_list_shared_links(path=path) + if shared_links.links: + return shared_links.links[0].url + + link_metadata = ( + self.dropbox_client.sharing_create_shared_link_with_settings(path) + ) + return link_metadata.url + except ApiError as err: + logger.exception(f"Failed to create a shared link for {path}: {err}") + return "" + + def _yield_files_recursive( + self, + path: str, + start: SecondsSinceUnixEpoch | None, + end: SecondsSinceUnixEpoch | None, + ) -> GenerateDocumentsOutput: + """Yield files in batches from a specified Dropbox folder, including subfolders.""" + if self.dropbox_client is None: + raise ConnectorMissingCredentialError("Dropbox") + + result = self.dropbox_client.files_list_folder( + path, + limit=self.batch_size, + recursive=False, + include_non_downloadable_files=False, + ) + + while True: + batch: list[Document] = [] + for entry in result.entries: + if isinstance(entry, FileMetadata): + modified_time = entry.client_modified + if modified_time.tzinfo is None: + # If no timezone info, assume it is UTC + modified_time = modified_time.replace(tzinfo=timezone.utc) + else: + # If not in UTC, translate it + modified_time = modified_time.astimezone(timezone.utc) + + time_as_seconds = int(modified_time.timestamp()) + if start and time_as_seconds < start: + continue + if end and time_as_seconds > end: + continue + + downloaded_file = self._download_file(entry.path_display) + link = self._get_shared_link(entry.path_display) + try: + text = extract_file_text(entry.name, BytesIO(downloaded_file)) + batch.append( + Document( + id=f"doc:{entry.id}", + sections=[Section(link=link, text=text)], + source=DocumentSource.DROPBOX, + semantic_identifier=entry.name, + doc_updated_at=modified_time, + metadata={"type": "article"}, + ) + ) + except Exception as e: + logger.exception( + f"Error decoding file {entry.path_display} as utf-8 error occurred: {e}" + ) + + elif isinstance(entry, FolderMetadata): + yield from self._yield_files_recursive(entry.path_lower, start, end) + + if batch: + yield batch + + if not result.has_more: + break + + result = self.dropbox_client.files_list_folder_continue(result.cursor) + + def load_from_state(self) -> GenerateDocumentsOutput: + return self.poll_source(None, None) + + def poll_source( + self, start: SecondsSinceUnixEpoch | None, end: SecondsSinceUnixEpoch | None + ) -> GenerateDocumentsOutput: + if self.dropbox_client is None: + raise ConnectorMissingCredentialError("Dropbox") + + for batch in self._yield_files_recursive("", start, end): + yield batch + + return None + + +if __name__ == "__main__": + import os + + connector = DropboxConnector() + connector.load_credentials( + { + "dropbox_access_token": os.environ["DROPBOX_ACCESS_TOKEN"], + } + ) + document_batches = connector.load_from_state() + print(next(document_batches)) diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py index f68294be1ad..4873147e4f1 100644 --- a/backend/danswer/connectors/factory.py +++ b/backend/danswer/connectors/factory.py @@ -8,6 +8,7 @@ from danswer.connectors.danswer_jira.connector import JiraConnector from danswer.connectors.discourse.connector import DiscourseConnector from danswer.connectors.document360.connector import Document360Connector +from danswer.connectors.dropbox.connector import DropboxConnector from danswer.connectors.file.connector import LocalFileConnector from danswer.connectors.github.connector import GithubConnector from danswer.connectors.gitlab.connector import GitlabConnector @@ -73,6 +74,7 @@ def identify_connector_class( DocumentSource.GOOGLE_SITES: GoogleSitesConnector, DocumentSource.ZENDESK: ZendeskConnector, DocumentSource.LOOPIO: LoopioConnector, + DocumentSource.DROPBOX: DropboxConnector, DocumentSource.SHAREPOINT: SharepointConnector, DocumentSource.DISCOURSE: DiscourseConnector, DocumentSource.AXERO: AxeroConnector, diff --git a/backend/danswer/connectors/file/connector.py b/backend/danswer/connectors/file/connector.py index 2e6a9081d88..431b23f272a 100644 --- a/backend/danswer/connectors/file/connector.py +++ b/backend/danswer/connectors/file/connector.py @@ -69,7 +69,7 @@ def _process_file( if is_text_file_extension(file_name): encoding = detect_encoding(file) - file_content_raw, file_metadata = read_text_file(file, encoding=encoding) + file_content_raw, file_metadata = read_text_file(file, encoding=encoding, ignore_danswer_metadata=False) # Using the PDF reader function directly to pass in password cleanly elif extension == ".pdf": diff --git a/backend/danswer/connectors/gitlab/connector.py b/backend/danswer/connectors/gitlab/connector.py index 3b37dcdf5cf..55e358bbee0 100644 --- a/backend/danswer/connectors/gitlab/connector.py +++ b/backend/danswer/connectors/gitlab/connector.py @@ -6,6 +6,7 @@ from typing import Any import gitlab +import pytz from danswer.configs.app_configs import INDEX_BATCH_SIZE from danswer.configs.constants import DocumentSource @@ -114,12 +115,14 @@ def _fetch_from_gitlab( doc_batch: list[Document] = [] for mr in mr_batch: mr.updated_at = datetime.strptime( - mr.updated_at, "%Y-%m-%dT%H:%M:%S.%fZ" + mr.updated_at, "%Y-%m-%dT%H:%M:%S.%f%z" ) - if start is not None and mr.updated_at < start: + if start is not None and mr.updated_at < start.replace( + tzinfo=pytz.UTC + ): yield doc_batch return - if end is not None and mr.updated_at > end: + if end is not None and mr.updated_at > end.replace(tzinfo=pytz.UTC): continue doc_batch.append(_convert_merge_request_to_document(mr)) yield doc_batch @@ -131,13 +134,17 @@ def _fetch_from_gitlab( doc_batch = [] for issue in issue_batch: issue.updated_at = datetime.strptime( - issue.updated_at, "%Y-%m-%dT%H:%M:%S.%fZ" + issue.updated_at, "%Y-%m-%dT%H:%M:%S.%f%z" ) - if start is not None and issue.updated_at < start: - yield doc_batch - return - if end is not None and issue.updated_at > end: - continue + if start is not None: + start = start.replace(tzinfo=pytz.UTC) + if issue.updated_at < start: + yield doc_batch + return + if end is not None: + end = end.replace(tzinfo=pytz.UTC) + if issue.updated_at > end: + continue doc_batch.append(_convert_issue_to_document(issue)) yield doc_batch diff --git a/backend/danswer/connectors/sharepoint/connector.py b/backend/danswer/connectors/sharepoint/connector.py index d0e98f2fd61..0bd549f55e9 100644 --- a/backend/danswer/connectors/sharepoint/connector.py +++ b/backend/danswer/connectors/sharepoint/connector.py @@ -2,7 +2,8 @@ import os from datetime import datetime from datetime import timezone -from typing import Any +from typing import Any, Optional +from dataclasses import dataclass, field import msal # type: ignore from office365.graph_client import GraphClient # type: ignore @@ -58,6 +59,13 @@ def get_text_from_pptx_driveitem(driveitem_object: DriveItem) -> str: file_content = driveitem_object.get_content().execute_query().value return pptx_to_text(file=io.BytesIO(file_content)) +@dataclass +class SiteData: + url: str + folder: Optional[str] + siteobjects: list = field(default_factory=list) + driveitems: list = field(default_factory=list) + class SharepointConnector(LoadConnector, PollConnector): def __init__( @@ -67,7 +75,19 @@ def __init__( ) -> None: self.batch_size = batch_size self.graph_client: GraphClient | None = None - self.requested_site_list: list[str] = sites + self.site_data = self.extract_site_and_folder(sites) + + @staticmethod + def extract_site_and_folder(site_urls: list[str]) -> list[SiteData]: + site_data_list = [] + for url in site_urls: + parts = url.strip().split("/") + if "sites" in parts: + sites_index = parts.index("sites") + site_url = "/".join(parts[:sites_index + 2]) + folder = parts[sites_index + 2] if len(parts) > sites_index + 2 else None + site_data_list.append(SiteData(url=site_url, folder=folder, siteobjects=[], driveitems=[])) + return site_data_list def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: aad_client_id = credentials["aad_client_id"] @@ -94,7 +114,6 @@ def _acquire_token_func() -> dict[str, Any]: def get_all_driveitem_objects( self, - site_object_list: list[Site], start: datetime | None = None, end: datetime | None = None, ) -> list[DriveItem]: @@ -103,15 +122,24 @@ def get_all_driveitem_objects( filter_str = f"last_modified_datetime ge {start.isoformat()} and last_modified_datetime le {end.isoformat()}" driveitem_list = [] - for site_object in site_object_list: - site_list_objects = site_object.lists.get().execute_query() - for site_list_object in site_list_objects: + for element in self.site_data: + site_objects_list = [] + for site_object in element.siteobjects: + site_objects_sublist = site_object.lists.get().execute_query() + site_objects_list.extend(site_objects_sublist) + + for site_object in site_objects_list: try: - query = site_list_object.drive.root.get_files(True) + query = site_object.drive.root.get_files(True, 1000) if filter_str: query = query.filter(filter_str) driveitems = query.execute_query() - driveitem_list.extend(driveitems) + if element.folder: + filtered_driveitems = [item for item in driveitems if element.folder in item.parent_reference.path] + element.driveitems.extend(filtered_driveitems) + else: + element.driveitems.extend(driveitems) + except Exception: # Sites include things that do not contain .drive.root so this fails # but this is fine, as there are no actually documents in those @@ -123,20 +151,12 @@ def get_all_site_objects(self) -> list[Site]: if self.graph_client is None: raise ConnectorMissingCredentialError("Sharepoint") - site_object_list: list[Site] = [] - - sites_object = self.graph_client.sites.get().execute_query() - - if len(self.requested_site_list) > 0: - for requested_site in self.requested_site_list: - adjusted_string = "/" + requested_site.replace(" ", "") - for site_object in sites_object: - if site_object.web_url.endswith(adjusted_string): - site_object_list.append(site_object) + if self.site_data: + for element in self.site_data: + element.siteobjects = [self.graph_client.sites.get_by_url(element.url).get().execute_query()] else: - site_object_list.extend(sites_object) - - return site_object_list + site_objects = self.graph_client.sites.get().execute_query() + self.site_data = [SiteData(url=None, folder=None, siteobjects=site_objects, driveitems=[])] def _fetch_from_sharepoint( self, start: datetime | None = None, end: datetime | None = None @@ -144,28 +164,24 @@ def _fetch_from_sharepoint( if self.graph_client is None: raise ConnectorMissingCredentialError("Sharepoint") - site_object_list = self.get_all_site_objects() - - driveitem_list = self.get_all_driveitem_objects( - site_object_list=site_object_list, - start=start, - end=end, - ) + self.get_all_site_objects() + self.get_all_driveitem_objects(start=start, end=end) # goes over all urls, converts them into Document objects and then yields them in batches doc_batch: list[Document] = [] batch_count = 0 - for driveitem_object in driveitem_list: - logger.debug(f"Processing: {driveitem_object.web_url}") - doc_batch.append( - self.convert_driveitem_object_to_document(driveitem_object) - ) + for element in self.site_data: + for driveitem_object in element.driveitems: + logger.debug(f"Processing: {driveitem_object.web_url}") + doc_batch.append( + self.convert_driveitem_object_to_document(driveitem_object) + ) - batch_count += 1 - if batch_count >= self.batch_size: - yield doc_batch - batch_count = 0 - doc_batch = [] + batch_count += 1 + if batch_count >= self.batch_size: + yield doc_batch + batch_count = 0 + doc_batch = [] yield doc_batch def convert_driveitem_object_to_document( @@ -230,4 +246,4 @@ def poll_source( } ) document_batches = connector.load_from_state() - print(next(document_batches)) + print(next(document_batches)) \ No newline at end of file diff --git a/backend/danswer/danswerbot/slack/listener.py b/backend/danswer/danswerbot/slack/listener.py index ce7c9eda216..2ccce9fa773 100644 --- a/backend/danswer/danswerbot/slack/listener.py +++ b/backend/danswer/danswerbot/slack/listener.py @@ -10,6 +10,7 @@ from sqlalchemy.orm import Session from danswer.configs.constants import MessageType +from danswer.configs.danswerbot_configs import DANSWER_BOT_REPHRASE_MESSAGE from danswer.configs.danswerbot_configs import DANSWER_BOT_RESPOND_EVERY_CHANNEL from danswer.configs.danswerbot_configs import NOTIFY_SLACKBOT_NO_ANSWER from danswer.danswerbot.slack.config import get_slack_bot_config_for_channel @@ -40,6 +41,7 @@ from danswer.danswerbot.slack.utils import get_danswer_bot_app_id from danswer.danswerbot.slack.utils import read_slack_thread from danswer.danswerbot.slack.utils import remove_danswer_bot_tag +from danswer.danswerbot.slack.utils import rephrase_slack_message from danswer.danswerbot.slack.utils import respond_in_thread from danswer.db.embedding_model import get_current_db_embedding_model from danswer.db.engine import get_sqlalchemy_engine @@ -202,6 +204,14 @@ def build_request_details( msg = remove_danswer_bot_tag(msg, client=client.web_client) + if DANSWER_BOT_REPHRASE_MESSAGE: + logger.info(f"Rephrasing Slack message. Original message: {msg}") + try: + msg = rephrase_slack_message(msg) + logger.info(f"Rephrased message: {msg}") + except Exception as e: + logger.error(f"Error while trying to rephrase the Slack message: {e}") + if tagged: logger.info("User tagged DanswerBot") diff --git a/backend/danswer/danswerbot/slack/utils.py b/backend/danswer/danswerbot/slack/utils.py index 5895dc52f91..892734e9995 100644 --- a/backend/danswer/danswerbot/slack/utils.py +++ b/backend/danswer/danswerbot/slack/utils.py @@ -29,7 +29,12 @@ from danswer.danswerbot.slack.tokens import fetch_tokens from danswer.db.engine import get_sqlalchemy_engine from danswer.db.users import get_user_by_email +from danswer.llm.exceptions import GenAIDisabledException +from danswer.llm.factory import get_default_llm +from danswer.llm.utils import dict_based_prompt_to_langchain_prompt +from danswer.llm.utils import message_to_string from danswer.one_shot_answer.models import ThreadMessage +from danswer.prompts.miscellaneous_prompts import SLACK_LANGUAGE_REPHRASE_PROMPT from danswer.utils.logger import setup_logger from danswer.utils.telemetry import optional_telemetry from danswer.utils.telemetry import RecordType @@ -41,6 +46,30 @@ DANSWER_BOT_APP_ID: str | None = None +def rephrase_slack_message(msg: str) -> str: + def _get_rephrase_message() -> list[dict[str, str]]: + messages = [ + { + "role": "user", + "content": SLACK_LANGUAGE_REPHRASE_PROMPT.format(query=msg), + }, + ] + + return messages + + try: + llm = get_default_llm(use_fast_llm=False, timeout=5) + except GenAIDisabledException: + logger.warning("Unable to rephrase Slack user message, Gen AI disabled") + return msg + messages = _get_rephrase_message() + filled_llm_prompt = dict_based_prompt_to_langchain_prompt(messages) + model_output = message_to_string(llm.invoke(filled_llm_prompt)) + logger.debug(model_output) + + return model_output + + def update_emote_react( emoji: str, channel: str, diff --git a/backend/danswer/db/chat.py b/backend/danswer/db/chat.py index 3e006b021b2..50458efd8d8 100644 --- a/backend/danswer/db/chat.py +++ b/backend/danswer/db/chat.py @@ -1,40 +1,25 @@ -from collections.abc import Sequence -from functools import lru_cache from uuid import UUID from sqlalchemy import delete -from sqlalchemy import func -from sqlalchemy import not_ from sqlalchemy import nullsfirst from sqlalchemy import or_ from sqlalchemy import select -from sqlalchemy import update from sqlalchemy.exc import MultipleResultsFound from sqlalchemy.orm import Session from danswer.auth.schemas import UserRole from danswer.configs.chat_configs import HARD_DELETE_CHATS from danswer.configs.constants import MessageType -from danswer.db.constants import SLACK_BOT_PERSONA_PREFIX -from danswer.db.engine import get_sqlalchemy_engine from danswer.db.models import ChatMessage from danswer.db.models import ChatSession from danswer.db.models import ChatSessionSharedStatus -from danswer.db.models import DocumentSet as DBDocumentSet -from danswer.db.models import Persona -from danswer.db.models import Persona__User -from danswer.db.models import Persona__UserGroup from danswer.db.models import Prompt from danswer.db.models import SearchDoc from danswer.db.models import SearchDoc as DBSearchDoc -from danswer.db.models import StarterMessage -from danswer.db.models import Tool from danswer.db.models import User -from danswer.db.models import User__UserGroup from danswer.file_store.models import FileDescriptor from danswer.llm.override_models import LLMOverride from danswer.llm.override_models import PromptOverride -from danswer.search.enums import RecencyBiasSetting from danswer.search.models import RetrievalDocs from danswer.search.models import SavedSearchDoc from danswer.search.models import SearchDoc as ServerSearchDoc @@ -357,396 +342,6 @@ def get_prompt_by_id( return prompt -@lru_cache() -def get_default_prompt() -> Prompt: - with Session(get_sqlalchemy_engine()) as db_session: - stmt = select(Prompt).where(Prompt.id == 0) - - result = db_session.execute(stmt) - prompt = result.scalar_one_or_none() - - if prompt is None: - raise RuntimeError("Default Prompt not found") - - return prompt - - -def get_persona_by_id( - persona_id: int, - # if user is `None` assume the user is an admin or auth is disabled - user: User | None, - db_session: Session, - include_deleted: bool = False, -) -> Persona: - stmt = select(Persona).where(Persona.id == persona_id) - - # if user is an admin, they should have access to all Personas - if user is not None and user.role != UserRole.ADMIN: - stmt = stmt.where(or_(Persona.user_id == user.id, Persona.user_id.is_(None))) - - if not include_deleted: - stmt = stmt.where(Persona.deleted.is_(False)) - - result = db_session.execute(stmt) - persona = result.scalar_one_or_none() - - if persona is None: - raise ValueError( - f"Persona with ID {persona_id} does not exist or does not belong to user" - ) - - return persona - - -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: - return [] - prompts = db_session.scalars(select(Prompt).where(Prompt.id.in_(prompt_ids))).all() - - return prompts - - -def get_personas_by_ids( - persona_ids: list[int], db_session: Session -) -> Sequence[Persona]: - """Unsafe, can fetch personas from all users""" - if not persona_ids: - return [] - personas = db_session.scalars( - select(Persona).where(Persona.id.in_(persona_ids)) - ).all() - - return personas - - -def get_prompt_by_name( - prompt_name: str, user: User | None, db_session: Session -) -> Prompt | None: - stmt = select(Prompt).where(Prompt.name == prompt_name) - - # if user is not specified OR they are an admin, they should - # have access to all prompts, so this where clause is not needed - if user and user.role != UserRole.ADMIN: - stmt = stmt.where(Prompt.user_id == user.id) - - result = db_session.execute(stmt).scalar_one_or_none() - return result - - -def get_persona_by_name( - persona_name: str, user: User | None, db_session: Session -) -> Persona | None: - """Admins can see all, regular users can only fetch their own. - If user is None, assume the user is an admin or auth is disabled.""" - stmt = select(Persona).where(Persona.name == persona_name) - if user and user.role != UserRole.ADMIN: - stmt = stmt.where(Persona.user_id == user.id) - result = db_session.execute(stmt).scalar_one_or_none() - return result - - -def upsert_prompt( - user: User | None, - name: str, - description: str, - system_prompt: str, - task_prompt: str, - include_citations: bool, - datetime_aware: bool, - personas: list[Persona] | None, - db_session: Session, - prompt_id: int | None = None, - default_prompt: bool = True, - commit: bool = True, -) -> Prompt: - if prompt_id is not None: - prompt = db_session.query(Prompt).filter_by(id=prompt_id).first() - else: - prompt = get_prompt_by_name(prompt_name=name, user=user, db_session=db_session) - - if prompt: - if not default_prompt and prompt.default_prompt: - raise ValueError("Cannot update default prompt with non-default.") - - prompt.name = name - prompt.description = description - prompt.system_prompt = system_prompt - prompt.task_prompt = task_prompt - prompt.include_citations = include_citations - prompt.datetime_aware = datetime_aware - prompt.default_prompt = default_prompt - - if personas is not None: - prompt.personas.clear() - prompt.personas = personas - - else: - prompt = Prompt( - id=prompt_id, - user_id=user.id if user else None, - name=name, - description=description, - system_prompt=system_prompt, - task_prompt=task_prompt, - include_citations=include_citations, - datetime_aware=datetime_aware, - default_prompt=default_prompt, - personas=personas or [], - ) - db_session.add(prompt) - - if commit: - db_session.commit() - else: - # Flush the session so that the Prompt has an ID - db_session.flush() - - return prompt - - -def upsert_persona( - user: User | None, - name: str, - description: str, - num_chunks: float, - llm_relevance_filter: bool, - llm_filter_extraction: bool, - recency_bias: RecencyBiasSetting, - prompts: list[Prompt] | None, - document_sets: list[DBDocumentSet] | None, - llm_model_provider_override: str | None, - llm_model_version_override: str | None, - starter_messages: list[StarterMessage] | None, - is_public: bool, - db_session: Session, - tool_ids: list[int] | None = None, - persona_id: int | None = None, - default_persona: bool = False, - commit: bool = True, -) -> Persona: - if persona_id is not None: - persona = db_session.query(Persona).filter_by(id=persona_id).first() - else: - persona = get_persona_by_name( - persona_name=name, user=user, db_session=db_session - ) - - # Fetch and attach tools by IDs - tools = None - if tool_ids is not None: - tools = db_session.query(Tool).filter(Tool.id.in_(tool_ids)).all() - if not tools and tool_ids: - raise ValueError("Tools not found") - - if persona: - if not default_persona and persona.default_persona: - raise ValueError("Cannot update default persona with non-default.") - - persona.name = name - persona.description = description - persona.num_chunks = num_chunks - persona.llm_relevance_filter = llm_relevance_filter - persona.llm_filter_extraction = llm_filter_extraction - persona.recency_bias = recency_bias - persona.default_persona = default_persona - persona.llm_model_provider_override = llm_model_provider_override - persona.llm_model_version_override = llm_model_version_override - persona.starter_messages = starter_messages - persona.deleted = False # Un-delete if previously deleted - persona.is_public = is_public - - # Do not delete any associations manually added unless - # a new updated list is provided - if document_sets is not None: - persona.document_sets.clear() - persona.document_sets = document_sets or [] - - if prompts is not None: - persona.prompts.clear() - persona.prompts = prompts - - if tools is not None: - persona.tools = tools - - else: - persona = Persona( - id=persona_id, - user_id=user.id if user else None, - is_public=is_public, - name=name, - description=description, - num_chunks=num_chunks, - llm_relevance_filter=llm_relevance_filter, - llm_filter_extraction=llm_filter_extraction, - recency_bias=recency_bias, - default_persona=default_persona, - prompts=prompts or [], - document_sets=document_sets or [], - llm_model_provider_override=llm_model_provider_override, - llm_model_version_override=llm_model_version_override, - starter_messages=starter_messages, - tools=tools or [], - ) - db_session.add(persona) - - if commit: - db_session.commit() - else: - # flush the session so that the persona has an ID - db_session.flush() - - return persona - - -def mark_prompt_as_deleted( - prompt_id: int, - user: User | None, - db_session: Session, -) -> None: - prompt = get_prompt_by_id(prompt_id=prompt_id, user=user, db_session=db_session) - prompt.deleted = True - db_session.commit() - - -def mark_persona_as_deleted( - persona_id: int, - user: User | None, - db_session: Session, -) -> None: - persona = get_persona_by_id(persona_id=persona_id, user=user, db_session=db_session) - persona.deleted = True - db_session.commit() - - -def mark_persona_as_not_deleted( - persona_id: int, - user: User | None, - db_session: Session, -) -> None: - persona = get_persona_by_id( - persona_id=persona_id, user=user, db_session=db_session, include_deleted=True - ) - if persona.deleted: - persona.deleted = False - db_session.commit() - else: - raise ValueError(f"Persona with ID {persona_id} is not deleted.") - - -def mark_delete_persona_by_name( - persona_name: str, db_session: Session, is_default: bool = True -) -> None: - stmt = ( - update(Persona) - .where(Persona.name == persona_name, Persona.default_persona == is_default) - .values(deleted=True) - ) - - db_session.execute(stmt) - db_session.commit() - - -def delete_old_default_personas( - db_session: Session, -) -> None: - """Note, this locks out the Summarize and Paraphrase personas for now - Need a more graceful fix later or those need to never have IDs""" - stmt = ( - update(Persona) - .where(Persona.default_persona, Persona.id > 0) - .values(deleted=True, name=func.concat(Persona.name, "_old")) - ) - - db_session.execute(stmt) - db_session.commit() - - -def update_persona_visibility( - persona_id: int, - is_visible: bool, - db_session: Session, -) -> None: - persona = get_persona_by_id(persona_id=persona_id, user=None, db_session=db_session) - persona.is_visible = is_visible - db_session.commit() - - -def update_all_personas_display_priority( - display_priority_map: dict[int, int], - db_session: Session, -) -> None: - """Updates the display priority of all lives Personas""" - personas = get_personas(user_id=None, db_session=db_session) - available_persona_ids = {persona.id for persona in personas} - if available_persona_ids != set(display_priority_map.keys()): - raise ValueError("Invalid persona IDs provided") - - for persona in personas: - persona.display_priority = display_priority_map[persona.id] - - db_session.commit() - - -def get_prompts( - user_id: UUID | None, - db_session: Session, - include_default: bool = True, - include_deleted: bool = False, -) -> Sequence[Prompt]: - stmt = select(Prompt).where( - or_(Prompt.user_id == user_id, Prompt.user_id.is_(None)) - ) - - if not include_default: - stmt = stmt.where(Prompt.default_prompt.is_(False)) - if not include_deleted: - stmt = stmt.where(Prompt.deleted.is_(False)) - - return db_session.scalars(stmt).all() - - -def get_personas( - # if user_id is `None` assume the user is an admin or auth is disabled - user_id: UUID | None, - db_session: Session, - include_default: bool = True, - include_slack_bot_personas: bool = False, - include_deleted: bool = False, -) -> Sequence[Persona]: - stmt = select(Persona).distinct() - if user_id is not None: - # Subquery to find all groups the user belongs to - user_groups_subquery = ( - select(User__UserGroup.user_group_id) - .where(User__UserGroup.user_id == user_id) - .subquery() - ) - - # Include personas where the user is directly related or part of a user group that has access - access_conditions = or_( - Persona.is_public == True, # noqa: E712 - Persona.id.in_( # User has access through list of users with access - select(Persona__User.persona_id).where(Persona__User.user_id == user_id) - ), - Persona.id.in_( # User is part of a group that has access - select(Persona__UserGroup.persona_id).where( - Persona__UserGroup.user_group_id.in_(user_groups_subquery) # type: ignore - ) - ), - ) - stmt = stmt.where(access_conditions) - - if not include_default: - stmt = stmt.where(Persona.default_persona.is_(False)) - if not include_slack_bot_personas: - stmt = stmt.where(not_(Persona.name.startswith(SLACK_BOT_PERSONA_PREFIX))) - if not include_deleted: - stmt = stmt.where(Persona.deleted.is_(False)) - - return db_session.scalars(stmt).all() - - def get_doc_query_identifiers_from_model( search_doc_ids: list[int], chat_session: ChatSession, @@ -867,15 +462,3 @@ def translate_db_message_to_chat_message_detail( ) return chat_msg_detail - - -def delete_persona_by_name( - persona_name: str, db_session: Session, is_default: bool = True -) -> None: - stmt = delete(Persona).where( - Persona.name == persona_name, Persona.default_persona == is_default - ) - - db_session.execute(stmt) - - db_session.commit() diff --git a/backend/danswer/db/engine.py b/backend/danswer/db/engine.py index a8938ca0155..b8a7f858fd2 100644 --- a/backend/danswer/db/engine.py +++ b/backend/danswer/db/engine.py @@ -61,7 +61,9 @@ def get_sqlalchemy_engine() -> Engine: global _SYNC_ENGINE if _SYNC_ENGINE is None: connection_string = build_connection_string(db_api=SYNC_DB_API) - _SYNC_ENGINE = create_engine(connection_string, connect_args=connect_args) + _SYNC_ENGINE = create_engine( + connection_string, pool_size=40, max_overflow=10, connect_args=connect_args + ) return _SYNC_ENGINE @@ -71,7 +73,7 @@ def get_sqlalchemy_async_engine() -> AsyncEngine: if _ASYNC_ENGINE is None: connection_string = build_connection_string() _ASYNC_ENGINE = create_async_engine( - connection_string, connect_args=connect_args + connection_string, pool_size=40, max_overflow=10, connect_args=connect_args ) return _ASYNC_ENGINE @@ -95,4 +97,27 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]: yield async_session +async def warm_up_connections( + sync_connections_to_warm_up: int = 10, async_connections_to_warm_up: int = 10 +) -> None: + sync_postgres_engine = get_sqlalchemy_engine() + connections = [ + sync_postgres_engine.connect() for _ in range(sync_connections_to_warm_up) + ] + for conn in connections: + conn.execute(text("SELECT 1")) + for conn in connections: + conn.close() + + async_postgres_engine = get_sqlalchemy_async_engine() + async_connections = [ + await async_postgres_engine.connect() + for _ in range(async_connections_to_warm_up) + ] + for async_conn in async_connections: + await async_conn.execute(text("SELECT 1")) + for async_conn in async_connections: + await async_conn.close() + + SessionFactory = sessionmaker(bind=get_sqlalchemy_engine()) 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..88a72cc9331 100644 --- a/backend/danswer/db/persona.py +++ b/backend/danswer/db/persona.py @@ -1,15 +1,30 @@ +from collections.abc import Sequence +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_ +from sqlalchemy import or_ from sqlalchemy import select +from sqlalchemy import update from sqlalchemy.orm import Session -from danswer.db.chat import get_prompts_by_ids -from danswer.db.chat import upsert_persona +from danswer.auth.schemas import UserRole +from danswer.db.constants import SLACK_BOT_PERSONA_PREFIX from danswer.db.document_set import get_document_sets_by_ids +from danswer.db.engine import get_sqlalchemy_engine +from danswer.db.models import DocumentSet from danswer.db.models import Persona from danswer.db.models import Persona__User +from danswer.db.models import Persona__UserGroup +from danswer.db.models import Prompt +from danswer.db.models import StarterMessage +from danswer.db.models import Tool from danswer.db.models import User +from danswer.db.models import User__UserGroup +from danswer.search.enums import RecencyBiasSetting from danswer.server.features.persona.models import CreatePersonaRequest from danswer.server.features.persona.models import PersonaSnapshot from danswer.utils.logger import setup_logger @@ -45,6 +60,7 @@ def create_update_persona( user: User | None, db_session: Session, ) -> PersonaSnapshot: + """Higher level function than upsert_persona, although either is valid to use.""" # Permission to actually use these is checked later document_sets = list( get_document_sets_by_ids( @@ -97,5 +113,486 @@ 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)) + + +def get_prompts( + user_id: UUID | None, + db_session: Session, + include_default: bool = True, + include_deleted: bool = False, +) -> Sequence[Prompt]: + stmt = select(Prompt).where( + or_(Prompt.user_id == user_id, Prompt.user_id.is_(None)) + ) + + if not include_default: + stmt = stmt.where(Prompt.default_prompt.is_(False)) + if not include_deleted: + stmt = stmt.where(Prompt.deleted.is_(False)) + + return db_session.scalars(stmt).all() + + +def get_personas( + # if user_id is `None` assume the user is an admin or auth is disabled + user_id: UUID | None, + db_session: Session, + include_default: bool = True, + include_slack_bot_personas: bool = False, + include_deleted: bool = False, +) -> Sequence[Persona]: + stmt = select(Persona).distinct() + if user_id is not None: + # Subquery to find all groups the user belongs to + user_groups_subquery = ( + select(User__UserGroup.user_group_id) + .where(User__UserGroup.user_id == user_id) + .subquery() + ) + + # Include personas where the user is directly related or part of a user group that has access + access_conditions = or_( + Persona.is_public == True, # noqa: E712 + Persona.id.in_( # User has access through list of users with access + select(Persona__User.persona_id).where(Persona__User.user_id == user_id) + ), + Persona.id.in_( # User is part of a group that has access + select(Persona__UserGroup.persona_id).where( + Persona__UserGroup.user_group_id.in_(user_groups_subquery) # type: ignore + ) + ), + ) + stmt = stmt.where(access_conditions) + + if not include_default: + stmt = stmt.where(Persona.default_persona.is_(False)) + if not include_slack_bot_personas: + stmt = stmt.where(not_(Persona.name.startswith(SLACK_BOT_PERSONA_PREFIX))) + if not include_deleted: + stmt = stmt.where(Persona.deleted.is_(False)) + + return db_session.scalars(stmt).all() + + +def mark_persona_as_deleted( + persona_id: int, + user: User | None, + db_session: Session, +) -> None: + persona = get_persona_by_id(persona_id=persona_id, user=user, db_session=db_session) + persona.deleted = True + db_session.commit() + + +def mark_persona_as_not_deleted( + persona_id: int, + user: User | None, + db_session: Session, +) -> None: + persona = get_persona_by_id( + persona_id=persona_id, user=user, db_session=db_session, include_deleted=True + ) + if persona.deleted: + persona.deleted = False + db_session.commit() + else: + raise ValueError(f"Persona with ID {persona_id} is not deleted.") + + +def mark_delete_persona_by_name( + persona_name: str, db_session: Session, is_default: bool = True +) -> None: + stmt = ( + update(Persona) + .where(Persona.name == persona_name, Persona.default_persona == is_default) + .values(deleted=True) + ) + + db_session.execute(stmt) + db_session.commit() + + +def update_all_personas_display_priority( + display_priority_map: dict[int, int], + db_session: Session, +) -> None: + """Updates the display priority of all lives Personas""" + personas = get_personas(user_id=None, db_session=db_session) + available_persona_ids = {persona.id for persona in personas} + if available_persona_ids != set(display_priority_map.keys()): + raise ValueError("Invalid persona IDs provided") + + for persona in personas: + persona.display_priority = display_priority_map[persona.id] + + db_session.commit() + + +def upsert_prompt( + user: User | None, + name: str, + description: str, + system_prompt: str, + task_prompt: str, + include_citations: bool, + datetime_aware: bool, + personas: list[Persona] | None, + db_session: Session, + prompt_id: int | None = None, + default_prompt: bool = True, + commit: bool = True, +) -> Prompt: + if prompt_id is not None: + prompt = db_session.query(Prompt).filter_by(id=prompt_id).first() + else: + prompt = get_prompt_by_name(prompt_name=name, user=user, db_session=db_session) + + if prompt: + if not default_prompt and prompt.default_prompt: + raise ValueError("Cannot update default prompt with non-default.") + + prompt.name = name + prompt.description = description + prompt.system_prompt = system_prompt + prompt.task_prompt = task_prompt + prompt.include_citations = include_citations + prompt.datetime_aware = datetime_aware + prompt.default_prompt = default_prompt + + if personas is not None: + prompt.personas.clear() + prompt.personas = personas + + else: + prompt = Prompt( + id=prompt_id, + user_id=user.id if user else None, + name=name, + description=description, + system_prompt=system_prompt, + task_prompt=task_prompt, + include_citations=include_citations, + datetime_aware=datetime_aware, + default_prompt=default_prompt, + personas=personas or [], + ) + db_session.add(prompt) + + if commit: + db_session.commit() + else: + # Flush the session so that the Prompt has an ID + db_session.flush() + + return prompt + + +def upsert_persona( + user: User | None, + name: str, + description: str, + num_chunks: float, + llm_relevance_filter: bool, + llm_filter_extraction: bool, + recency_bias: RecencyBiasSetting, + prompts: list[Prompt] | None, + document_sets: list[DocumentSet] | None, + llm_model_provider_override: str | None, + llm_model_version_override: str | None, + starter_messages: list[StarterMessage] | None, + is_public: bool, + db_session: Session, + tool_ids: list[int] | None = None, + persona_id: int | None = None, + default_persona: bool = False, + commit: bool = True, +) -> Persona: + if persona_id is not None: + persona = db_session.query(Persona).filter_by(id=persona_id).first() + else: + persona = get_persona_by_name( + persona_name=name, user=user, db_session=db_session + ) + + # Fetch and attach tools by IDs + tools = None + if tool_ids is not None: + tools = db_session.query(Tool).filter(Tool.id.in_(tool_ids)).all() + if not tools and tool_ids: + raise ValueError("Tools not found") + + if 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 + persona.llm_relevance_filter = llm_relevance_filter + persona.llm_filter_extraction = llm_filter_extraction + persona.recency_bias = recency_bias + persona.default_persona = default_persona + persona.llm_model_provider_override = llm_model_provider_override + persona.llm_model_version_override = llm_model_version_override + persona.starter_messages = starter_messages + persona.deleted = False # Un-delete if previously deleted + persona.is_public = is_public + + # Do not delete any associations manually added unless + # a new updated list is provided + if document_sets is not None: + persona.document_sets.clear() + persona.document_sets = document_sets or [] + + if prompts is not None: + persona.prompts.clear() + persona.prompts = prompts + + if tools is not None: + persona.tools = tools + + else: + persona = Persona( + id=persona_id, + user_id=user.id if user else None, + is_public=is_public, + name=name, + description=description, + num_chunks=num_chunks, + llm_relevance_filter=llm_relevance_filter, + llm_filter_extraction=llm_filter_extraction, + recency_bias=recency_bias, + default_persona=default_persona, + prompts=prompts or [], + document_sets=document_sets or [], + llm_model_provider_override=llm_model_provider_override, + llm_model_version_override=llm_model_version_override, + starter_messages=starter_messages, + tools=tools or [], + ) + db_session.add(persona) + + if commit: + db_session.commit() + else: + # flush the session so that the persona has an ID + db_session.flush() + + return persona + + +def mark_prompt_as_deleted( + prompt_id: int, + user: User | None, + db_session: Session, +) -> None: + prompt = get_prompt_by_id(prompt_id=prompt_id, user=user, db_session=db_session) + prompt.deleted = True + db_session.commit() + + +def delete_old_default_personas( + db_session: Session, +) -> None: + """Note, this locks out the Summarize and Paraphrase personas for now + Need a more graceful fix later or those need to never have IDs""" + stmt = ( + update(Persona) + .where(Persona.default_persona, Persona.id > 0) + .values(deleted=True, name=func.concat(Persona.name, "_old")) + ) + + db_session.execute(stmt) + db_session.commit() + + +def update_persona_visibility( + persona_id: int, + is_visible: bool, + db_session: Session, +) -> None: + persona = get_persona_by_id(persona_id=persona_id, user=None, db_session=db_session) + persona.is_visible = is_visible + db_session.commit() + + +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: + return [] + prompts = db_session.scalars(select(Prompt).where(Prompt.id.in_(prompt_ids))).all() + + return prompts + + +def get_prompt_by_id( + prompt_id: int, + user: User | None, + db_session: Session, + include_deleted: bool = False, +) -> Prompt: + stmt = select(Prompt).where(Prompt.id == prompt_id) + + # if user is not specified OR they are an admin, they should + # have access to all prompts, so this where clause is not needed + if user and user.role != UserRole.ADMIN: + stmt = stmt.where(or_(Prompt.user_id == user.id, Prompt.user_id.is_(None))) + + if not include_deleted: + stmt = stmt.where(Prompt.deleted.is_(False)) + + result = db_session.execute(stmt) + prompt = result.scalar_one_or_none() + + if prompt is None: + raise ValueError( + f"Prompt with ID {prompt_id} does not exist or does not belong to user" + ) + + return prompt + + +@lru_cache() +def get_default_prompt() -> Prompt: + with Session(get_sqlalchemy_engine()) as db_session: + stmt = select(Prompt).where(Prompt.id == 0) + + result = db_session.execute(stmt) + prompt = result.scalar_one_or_none() + + if prompt is None: + raise RuntimeError("Default Prompt not found") + + return prompt + + +def get_persona_by_id( + persona_id: int, + # if user is `None` assume the user is an admin or auth is disabled + user: User | None, + db_session: Session, + include_deleted: bool = False, +) -> Persona: + stmt = select(Persona).where(Persona.id == persona_id) + + # if user is an admin, they should have access to all Personas + if user is not None and user.role != UserRole.ADMIN: + stmt = stmt.where(or_(Persona.user_id == user.id, Persona.user_id.is_(None))) + + if not include_deleted: + stmt = stmt.where(Persona.deleted.is_(False)) + + result = db_session.execute(stmt) + persona = result.scalar_one_or_none() + + if persona is None: + raise ValueError( + f"Persona with ID {persona_id} does not exist or does not belong to user" + ) + + return persona + + +def get_personas_by_ids( + persona_ids: list[int], db_session: Session +) -> Sequence[Persona]: + """Unsafe, can fetch personas from all users""" + if not persona_ids: + return [] + personas = db_session.scalars( + select(Persona).where(Persona.id.in_(persona_ids)) + ).all() + + return personas + + +def get_prompt_by_name( + prompt_name: str, user: User | None, db_session: Session +) -> Prompt | None: + stmt = select(Prompt).where(Prompt.name == prompt_name) + + # if user is not specified OR they are an admin, they should + # have access to all prompts, so this where clause is not needed + if user and user.role != UserRole.ADMIN: + stmt = stmt.where(Prompt.user_id == user.id) + + result = db_session.execute(stmt).scalar_one_or_none() + return result + + +def get_persona_by_name( + persona_name: str, user: User | None, db_session: Session +) -> Persona | None: + """Admins can see all, regular users can only fetch their own. + If user is None, assume the user is an admin or auth is disabled.""" + stmt = select(Persona).where(Persona.name == persona_name) + if user and user.role != UserRole.ADMIN: + stmt = stmt.where(Persona.user_id == user.id) + result = db_session.execute(stmt).scalar_one_or_none() + return result + + +def delete_persona_by_name( + persona_name: str, db_session: Session, is_default: bool = True +) -> None: + stmt = delete(Persona).where( + Persona.name == persona_name, Persona.default_persona == is_default + ) + + db_session.execute(stmt) + + db_session.commit() diff --git a/backend/danswer/db/slack_bot_config.py b/backend/danswer/db/slack_bot_config.py index 973d7624437..43418f6217e 100644 --- a/backend/danswer/db/slack_bot_config.py +++ b/backend/danswer/db/slack_bot_config.py @@ -4,7 +4,6 @@ from sqlalchemy.orm import Session from danswer.configs.chat_configs import MAX_CHUNKS_FED_TO_CHAT -from danswer.db.chat import upsert_persona from danswer.db.constants import SLACK_BOT_PERSONA_PREFIX from danswer.db.document_set import get_document_sets_by_ids from danswer.db.models import ChannelConfig @@ -12,6 +11,10 @@ from danswer.db.models import Persona__DocumentSet from danswer.db.models import SlackBotConfig from danswer.db.models import SlackBotResponseType +from danswer.db.models import User +from danswer.db.persona import get_default_prompt +from danswer.db.persona import mark_persona_as_deleted +from danswer.db.persona import upsert_persona from danswer.search.enums import RecencyBiasSetting @@ -48,6 +51,7 @@ def create_slack_bot_persona( # create/update persona associated with the slack bot persona_name = _build_persona_name(channel_names) + default_prompt = get_default_prompt() persona = upsert_persona( user=None, # Slack Bot Personas are not attached to users persona_id=existing_persona_id, @@ -57,7 +61,7 @@ def create_slack_bot_persona( llm_relevance_filter=True, llm_filter_extraction=True, recency_bias=RecencyBiasSetting.AUTO, - prompts=None, + prompts=[default_prompt], document_sets=document_sets, llm_model_provider_override=None, llm_model_version_override=None, @@ -133,6 +137,7 @@ def update_slack_bot_config( def remove_slack_bot_config( slack_bot_config_id: int, + user: User | None, db_session: Session, ) -> None: slack_bot_config = db_session.scalar( @@ -156,7 +161,9 @@ def remove_slack_bot_config( _cleanup_relationships( db_session=db_session, persona_id=existing_persona_id ) - db_session.delete(existing_persona) + mark_persona_as_deleted( + persona_id=existing_persona_id, user=user, db_session=db_session + ) db_session.delete(slack_bot_config) db_session.commit() diff --git a/backend/danswer/llm/answering/prompts/citations_prompt.py b/backend/danswer/llm/answering/prompts/citations_prompt.py index 81626e272c8..0a6e2c75e53 100644 --- a/backend/danswer/llm/answering/prompts/citations_prompt.py +++ b/backend/danswer/llm/answering/prompts/citations_prompt.py @@ -4,8 +4,8 @@ from danswer.chat.models import LlmDoc from danswer.configs.chat_configs import MULTILINGUAL_QUERY_EXPANSION from danswer.configs.model_configs import GEN_AI_SINGLE_USER_MESSAGE_EXPECTED_MAX_TOKENS -from danswer.db.chat import get_default_prompt from danswer.db.models import Persona +from danswer.db.persona import get_default_prompt from danswer.file_store.utils import InMemoryChatFile from danswer.llm.answering.models import PromptConfig from danswer.llm.factory import get_llm_for_persona diff --git a/backend/danswer/llm/answering/stream_processing/quotes_processing.py b/backend/danswer/llm/answering/stream_processing/quotes_processing.py index 61d379a5389..10d15b7195c 100644 --- a/backend/danswer/llm/answering/stream_processing/quotes_processing.py +++ b/backend/danswer/llm/answering/stream_processing/quotes_processing.py @@ -247,6 +247,17 @@ def process_model_tokens( if found_answer_start and not found_answer_end: if is_json_prompt and _stream_json_answer_end(model_previous, token): found_answer_end = True + + # return the remaining part of the answer e.g. token might be 'd.", ' and we should yield 'd.' + if token: + try: + answer_token_section = token.index('"') + yield DanswerAnswerPiece( + answer_piece=hold_quote + token[:answer_token_section] + ) + except ValueError: + logger.error("Quotation mark not found in token") + yield DanswerAnswerPiece(answer_piece=hold_quote + token) yield DanswerAnswerPiece(answer_piece=None) continue elif not is_json_prompt: diff --git a/backend/danswer/llm/chat_llm.py b/backend/danswer/llm/chat_llm.py index 4d7409eff48..d450efa9107 100644 --- a/backend/danswer/llm/chat_llm.py +++ b/backend/danswer/llm/chat_llm.py @@ -283,7 +283,7 @@ def _completion( # actual input messages=prompt, tools=tools, - tool_choice=tool_choice, + tool_choice=tool_choice if tools else None, # streaming choice stream=stream, # model params diff --git a/backend/danswer/llm/interfaces.py b/backend/danswer/llm/interfaces.py index 917da56f2fb..1f99383fae8 100644 --- a/backend/danswer/llm/interfaces.py +++ b/backend/danswer/llm/interfaces.py @@ -21,12 +21,6 @@ class LLMConfig(BaseModel): api_key: str | None -class LLMConfig(BaseModel): - model_provider: str - model_name: str - temperature: float - - class LLM(abc.ABC): """Mimics the LangChain LLM / BaseChatModel interfaces to make it easy to use these implementations to connect to a variety of LLM providers.""" diff --git a/backend/danswer/main.py b/backend/danswer/main.py index a40531a527e..92caa608f87 100644 --- a/backend/danswer/main.py +++ b/backend/danswer/main.py @@ -27,13 +27,13 @@ from danswer.configs.app_configs import AUTH_TYPE from danswer.configs.app_configs import DISABLE_GENERATIVE_AI from danswer.configs.app_configs import DISABLE_INDEX_UPDATE_ON_SWAP +from danswer.configs.app_configs import LOG_ENDPOINT_LATENCY 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 from danswer.configs.app_configs import WEB_DOMAIN from danswer.configs.chat_configs import MULTILINGUAL_QUERY_EXPANSION from danswer.configs.constants import AuthType -from danswer.db.chat import delete_old_default_personas from danswer.db.connector import create_initial_default_connector from danswer.db.connector_credential_pair import associate_default_cc_pair from danswer.db.connector_credential_pair import get_connector_credential_pairs @@ -42,8 +42,10 @@ from danswer.db.embedding_model import get_current_db_embedding_model from danswer.db.embedding_model import get_secondary_db_embedding_model from danswer.db.engine import get_sqlalchemy_engine +from danswer.db.engine import warm_up_connections from danswer.db.index_attempt import cancel_indexing_attempts_past_model from danswer.db.index_attempt import expire_index_attempts +from danswer.db.persona import delete_old_default_personas from danswer.db.swap_index import check_index_swap from danswer.document_index.factory import get_default_document_index from danswer.llm.llm_initialization import load_llm_providers @@ -70,6 +72,7 @@ from danswer.server.manage.secondary_index import router as secondary_index_router from danswer.server.manage.slack_bot import router as slack_bot_management_router from danswer.server.manage.users import router as user_router +from danswer.server.middleware.latency_logging import add_latency_logging_middleware from danswer.server.query_and_chat.chat_backend import router as chat_router from danswer.server.query_and_chat.query_backend import ( admin_router as admin_query_router, @@ -165,6 +168,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator: f"Using multilingual flow with languages: {MULTILINGUAL_QUERY_EXPANSION}" ) + # fill up Postgres connection pools + await warm_up_connections() + with Session(engine) as db_session: check_index_swap(db_session=db_session) db_embedding_model = get_current_db_embedding_model(db_session) @@ -356,6 +362,8 @@ def get_application() -> FastAPI: allow_methods=["*"], allow_headers=["*"], ) + if LOG_ENDPOINT_LATENCY: + add_latency_logging_middleware(application, logger) # Ensure all routes have auth enabled or are explicitly marked as public check_router_auth(application) diff --git a/backend/danswer/one_shot_answer/answer_question.py b/backend/danswer/one_shot_answer/answer_question.py index e5f45ada12d..2555d2d708e 100644 --- a/backend/danswer/one_shot_answer/answer_question.py +++ b/backend/danswer/one_shot_answer/answer_question.py @@ -19,11 +19,11 @@ from danswer.db.chat import create_db_search_doc from danswer.db.chat import create_new_chat_message from danswer.db.chat import get_or_create_root_message -from danswer.db.chat import get_prompt_by_id from danswer.db.chat import translate_db_message_to_chat_message_detail from danswer.db.chat import translate_db_search_doc_to_server_search_doc from danswer.db.engine import get_session_context_manager from danswer.db.models import User +from danswer.db.persona import get_prompt_by_id from danswer.llm.answering.answer import Answer from danswer.llm.answering.models import AnswerStyleConfig from danswer.llm.answering.models import CitationConfig @@ -39,6 +39,8 @@ from danswer.search.models import RerankMetricsContainer from danswer.search.models import RetrievalMetricsContainer from danswer.search.utils import chunks_or_sections_to_search_docs +from danswer.search.utils import dedupe_documents +from danswer.search.utils import drop_llm_indices from danswer.secondary_llm_flows.answer_validation import get_answer_validity from danswer.secondary_llm_flows.query_expansion import thread_based_query_rephrase from danswer.server.query_and_chat.models import ChatMessageDetail @@ -130,8 +132,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: @@ -194,6 +198,7 @@ def stream_answer_objects( skip_explicit_tool_calling=True, ) # won't be any ImageGenerationDisplay responses since that tool is never passed in + dropped_inds: list[int] = [] for packet in cast(AnswerObjectIterator, answer.processed_streamed_output): # for one-shot flow, don't currently do anything with these if isinstance(packet, ToolResponse): @@ -204,11 +209,14 @@ def stream_answer_objects( search_response_summary.top_sections ) + # Deduping happens at the last step to avoid harming quality by dropping content early on + deduped_docs = top_docs + if query_req.retrieval_options.dedupe_docs: + deduped_docs, dropped_inds = dedupe_documents(top_docs) + reference_db_search_docs = [ - create_db_search_doc( - server_search_doc=top_doc, db_session=db_session - ) - for top_doc in top_docs + create_db_search_doc(server_search_doc=doc, db_session=db_session) + for doc in deduped_docs ] response_docs = [ @@ -227,6 +235,15 @@ def stream_answer_objects( ) yield initial_response elif packet.id == SECTION_RELEVANCE_LIST_ID: + chunk_indices = packet.response + + if reference_db_search_docs is not None and dropped_inds: + chunk_indices = drop_llm_indices( + llm_indices=chunk_indices, + search_docs=reference_db_search_docs, + dropped_indices=dropped_inds, + ) + yield LLMRelevanceFilterResponse(relevant_chunk_indices=packet.response) else: yield packet diff --git a/backend/danswer/prompts/miscellaneous_prompts.py b/backend/danswer/prompts/miscellaneous_prompts.py index 340908f11c6..876f0f3591a 100644 --- a/backend/danswer/prompts/miscellaneous_prompts.py +++ b/backend/danswer/prompts/miscellaneous_prompts.py @@ -11,6 +11,20 @@ {query} """.strip() +SLACK_LANGUAGE_REPHRASE_PROMPT = """ +As an AI assistant employed by an organization, \ +your role is to transform user messages into concise \ +inquiries suitable for a Large Language Model (LLM) that \ +retrieves pertinent materials within a Retrieval-Augmented \ +Generation (RAG) framework. Ensure to reply in the identical \ +language as the original request. When faced with multiple \ +questions within a single query, distill them into a singular, \ +unified question, disregarding any direct mentions. + +Query: +{query} +""".strip() + # Use the following for easy viewing of prompts if __name__ == "__main__": 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: diff --git a/backend/danswer/search/models.py b/backend/danswer/search/models.py index 16a64d82049..f0069d5c750 100644 --- a/backend/danswer/search/models.py +++ b/backend/danswer/search/models.py @@ -116,6 +116,9 @@ class RetrievalDetails(ChunkContext): offset: int | None = None limit: int | None = None + # If this is set, only the highest matching chunk (or merged chunks) is returned + dedupe_docs: bool = False + class InferenceChunk(BaseChunk): document_id: str diff --git a/backend/danswer/search/search_nlp_models.py b/backend/danswer/search/search_nlp_models.py index 5243a29c067..761d9aa791f 100644 --- a/backend/danswer/search/search_nlp_models.py +++ b/backend/danswer/search/search_nlp_models.py @@ -24,6 +24,7 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false" os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1" +os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "1" logger = setup_logger() diff --git a/backend/danswer/search/utils.py b/backend/danswer/search/utils.py index fbcb205e3cf..5b5a6464aad 100644 --- a/backend/danswer/search/utils.py +++ b/backend/danswer/search/utils.py @@ -1,10 +1,39 @@ from collections.abc import Sequence +from typing import TypeVar +from danswer.db.models import SearchDoc as DBSearchDoc from danswer.search.models import InferenceChunk from danswer.search.models import InferenceSection from danswer.search.models import SearchDoc +T = TypeVar("T", InferenceSection, InferenceChunk, SearchDoc) + + +def dedupe_documents(items: list[T]) -> tuple[list[T], list[int]]: + seen_ids = set() + deduped_items = [] + dropped_indices = [] + for index, item in enumerate(items): + if item.document_id not in seen_ids: + seen_ids.add(item.document_id) + deduped_items.append(item) + else: + dropped_indices.append(index) + return deduped_items, dropped_indices + + +def drop_llm_indices( + llm_indices: list[int], search_docs: list[DBSearchDoc], dropped_indices: list[int] +) -> list[int]: + llm_bools = [True if i in llm_indices else False for i in range(len(search_docs))] + if dropped_indices: + llm_bools = [ + val for ind, val in enumerate(llm_bools) if ind not in dropped_indices + ] + return [i for i, val in enumerate(llm_bools) if val] + + def chunks_or_sections_to_search_docs( chunks: Sequence[InferenceChunk | InferenceSection] | None, ) -> list[SearchDoc]: 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 ) 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..006f7506894 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 @@ -5,15 +7,16 @@ from danswer.auth.users import current_admin_user from danswer.auth.users import current_user -from danswer.db.chat import get_persona_by_id -from danswer.db.chat import get_personas -from danswer.db.chat import mark_persona_as_deleted -from danswer.db.chat import mark_persona_as_not_deleted -from danswer.db.chat import update_all_personas_display_priority -from danswer.db.chat import update_persona_visibility 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 get_persona_by_id +from danswer.db.persona import get_personas +from danswer.db.persona import mark_persona_as_deleted +from danswer.db.persona import mark_persona_as_not_deleted +from danswer.db.persona import update_all_personas_display_priority +from danswer.db.persona import update_persona_shared_users +from danswer.db.persona import update_persona_visibility 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/features/prompt/api.py b/backend/danswer/server/features/prompt/api.py index 24c886ab915..aebcbb8434d 100644 --- a/backend/danswer/server/features/prompt/api.py +++ b/backend/danswer/server/features/prompt/api.py @@ -5,13 +5,13 @@ from starlette import status from danswer.auth.users import current_user -from danswer.db.chat import get_personas_by_ids -from danswer.db.chat import get_prompt_by_id -from danswer.db.chat import get_prompts -from danswer.db.chat import mark_prompt_as_deleted -from danswer.db.chat import upsert_prompt from danswer.db.engine import get_session from danswer.db.models import User +from danswer.db.persona import get_personas_by_ids +from danswer.db.persona import get_prompt_by_id +from danswer.db.persona import get_prompts +from danswer.db.persona import mark_prompt_as_deleted +from danswer.db.persona import upsert_prompt from danswer.server.features.prompt.models import CreatePromptRequest from danswer.server.features.prompt.models import PromptSnapshot from danswer.utils.logger import setup_logger 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/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/slack_bot.py b/backend/danswer/server/manage/slack_bot.py index 40e8663b054..aade420adeb 100644 --- a/backend/danswer/server/manage/slack_bot.py +++ b/backend/danswer/server/manage/slack_bot.py @@ -7,11 +7,11 @@ from danswer.danswerbot.slack.config import validate_channel_names from danswer.danswerbot.slack.tokens import fetch_tokens from danswer.danswerbot.slack.tokens import save_tokens -from danswer.db.chat import get_persona_by_id from danswer.db.constants import SLACK_BOT_PERSONA_PREFIX from danswer.db.engine import get_session from danswer.db.models import ChannelConfig from danswer.db.models import User +from danswer.db.persona import get_persona_by_id from danswer.db.slack_bot_config import create_slack_bot_persona from danswer.db.slack_bot_config import fetch_slack_bot_config from danswer.db.slack_bot_config import fetch_slack_bot_configs @@ -172,10 +172,10 @@ def patch_slack_bot_config( def delete_slack_bot_config( slack_bot_config_id: int, db_session: Session = Depends(get_session), - _: User | None = Depends(current_admin_user), + user: User | None = Depends(current_admin_user), ) -> None: remove_slack_bot_config( - slack_bot_config_id=slack_bot_config_id, db_session=db_session + slack_bot_config_id=slack_bot_config_id, user=user, db_session=db_session ) 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/backend/danswer/server/middleware/latency_logging.py b/backend/danswer/server/middleware/latency_logging.py new file mode 100644 index 00000000000..f2bc3127af4 --- /dev/null +++ b/backend/danswer/server/middleware/latency_logging.py @@ -0,0 +1,23 @@ +import logging +import time +from collections.abc import Awaitable +from collections.abc import Callable + +from fastapi import FastAPI +from fastapi import Request +from fastapi import Response + + +def add_latency_logging_middleware(app: FastAPI, logger: logging.LoggerAdapter) -> None: + @app.middleware("http") + async def log_latency( + request: Request, call_next: Callable[[Request], Awaitable[Response]] + ) -> Response: + start_time = time.monotonic() + response = await call_next(request) + process_time = time.monotonic() - start_time + logger.info( + f"Path: {request.url.path} - Method: {request.method} - " + f"Status Code: {response.status_code} - Time: {process_time:.4f} secs" + ) + return response diff --git a/backend/danswer/server/query_and_chat/chat_backend.py b/backend/danswer/server/query_and_chat/chat_backend.py index 97dcc62d00e..ab72699c2bc 100644 --- a/backend/danswer/server/query_and_chat/chat_backend.py +++ b/backend/danswer/server/query_and_chat/chat_backend.py @@ -24,7 +24,6 @@ from danswer.db.chat import get_chat_session_by_id from danswer.db.chat import get_chat_sessions_by_user from danswer.db.chat import get_or_create_root_message -from danswer.db.chat import get_persona_by_id from danswer.db.chat import set_as_latest_chat_message from danswer.db.chat import translate_db_message_to_chat_message_detail from danswer.db.chat import update_chat_session @@ -32,6 +31,7 @@ from danswer.db.feedback import create_chat_message_feedback from danswer.db.feedback import create_doc_retrieval_feedback from danswer.db.models import User +from danswer.db.persona import get_persona_by_id from danswer.document_index.document_index_utils import get_both_index_names from danswer.document_index.factory import get_default_document_index from danswer.file_processing.extract_file_text import extract_file_text diff --git a/backend/requirements/default.txt b/backend/requirements/default.txt index 710fa0d3ba1..bcf8478f85f 100644 --- a/backend/requirements/default.txt +++ b/backend/requirements/default.txt @@ -35,7 +35,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 @@ -71,3 +71,4 @@ uvicorn==0.21.1 zulip==0.8.2 hubspot-api-client==8.1.0 zenpy==2.0.41 +dropbox==11.36.2 diff --git a/deployment/data/nginx/app.conf.template b/deployment/data/nginx/app.conf.template index e02d8ff2f84..b698c744bf5 100644 --- a/deployment/data/nginx/app.conf.template +++ b/deployment/data/nginx/app.conf.template @@ -1,3 +1,9 @@ +# Log format to include request latency +log_format custom_main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time'; + upstream api_server { # fail_timeout=0 means we always retry an upstream even if it failed # to return a good HTTP response @@ -20,6 +26,8 @@ server { client_max_body_size 5G; # Maximum upload size + access_log /var/log/nginx/access.log custom_main; + # Match both /api/* and /openapi.json in a single rule location ~ ^/(api|openapi.json)(/.*)?$ { # Rewrite /api prefixed matched paths diff --git a/deployment/data/nginx/app.conf.template.dev b/deployment/data/nginx/app.conf.template.dev index a0e7237e7e8..a7a0efa192b 100644 --- a/deployment/data/nginx/app.conf.template.dev +++ b/deployment/data/nginx/app.conf.template.dev @@ -1,3 +1,9 @@ +# Override log format to include request latency +log_format custom_main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time'; + upstream api_server { # fail_timeout=0 means we always retry an upstream even if it failed # to return a good HTTP response @@ -20,6 +26,8 @@ server { client_max_body_size 5G; # Maximum upload size + access_log /var/log/nginx/access.log custom_main; + # Match both /api/* and /openapi.json in a single rule location ~ ^/(api|openapi.json)(/.*)?$ { # Rewrite /api prefixed matched paths @@ -58,3 +66,4 @@ server { proxy_pass http://web_server; } } + diff --git a/deployment/data/nginx/app.conf.template.no-letsencrypt b/deployment/data/nginx/app.conf.template.no-letsencrypt index abf4371fc8e..4d5096374a4 100644 --- a/deployment/data/nginx/app.conf.template.no-letsencrypt +++ b/deployment/data/nginx/app.conf.template.no-letsencrypt @@ -1,3 +1,9 @@ +# Log format to include request latency +log_format custom_main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time'; + upstream api_server { # fail_timeout=0 means we always retry an upstream even if it failed # to return a good HTTP response @@ -20,6 +26,8 @@ server { client_max_body_size 5G; # Maximum upload size + access_log /var/log/nginx/access.log custom_main; + # Match both /api/* and /openapi.json in a single rule location ~ ^/(api|openapi.json)(/.*)?$ { # Rewrite /api prefixed matched paths diff --git a/deployment/docker_compose/README.md b/deployment/docker_compose/README.md index 96285681e94..a12f22bea7d 100644 --- a/deployment/docker_compose/README.md +++ b/deployment/docker_compose/README.md @@ -2,10 +2,10 @@ # Deploying Danswer using Docker Compose -For general information, please read the instructions in this [README](https://github.com/danswer-ai/danswer/blob/main/deployment/docker_compose/README.md). +For general information, please read the instructions in this [README](https://github.com/danswer-ai/danswer/blob/main/deployment/README.md). ## Deploy in a system without GPU support -This part is elaborated precisely in in this [README](https://github.com/danswer-ai/danswer/blob/main/deployment/docker_compose/README.md) in section *Docker Compose*. If you have any questions, please feel free to open an issue or get in touch in slack for support. +This part is elaborated precisely in in this [README](https://github.com/danswer-ai/danswer/blob/main/deployment/README.md) in section *Docker Compose*. If you have any questions, please feel free to open an issue or get in touch in slack for support. ## Deploy in a system with GPU support Running Model servers with GPU support while indexing and querying can result in significant improvements in performance. This is highly recommended if you have access to resources. Currently, Danswer offloads embedding model and tokenizers to the GPU VRAM and the size needed depends on chosen embedding model. Default embedding models `intfloat/e5-base-v2` takes up about 1GB of VRAM and since we need this for inference and embedding pipeline, you would need roughly 2GB of VRAM. diff --git a/deployment/docker_compose/docker-compose.dev.yml b/deployment/docker_compose/docker-compose.dev.yml index 7dcc3bc347d..c7175faa1f5 100644 --- a/deployment/docker_compose/docker-compose.dev.yml +++ b/deployment/docker_compose/docker-compose.dev.yml @@ -81,6 +81,7 @@ services: # If set to `true` will enable additional logs about Vespa query performance # (time spent on finding the right docs + time spent fetching summaries from disk) - LOG_VESPA_TIMING_INFORMATION=${LOG_VESPA_TIMING_INFORMATION:-} + - LOG_ENDPOINT_LATENCY=${LOG_ENDPOINT_LATENCY:-} extra_hosts: - "host.docker.internal:host-gateway" logging: @@ -195,6 +196,7 @@ services: - NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA=${NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA:-false} - NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS:-} - NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-} + - NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-} depends_on: - api_server restart: always diff --git a/deployment/docker_compose/docker-compose.gpu-dev.yml b/deployment/docker_compose/docker-compose.gpu-dev.yml index f6c2c7fdd30..89dd673f91b 100644 --- a/deployment/docker_compose/docker-compose.gpu-dev.yml +++ b/deployment/docker_compose/docker-compose.gpu-dev.yml @@ -197,6 +197,7 @@ services: - NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA=${NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA:-false} - NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS:-} - NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-} + - NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-} depends_on: - api_server restart: always @@ -214,6 +215,7 @@ services: reservations: devices: - driver: nvidia + count: all capabilities: [gpu] build: context: ../../backend @@ -252,6 +254,7 @@ services: reservations: devices: - driver: nvidia + count: all capabilities: [gpu] command: > /bin/sh -c "if [ \"${DISABLE_MODEL_SERVER:-false}\" = \"True\" ]; then diff --git a/deployment/docker_compose/docker-compose.prod-no-letsencrypt.yml b/deployment/docker_compose/docker-compose.prod-no-letsencrypt.yml index 226df69d8e4..c2ef8a13274 100644 --- a/deployment/docker_compose/docker-compose.prod-no-letsencrypt.yml +++ b/deployment/docker_compose/docker-compose.prod-no-letsencrypt.yml @@ -67,6 +67,7 @@ services: - NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA=${NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA:-false} - NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS:-} - NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-} + - NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-} depends_on: - api_server restart: always diff --git a/deployment/docker_compose/docker-compose.prod.yml b/deployment/docker_compose/docker-compose.prod.yml index b93ce7c3843..26ef6101ce7 100644 --- a/deployment/docker_compose/docker-compose.prod.yml +++ b/deployment/docker_compose/docker-compose.prod.yml @@ -67,6 +67,7 @@ services: - NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA=${NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA:-false} - NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS:-} - NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS:-} + - NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT:-} depends_on: - api_server restart: always diff --git a/deployment/docker_compose/init-letsencrypt.sh b/deployment/docker_compose/init-letsencrypt.sh index 5eb3c73b9a1..9eec409fada 100755 --- a/deployment/docker_compose/init-letsencrypt.sh +++ b/deployment/docker_compose/init-letsencrypt.sh @@ -21,7 +21,13 @@ docker_compose_cmd() { # Assign appropriate Docker Compose command COMPOSE_CMD=$(docker_compose_cmd) -domains=("$DOMAIN" "www.$DOMAIN") +# Only add www to domain list if domain wasn't explicitly set as a subdomain +if [[ ! $DOMAIN == www.* ]]; then + domains=("$DOMAIN" "www.$DOMAIN") +else + domains=("$DOMAIN") +fi + rsa_key_size=4096 data_path="../data/certbot" email="$EMAIL" # Adding a valid address is strongly recommended diff --git a/deployment/helm/Chart.lock b/deployment/helm/Chart.lock index 7486bf317f2..918b44f6ebf 100644 --- a/deployment/helm/Chart.lock +++ b/deployment/helm/Chart.lock @@ -1,6 +1,12 @@ dependencies: - name: postgresql repository: https://charts.bitnami.com/bitnami - version: 14.1.0 -digest: sha256:526d286ca7143959104d8a7f9b196706efdbd89dcc37943a1b54016f224d4b4d -generated: "2024-02-16T12:21:42.36744+01:00" + version: 14.3.1 +- name: vespa + repository: https://unoplat.github.io/vespa-helm-charts + version: 0.2.3 +- name: nginx + repository: oci://registry-1.docker.io/bitnamicharts + version: 15.14.0 +digest: sha256:ab17b5d2c3883055cb4a26bf530043521be5220c24f804e954bb428273d16ba8 +generated: "2024-05-24T16:55:30.598279-07:00" diff --git a/deployment/helm/Chart.yaml b/deployment/helm/Chart.yaml index a36131be126..7763f33bec5 100644 --- a/deployment/helm/Chart.yaml +++ b/deployment/helm/Chart.yaml @@ -5,20 +5,31 @@ home: https://www.danswer.ai/ sources: - "https://github.com/danswer-ai/danswer" type: application -version: 0.1.0 -appVersion: "v0.3.42" +version: 0.2.0 +appVersion: "latest" annotations: category: Productivity licenses: MIT images: | - name: webserver - image: docker.io/danswer/danswer-web-server:v0.3.42 + image: docker.io/danswer/danswer-web-server:latest - name: background - image: docker.io/danswer/danswer-backend:v0.3.42 + image: docker.io/danswer/danswer-backend:latest - name: vespa image: vespaengine/vespa:8.277.17 dependencies: - name: postgresql - version: "14.1.0" + version: 14.3.1 repository: https://charts.bitnami.com/bitnami - condition: postgresql.enabled \ No newline at end of file + condition: postgresql.enabled + - name: vespa + version: 0.2.3 + repository: https://unoplat.github.io/vespa-helm-charts + condition: vespa.enabled + - name: nginx + version: 15.14.0 + repository: oci://registry-1.docker.io/bitnamicharts + condition: nginx.enabled + + + \ No newline at end of file diff --git a/deployment/helm/templates/NOTES.txt b/deployment/helm/templates/NOTES.txt deleted file mode 100644 index 41703407b6b..00000000000 --- a/deployment/helm/templates/NOTES.txt +++ /dev/null @@ -1,22 +0,0 @@ -1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.webserver.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "danswer-stack.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.webserver.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "danswer-stack.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "danswer-stack.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.webserver.service.port }} -{{- else if contains "ClusterIP" .Values.webserver.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "danswer-stack.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} diff --git a/deployment/helm/templates/_helpers.tpl b/deployment/helm/templates/_helpers.tpl index 4e6672fd677..483a5b5e5af 100644 --- a/deployment/helm/templates/_helpers.tpl +++ b/deployment/helm/templates/_helpers.tpl @@ -60,3 +60,24 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Set secret name +*/}} +{{- define "danswer-stack.secretName" -}} +{{- default (default "danswer-secrets" .Values.auth.secretName) .Values.auth.existingSecret }} +{{- end }} + +{{/* +Create env vars from secrets +*/}} +{{- define "danswer-stack.envSecrets" -}} + {{- range $name, $key := .Values.auth.secretKeys }} +- name: {{ $name | upper | replace "-" "_" | quote }} + valueFrom: + secretKeyRef: + name: {{ include "danswer-stack.secretName" $ }} + key: {{ default $name $key }} + {{- end }} +{{- end }} + diff --git a/deployment/helm/templates/api-deployment.yaml b/deployment/helm/templates/api-deployment.yaml index 8c40f3408c2..7f10bffafd0 100644 --- a/deployment/helm/templates/api-deployment.yaml +++ b/deployment/helm/templates/api-deployment.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "danswer-stack.fullname" . }}-api + name: {{ include "danswer-stack.fullname" . }}-api-deployment labels: {{- include "danswer-stack.labels" . | nindent 4 }} spec: @@ -11,6 +11,9 @@ spec: selector: matchLabels: {{- include "danswer-stack.selectorLabels" . | nindent 6 }} + {{- if .Values.api.deploymentLabels }} + {{- toYaml .Values.api.deploymentLabels | nindent 6 }} + {{- end }} template: metadata: {{- with .Values.api.podAnnotations }} @@ -31,7 +34,7 @@ spec: securityContext: {{- toYaml .Values.api.podSecurityContext | nindent 8 }} containers: - - name: {{ .Chart.Name }} + - name: api-server securityContext: {{- toYaml .Values.api.securityContext | nindent 12 }} image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag | default .Chart.AppVersion }}" @@ -51,60 +54,6 @@ spec: {{- toYaml .Values.api.resources | nindent 12 }} envFrom: - configMapRef: - name: {{ include "danswer-stack.fullname" . }} + name: {{ .Values.config.envConfigMapName }} env: - - name: INTERNAL_URL - value: {{ (list "http://" (include "danswer-stack.fullname" .) "-api:" .Values.api.service.port | join "") | quote }} - - name: VESPA_HOST - value: {{ (list (include "danswer-stack.fullname" .) "vespa" | join "-") }} - {{- if .Values.postgresql.enabled }} - - name: POSTGRES_HOST - value: {{ (list .Release.Name "postgresql" | join "-") }} - - name: POSTGRES_DB - value: {{ .Values.postgresql.auth.database }} - - name: POSTGRES_USER - value: {{ .Values.postgresql.auth.username }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ (list .Release.Name "postgresql" | join "-") }} - key: password - {{- end }} - volumeMounts: - - name: dynamic-storage - mountPath: /home/storage - - name: connector-storage - mountPath: /home/file_connector_storage - {{- if .Values.api.volumeMounts }} - {{- .Values.api.volumeMounts | toYaml | nindent 12}} - {{- end }} - volumes: - - name: dynamic-storage - {{- if .Values.persistence.dynamic.enabled }} - persistentVolumeClaim: - claimName: {{ .Values.persistence.dynamic.existingClaim | default (list (include "danswer-stack.fullname" .) "dynamic" | join "-") }} - {{- else }} - emptyDir: { } - {{- end }} - - name: connector-storage - {{- if .Values.persistence.connector.enabled }} - persistentVolumeClaim: - claimName: {{ .Values.persistence.connector.existingClaim | default (list (include "danswer-stack.fullname" .) "connector" | join "-") }} - {{- else }} - emptyDir: { } - {{- end }} - {{- if .Values.api.volumes }} - {{- .Values.api.volumes | toYaml | nindent 8}} - {{- end }} - {{- with .Values.api.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.api.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.api.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} + {{- include "danswer-stack.envSecrets" . | nindent 12}} diff --git a/deployment/helm/templates/api-service.yaml b/deployment/helm/templates/api-service.yaml index f4e4e0be693..1fd74d4ddf5 100644 --- a/deployment/helm/templates/api-service.yaml +++ b/deployment/helm/templates/api-service.yaml @@ -1,9 +1,13 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "danswer-stack.fullname" . }}-api + # INTERNAL_URL env variable depends on this, don't change without changing INTERNAL_URL + name: {{ include "danswer-stack.fullname" . }}-api-service labels: {{- include "danswer-stack.labels" . | nindent 4 }} + {{- if .Values.api.deploymentLabels }} + {{- toYaml .Values.api.deploymentLabels | nindent 4 }} + {{- end }} spec: type: {{ .Values.api.service.type }} ports: @@ -13,3 +17,6 @@ spec: name: api-server-port selector: {{- include "danswer-stack.selectorLabels" . | nindent 4 }} + {{- if .Values.api.deploymentLabels }} + {{- toYaml .Values.api.deploymentLabels | nindent 4 }} + {{- end }} diff --git a/deployment/helm/templates/background-deployment.yaml b/deployment/helm/templates/background-deployment.yaml index 59cfc524626..3cd65a99af4 100644 --- a/deployment/helm/templates/background-deployment.yaml +++ b/deployment/helm/templates/background-deployment.yaml @@ -11,6 +11,9 @@ spec: selector: matchLabels: {{- include "danswer-stack.selectorLabels" . | nindent 6 }} + {{- if .Values.background.deploymentLabels }} + {{- toYaml .Values.background.deploymentLabels | nindent 6 }} + {{- end }} template: metadata: {{- with .Values.background.podAnnotations }} @@ -31,7 +34,7 @@ spec: securityContext: {{- toYaml .Values.background.podSecurityContext | nindent 8 }} containers: - - name: {{ .Chart.Name }} + - name: background securityContext: {{- toYaml .Values.background.securityContext | nindent 12 }} image: "{{ .Values.background.image.repository }}:{{ .Values.background.image.tag | default .Chart.AppVersion }}" @@ -41,60 +44,8 @@ spec: {{- toYaml .Values.background.resources | nindent 12 }} envFrom: - configMapRef: - name: {{ include "danswer-stack.fullname" . }} + name: {{ .Values.config.envConfigMapName }} env: - - name: INTERNAL_URL - value: {{ (list "http://" (include "danswer-stack.fullname" .) "-api:" .Values.api.service.port | join "") | quote }} - - name: VESPA_HOST - value: {{ (list (include "danswer-stack.fullname" .) "vespa" | join "-") }} - {{- if .Values.postgresql.enabled }} - - name: POSTGRES_HOST - value: {{ (list .Release.Name "postgresql" | join "-") }} - - name: POSTGRES_DB - value: {{ .Values.postgresql.auth.database }} - - name: POSTGRES_USER - value: {{ .Values.postgresql.auth.username }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ (list .Release.Name "postgresql" | join "-") }} - key: password - {{- end }} - volumeMounts: - - name: dynamic-storage - mountPath: /home/storage - - name: connector-storage - mountPath: /home/file_connector_storage - {{- if .Values.background.volumeMounts }} - {{- .Values.background.volumeMounts | toYaml | nindent 12}} - {{- end }} - volumes: - - name: dynamic-storage - {{- if .Values.persistence.dynamic.enabled }} - persistentVolumeClaim: - claimName: {{ .Values.persistence.dynamic.existingClaim | default (list (include "danswer-stack.fullname" .) "dynamic" | join "-") }} - {{- else }} - emptyDir: { } - {{- end }} - - name: connector-storage - {{- if .Values.persistence.connector.enabled }} - persistentVolumeClaim: - claimName: {{ .Values.persistence.connector.existingClaim | default (list (include "danswer-stack.fullname" .) "connector" | join "-") }} - {{- else }} - emptyDir: { } - {{- end }} - {{- if .Values.background.volumes }} - {{- .Values.background.volumes | toYaml | nindent 8}} - {{- end }} - {{- with .Values.background.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.background.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.background.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} + - name: ENABLE_MINI_CHUNK + value: "{{ .Values.background.enableMiniChunk }}" + {{- include "danswer-stack.envSecrets" . | nindent 12}} diff --git a/deployment/helm/templates/configmap.yaml b/deployment/helm/templates/configmap.yaml index a393977986d..8119ae0459c 100755 --- a/deployment/helm/templates/configmap.yaml +++ b/deployment/helm/templates/configmap.yaml @@ -1,11 +1,15 @@ apiVersion: v1 kind: ConfigMap metadata: - name: {{ include "danswer-stack.fullname" . }} + name: {{ .Values.config.envConfigMapName }} labels: {{- include "danswer-stack.labels" . | nindent 4 }} data: -{{- range $key, $value := .Values.config }} - {{ $key }}: |- - {{- $value | nindent 4 }} -{{- end }} + INTERNAL_URL: "http://{{ include "danswer-stack.fullname" . }}-api-service:{{ .Values.api.service.port | default 8080 }}" + POSTGRES_HOST: {{ .Release.Name }}-postgresql + VESPA_HOST: "document-index-service" + MODEL_SERVER_HOST: "{{ include "danswer-stack.fullname" . }}-inference-model-service" + INDEXING_MODEL_SERVER_HOST: "{{ include "danswer-stack.fullname" . }}-indexing-model-service" +{{- range $key, $value := .Values.configMap }} + {{ $key }}: "{{ $value }}" +{{- end }} \ No newline at end of file diff --git a/deployment/helm/templates/connector-pvc.yaml b/deployment/helm/templates/connector-pvc.yaml deleted file mode 100644 index 41c41c3cffa..00000000000 --- a/deployment/helm/templates/connector-pvc.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if and .Values.persistence.connector.enabled (not .Values.persistence.connector.existingClaim)}} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "danswer-stack.fullname" . }}-connector - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - accessModes: - {{- range .Values.persistence.connector.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.connector.size | quote }} - {{- with .Values.persistence.connector.storageClassName }} - storageClassName: {{ . }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/deployment/helm/templates/danswer-secret.yaml b/deployment/helm/templates/danswer-secret.yaml new file mode 100644 index 00000000000..6b2aa317204 --- /dev/null +++ b/deployment/helm/templates/danswer-secret.yaml @@ -0,0 +1,11 @@ +{{- if not .Values.auth.existingSecret -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "danswer-stack.secretName" . }} +type: Opaque +stringData: + {{- range $name, $value := .Values.auth.secrets }} + {{ $name }}: {{ $value | quote }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/deployment/helm/templates/dynamic-pvc.yaml b/deployment/helm/templates/dynamic-pvc.yaml deleted file mode 100644 index 703b33acb59..00000000000 --- a/deployment/helm/templates/dynamic-pvc.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if and .Values.persistence.dynamic.enabled (not .Values.persistence.dynamic.existingClaim)}} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "danswer-stack.fullname" . }}-dynamic - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - accessModes: - {{- range .Values.persistence.dynamic.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.dynamic.size | quote }} - {{- with .Values.persistence.dynamic.storageClassName }} - storageClassName: {{ . }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/deployment/kubernetes/charts/danswer-stack/templates/indexing-model-deployment.yaml b/deployment/helm/templates/indexing-model-deployment.yaml similarity index 91% rename from deployment/kubernetes/charts/danswer-stack/templates/indexing-model-deployment.yaml rename to deployment/helm/templates/indexing-model-deployment.yaml index 5bd866b55c4..cc88aefb79a 100644 --- a/deployment/kubernetes/charts/danswer-stack/templates/indexing-model-deployment.yaml +++ b/deployment/helm/templates/indexing-model-deployment.yaml @@ -34,11 +34,10 @@ spec: envFrom: - configMapRef: name: {{ .Values.config.envConfigMapName }} - {{- if .Values.indexCapability.indexingOnly }} env: - name: INDEXING_ONLY - value: "{{ .Values.indexCapability.indexingOnly }}" - {{- end }} + value: "{{ default "True" .Values.indexCapability.indexingOnly }}" + {{- include "danswer-stack.envSecrets" . | nindent 10}} volumeMounts: {{- range .Values.indexCapability.volumeMounts }} - name: {{ .name }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/indexing-model-pvc.yaml b/deployment/helm/templates/indexing-model-pvc.yaml similarity index 100% rename from deployment/kubernetes/charts/danswer-stack/templates/indexing-model-pvc.yaml rename to deployment/helm/templates/indexing-model-pvc.yaml diff --git a/deployment/kubernetes/charts/danswer-stack/templates/indexing-model-service.yaml b/deployment/helm/templates/indexing-model-service.yaml similarity index 100% rename from deployment/kubernetes/charts/danswer-stack/templates/indexing-model-service.yaml rename to deployment/helm/templates/indexing-model-service.yaml diff --git a/deployment/kubernetes/charts/danswer-stack/templates/inference-model-deployment.yaml b/deployment/helm/templates/inference-model-deployment.yaml similarity index 95% rename from deployment/kubernetes/charts/danswer-stack/templates/inference-model-deployment.yaml rename to deployment/helm/templates/inference-model-deployment.yaml index a09dfa4fbb5..43caddd29c3 100644 --- a/deployment/kubernetes/charts/danswer-stack/templates/inference-model-deployment.yaml +++ b/deployment/helm/templates/inference-model-deployment.yaml @@ -30,6 +30,8 @@ spec: envFrom: - configMapRef: name: {{ .Values.config.envConfigMapName }} + env: + {{- include "danswer-stack.envSecrets" . | nindent 12}} volumeMounts: {{- range .Values.inferenceCapability.deployment.volumeMounts }} - name: {{ .name }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/inference-model-pvc.yaml b/deployment/helm/templates/inference-model-pvc.yaml similarity index 100% rename from deployment/kubernetes/charts/danswer-stack/templates/inference-model-pvc.yaml rename to deployment/helm/templates/inference-model-pvc.yaml diff --git a/deployment/kubernetes/charts/danswer-stack/templates/inference-model-service.yaml b/deployment/helm/templates/inference-model-service.yaml similarity index 100% rename from deployment/kubernetes/charts/danswer-stack/templates/inference-model-service.yaml rename to deployment/helm/templates/inference-model-service.yaml diff --git a/deployment/helm/templates/ingress.yaml b/deployment/helm/templates/ingress.yaml deleted file mode 100644 index cfbef35dd7d..00000000000 --- a/deployment/helm/templates/ingress.yaml +++ /dev/null @@ -1,60 +0,0 @@ -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "danswer-stack.fullname" . -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} - {{- end }} -{{- end }} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} - pathType: {{ .pathType }} - {{- end }} - backend: - {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} - service: - name: {{ (list $fullName .service) | join "-" }} - port: - number: {{ .servicePort }} - {{- else }} - serviceName: {{ (list $fullName .service) | join "-" }} - servicePort: {{ .servicePort }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/nginx-conf.yaml b/deployment/helm/templates/nginx-conf.yaml similarity index 92% rename from deployment/kubernetes/charts/danswer-stack/templates/nginx-conf.yaml rename to deployment/helm/templates/nginx-conf.yaml index 383151ea1d3..81ecbaaa2f6 100644 --- a/deployment/kubernetes/charts/danswer-stack/templates/nginx-conf.yaml +++ b/deployment/helm/templates/nginx-conf.yaml @@ -5,7 +5,7 @@ metadata: data: nginx.conf: | upstream api_server { - server {{ include "danswer-stack.fullname" . }}-api:{{ .Values.api.service.port }} fail_timeout=0; + server {{ include "danswer-stack.fullname" . }}-api-service:{{ .Values.api.service.port }} fail_timeout=0; } upstream web_server { diff --git a/deployment/helm/templates/secret.yaml b/deployment/helm/templates/secret.yaml deleted file mode 100755 index 58bfba87d95..00000000000 --- a/deployment/helm/templates/secret.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "danswer-stack.fullname" . }} - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -data: -{{- range $key, $value := .Values.secrets }} - {{ $key }}: '{{ $value | b64enc }}' -{{- end }} diff --git a/deployment/helm/templates/vespa-service.yaml b/deployment/helm/templates/vespa-service.yaml deleted file mode 100644 index 01216a28970..00000000000 --- a/deployment/helm/templates/vespa-service.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "danswer-stack.fullname" . }}-vespa - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - type: ClusterIP - ports: - - name: vespa-tenant-port - protocol: TCP - port: 19070 - targetPort: 19070 - - name: vespa-tenant-port-2 - protocol: TCP - port: 19071 - targetPort: 19071 - - name: vespa-port - protocol: TCP - port: 8080 - targetPort: 8080 - selector: - {{- include "danswer-stack.selectorLabels" . | nindent 4 }} diff --git a/deployment/helm/templates/vespa-statefulset.yaml b/deployment/helm/templates/vespa-statefulset.yaml deleted file mode 100644 index 674b52bc447..00000000000 --- a/deployment/helm/templates/vespa-statefulset.yaml +++ /dev/null @@ -1,83 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: {{ include "danswer-stack.fullname" . }}-vespa - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.vespa.replicaCount }} - selector: - matchLabels: - {{- include "danswer-stack.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.vespa.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "danswer-stack.labels" . | nindent 8 }} - {{- with .Values.vespa.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "danswer-stack.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.vespa.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.vespa.securityContext | nindent 12 }} - image: "{{ .Values.vespa.image.repository }}:{{ .Values.vespa.image.tag }}" - imagePullPolicy: {{ .Values.vespa.image.pullPolicy }} - ports: - - containerPort: 19070 - - containerPort: 19071 - - containerPort: 8081 - livenessProbe: - httpGet: - path: /state/v1/health - port: 19071 - scheme: HTTP - readinessProbe: - httpGet: - path: /state/v1/health - port: 19071 - scheme: HTTP - resources: - {{- toYaml .Values.vespa.resources | nindent 12 }} - volumeMounts: - - name: vespa-storage - mountPath: /opt/vespa/var/ - {{- with .Values.vespa.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.vespa.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.vespa.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- if .Values.persistence.vespa.enabled }} - volumeClaimTemplates: - - metadata: - name: vespa-storage - spec: - accessModes: - {{- range .Values.persistence.vespa.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.vespa.size | quote }} - {{- with .Values.persistence.vespa.storageClassName }} - storageClassName: {{ . }} - {{- end }} - {{- end }} \ No newline at end of file diff --git a/deployment/helm/templates/webserver-deployment.yaml b/deployment/helm/templates/webserver-deployment.yaml index c679e6e0a21..c3505248fc6 100644 --- a/deployment/helm/templates/webserver-deployment.yaml +++ b/deployment/helm/templates/webserver-deployment.yaml @@ -11,6 +11,9 @@ spec: selector: matchLabels: {{- include "danswer-stack.selectorLabels" . | nindent 6 }} + {{- if .Values.webserver.deploymentLabels }} + {{- toYaml .Values.webserver.deploymentLabels | nindent 6 }} + {{- end }} template: metadata: {{- with .Values.webserver.podAnnotations }} @@ -31,7 +34,7 @@ spec: securityContext: {{- toYaml .Values.webserver.podSecurityContext | nindent 8 }} containers: - - name: {{ .Chart.Name }} + - name: web-server securityContext: {{- toYaml .Values.webserver.securityContext | nindent 12 }} image: "{{ .Values.webserver.image.repository }}:{{ .Values.webserver.image.tag | default .Chart.AppVersion }}" @@ -40,37 +43,13 @@ spec: - name: http containerPort: {{ .Values.webserver.service.port }} protocol: TCP - livenessProbe: - httpGet: - path: / - port: http - readinessProbe: - httpGet: - path: / - port: http resources: {{- toYaml .Values.webserver.resources | nindent 12 }} envFrom: - configMapRef: - name: {{ include "danswer-stack.fullname" . }} + name: {{ .Values.config.envConfigMapName }} env: - - name: INTERNAL_URL - value: {{ (list "http://" (include "danswer-stack.fullname" .) "-api:" .Values.api.service.port | join "") | quote }} - - name: VESPA_HOST - value: {{ (list (include "danswer-stack.fullname" .) "vespa" | join "-") }} - {{- if .Values.postgresql.enabled }} - - name: POSTGRES_HOST - value: {{ (list .Release.Name "postgresql" | join "-") }} - - name: POSTGRES_DB - value: {{ .Values.postgresql.auth.database }} - - name: POSTGRES_USER - value: {{ .Values.postgresql.auth.username }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ (list .Release.Name "postgresql" | join "-") }} - key: password - {{- end }} + {{- include "danswer-stack.envSecrets" . | nindent 12}} {{- with .Values.webserver.volumeMounts }} volumeMounts: {{- toYaml . | nindent 12 }} @@ -79,15 +58,3 @@ spec: volumes: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.webserver.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.webserver.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.webserver.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/deployment/helm/templates/webserver-service.yaml b/deployment/helm/templates/webserver-service.yaml index 776b65f8f96..3e33566fce1 100644 --- a/deployment/helm/templates/webserver-service.yaml +++ b/deployment/helm/templates/webserver-service.yaml @@ -4,6 +4,9 @@ metadata: name: {{ include "danswer-stack.fullname" . }}-webserver labels: {{- include "danswer-stack.labels" . | nindent 4 }} + {{- if .Values.webserver.deploymentLabels }} + {{- toYaml .Values.webserver.deploymentLabels | nindent 4 }} + {{- end }} spec: type: {{ .Values.webserver.service.type }} ports: @@ -13,3 +16,6 @@ spec: name: http selector: {{- include "danswer-stack.selectorLabels" . | nindent 4 }} + {{- if .Values.webserver.deploymentLabels }} + {{- toYaml .Values.webserver.deploymentLabels | nindent 4 }} + {{- end }} diff --git a/deployment/helm/values.yaml b/deployment/helm/values.yaml index 8d994b55ff3..8ef4167a6eb 100644 --- a/deployment/helm/values.yaml +++ b/deployment/helm/values.yaml @@ -6,9 +6,68 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +inferenceCapability: + service: + name: inference-model-server-service + type: ClusterIP + port: 9000 + pvc: + name: inference-model-pvc + accessModes: + - ReadWriteOnce + storage: 3Gi + deployment: + name: inference-model-server-deployment + replicas: 1 + labels: + - key: app + value: inference-model-server + image: + repository: danswer/danswer-model-server + tag: latest + pullPolicy: IfNotPresent + command: ["uvicorn", "model_server.main:app", "--host", "0.0.0.0", "--port", "9000"] + port: 9000 + volumeMounts: + - name: inference-model-storage + mountPath: /root/.cache + volumes: + - name: inference-model-storage + persistentVolumeClaim: + claimName: inference-model-pvc + podLabels: + - key: app + value: inference-model-server + +indexCapability: + service: + type: ClusterIP + port: 9000 + name: indexing-model-server-port + deploymentLabels: + app: indexing-model-server + podLabels: + app: indexing-model-server + indexingOnly: "True" + podAnnotations: {} + volumeMounts: + - name: indexing-model-storage + mountPath: /root/.cache + volumes: + - name: indexing-model-storage + persistentVolumeClaim: + claimName: indexing-model-storage + indexingModelPVC: + name: indexing-model-storage + accessMode: "ReadWriteOnce" + storage: "3Gi" + +config: + envConfigMapName: env-configmap + serviceAccount: # Specifies whether a service account should be created - create: true + create: false # Automatically mount a ServiceAccount's API credentials? automount: true # Annotations to add to the service account @@ -17,6 +76,31 @@ serviceAccount: # If not set and create is true, a name is generated using the fullname template name: "" +postgresql: + primary: + persistence: + size: 5Gi + enabled: true + auth: + existingSecret: danswer-secrets + secretKeys: + adminPasswordKey: postgres_password #overwriting as postgres typically expects 'postgres-password' + +nginx: + containerPorts: + http: 1024 + extraEnvVars: + - name: DOMAIN + value: localhost + service: + ports: + http: 80 + danswer: 3000 + targetPort: + http: http + danswer: http + + existingServerBlockConfigmap: danswer-nginx-conf webserver: replicaCount: 1 @@ -25,10 +109,11 @@ webserver: pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" - + deploymentLabels: + app: web-server podAnnotations: {} - podLabels: {} - + podLabels: + app: web-server podSecurityContext: {} # fsGroup: 2000 @@ -87,10 +172,12 @@ api: pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "" - + deploymentLabels: + app: api-server podAnnotations: {} podLabels: scope: danswer-backend + app: api-server podSecurityContext: {} # fsGroup: 2000 @@ -107,17 +194,17 @@ api: type: ClusterIP port: 8080 - resources: + resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - requests: - cpu: 1500m - memory: 2Gi - # limits: - # cpu: 100m - # memory: 128Mi + # requests: + # cpu: 1000m # Requests 1 CPU core + # memory: 1Gi # Requests 1 GiB of memory + # limits: + # cpu: 2000m # Limits to 2 CPU cores + # memory: 2Gi # Limits to 2 GiB of memory autoscaling: enabled: false @@ -141,16 +228,7 @@ api: nodeSelector: {} tolerations: [] - affinity: - podAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - labelSelector: - matchExpressions: - - key: scope - operator: In - values: - - danswer-backend - topologyKey: "kubernetes.io/hostname" + background: replicaCount: 1 @@ -158,11 +236,13 @@ background: repository: danswer/danswer-backend pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "" + tag: latest podAnnotations: {} podLabels: scope: danswer-backend - + app: background + deploymentLabels: + app: background podSecurityContext: {} # fsGroup: 2000 @@ -173,18 +253,18 @@ background: # readOnlyRootFilesystem: true # runAsNonRoot: true # runAsUser: 1000 - - resources: + enableMiniChunk: "true" + resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - requests: - cpu: 2500m - memory: 5Gi - # limits: - # cpu: 100m - # memory: 128Mi + # requests: + # cpu: 1000m # Requests 1 CPU core + # memory: 1Gi # Requests 1 GiB of memory + # limits: + # cpu: 2000m # Limits to 2 CPU cores + # memory: 2Gi # Limits to 2 GiB of memory autoscaling: enabled: false @@ -208,25 +288,19 @@ background: nodeSelector: {} tolerations: [] - affinity: - podAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - - labelSelector: - matchExpressions: - - key: scope - operator: In - values: - - danswer-backend - topologyKey: "kubernetes.io/hostname" vespa: replicaCount: 1 image: - repository: vespaengine/vespa + repository: vespa pullPolicy: IfNotPresent tag: "8.277.17" podAnnotations: {} - podLabels: {} + podLabels: + app: vespa + app.kubernetes.io/instance: danswer + app.kubernetes.io/name: vespa + enabled: true podSecurityContext: {} # fsGroup: 2000 @@ -242,16 +316,14 @@ vespa: # runAsUser: 1000 resources: - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # The Vespa Helm chart specifies default resources, which are quite modest. We override + # them here to increase chances of the chart running successfully. requests: - cpu: 2500m - memory: 5Gi - # limits: - # cpu: 100m - # memory: 128Mi + cpu: 1500m + memory: 4000Mi + # limits: + # cpu: 100m + # memory: 128Mi nodeSelector: {} tolerations: [] @@ -281,97 +353,99 @@ persistence: storageClassName: "" accessModes: - ReadWriteOnce - size: 1Gi - connector: - enabled: true - existingClaim: "" - storageClassName: "" - accessModes: - - ReadWriteOnce - size: 1Gi - dynamic: - enabled: true - existingClaim: "" - storageClassName: "" - accessModes: - - ReadWriteOnce - size: 1Gi - -postgresql: - enabled: false - auth: - postgresPassword: "" - username: danswer - password: danswer - database: danswer - -config: - # Auth Setting, also check the secrets file - #AUTH_TYPE: "disabled" # Change this for production uses unless Danswer is only accessible behind VPN - #SESSION_EXPIRE_TIME_SECONDS: "86400" # 1 Day Default - #VALID_EMAIL_DOMAINS: "" # Can be something like danswer.ai, as an extra double-check - #SMTP_SERVER: "" # For sending verification emails, if unspecified then defaults to 'smtp.gmail.com' - #SMTP_PORT: "" # For sending verification emails, if unspecified then defaults to '587' - #SMTP_USER: "" # 'your-email@company.com' - #SMTP_PASS: "" # 'your-gmail-password' - #EMAIL_FROM: "" # 'your-email@company.com' SMTP_USER missing used instead + size: 5Gi + +auth: + # for storing smtp, oauth, slack, and other secrets + # keys are lowercased version of env vars (e.g. SMTP_USER -> smtp_user) + existingSecret: "" # danswer-secrets + # optionally override the secret keys to reference in the secret + secretKeys: + postgres_password: "postgres_password" + smtp_pass: "" + oauth_client_id: "" + oauth_client_secret: "" + oauth_cookie_secret: "" + gen_ai_api_key: "" + danswer_bot_slack_app_token: "" + danswer_bot_slack_bot_token: "" + # will be overridden by the existingSecret if set + secretName: "danswer-secrets" + # set values as strings, they will be base64 encoded + secrets: + postgres_password: "postgres" + smtp_pass: "" + oauth_client_id: "" + oauth_client_secret: "" + oauth_cookie_secret: "" + gen_ai_api_key: "" + danswer_bot_slack_app_token: "" + danswer_bot_slack_bot_token: "" + +configMap: + AUTH_TYPE: "disabled" # Change this for production uses unless Danswer is only accessible behind VPN + SESSION_EXPIRE_TIME_SECONDS: "86400" # 1 Day Default + VALID_EMAIL_DOMAINS: "" # Can be something like danswer.ai, as an extra double-check + SMTP_SERVER: "" # For sending verification emails, if unspecified then defaults to 'smtp.gmail.com' + SMTP_PORT: "" # For sending verification emails, if unspecified then defaults to '587' + SMTP_USER: "" # 'your-email@company.com' + # SMTP_PASS: "" # 'your-gmail-password' + EMAIL_FROM: "" # 'your-email@company.com' SMTP_USER missing used instead # Gen AI Settings - #GEN_AI_MODEL_PROVIDER: "openai" - #GEN_AI_MODEL_VERSION: "gpt-4" # "gpt-3.5-turbo-0125" # Use GPT-4 if you have it - #FAST_GEN_AI_MODEL_VERSION: "gpt-3.5-turbo-0125" - #GEN_AI_API_KEY: "" - #GEN_AI_API_ENDPOINT: "" - #GEN_AI_API_VERSION: "" - #GEN_AI_LLM_PROVIDER_TYPE: "" - #GEN_AI_MAX_TOKENS: "" - #QA_TIMEOUT: "60" - #MAX_CHUNKS_FED_TO_CHAT: "" - #DISABLE_LLM_FILTER_EXTRACTION: "" - #DISABLE_LLM_CHUNK_FILTER: "" - #DISABLE_LLM_CHOOSE_SEARCH: "" + GEN_AI_MODEL_PROVIDER: "" + GEN_AI_MODEL_VERSION: "" + FAST_GEN_AI_MODEL_VERSION: "" + # GEN_AI_API_KEY: "" + GEN_AI_API_ENDPOINT: "" + GEN_AI_API_VERSION: "" + GEN_AI_LLM_PROVIDER_TYPE: "" + GEN_AI_MAX_TOKENS: "" + QA_TIMEOUT: "60" + MAX_CHUNKS_FED_TO_CHAT: "" + DISABLE_LLM_FILTER_EXTRACTION: "" + DISABLE_LLM_CHUNK_FILTER: "" + DISABLE_LLM_CHOOSE_SEARCH: "" + DISABLE_LLM_QUERY_REPHRASE: "" # Query Options - #DOC_TIME_DECAY: "" - #HYBRID_ALPHA: "" - #EDIT_KEYWORD_QUERY: "" - #MULTILINGUAL_QUERY_EXPANSION: "" - #QA_PROMPT_OVERRIDE: "" + DOC_TIME_DECAY: "" + HYBRID_ALPHA: "" + EDIT_KEYWORD_QUERY: "" + MULTILINGUAL_QUERY_EXPANSION: "" + QA_PROMPT_OVERRIDE: "" # Don't change the NLP models unless you know what you're doing - #DOCUMENT_ENCODER_MODEL: "" - #NORMALIZE_EMBEDDINGS: "" - #ASYM_QUERY_PREFIX: "" - #ASYM_PASSAGE_PREFIX: "" - #ENABLE_RERANKING_REAL_TIME_FLOW: "" - #ENABLE_RERANKING_ASYNC_FLOW: "" - #MODEL_SERVER_HOST: "" - #MODEL_SERVER_PORT: "" - #INDEXING_MODEL_SERVER_HOST: "" - #MIN_THREADS_ML_MODELS: "" + DOCUMENT_ENCODER_MODEL: "" + NORMALIZE_EMBEDDINGS: "" + ASYM_QUERY_PREFIX: "" + ASYM_PASSAGE_PREFIX: "" + ENABLE_RERANKING_REAL_TIME_FLOW: "" + ENABLE_RERANKING_ASYNC_FLOW: "" + MODEL_SERVER_PORT: "" + MIN_THREADS_ML_MODELS: "" # Indexing Configs - #NUM_INDEXING_WORKERS: "" - #DASK_JOB_CLIENT_ENABLED: "" - #CONTINUE_ON_CONNECTOR_FAILURE: "" - #EXPERIMENTAL_CHECKPOINTING_ENABLED: "" - #CONFLUENCE_CONNECTOR_LABELS_TO_SKIP: "" - #GONG_CONNECTOR_START_TIME: "" - #NOTION_CONNECTOR_ENABLE_RECURSIVE_PAGE_LOOKUP: "" + NUM_INDEXING_WORKERS: "" + DISABLE_INDEX_UPDATE_ON_SWAP: "" + DASK_JOB_CLIENT_ENABLED: "" + CONTINUE_ON_CONNECTOR_FAILURE: "" + EXPERIMENTAL_CHECKPOINTING_ENABLED: "" + CONFLUENCE_CONNECTOR_LABELS_TO_SKIP: "" + JIRA_API_VERSION: "" + GONG_CONNECTOR_START_TIME: "" + NOTION_CONNECTOR_ENABLE_RECURSIVE_PAGE_LOOKUP: "" # DanswerBot SlackBot Configs - #DANSWER_BOT_SLACK_APP_TOKEN: "" - #DANSWER_BOT_SLACK_BOT_TOKEN: "" - #DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER: "" - #DANSWER_BOT_DISPLAY_ERROR_MSGS: "" - #DANSWER_BOT_RESPOND_EVERY_CHANNEL: "" - #DANSWER_BOT_DISABLE_COT: "" # Currently unused - #NOTIFY_SLACKBOT_NO_ANSWER: "" + # DANSWER_BOT_SLACK_APP_TOKEN: "" + # DANSWER_BOT_SLACK_BOT_TOKEN: "" + DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER: "" + DANSWER_BOT_DISPLAY_ERROR_MSGS: "" + DANSWER_BOT_RESPOND_EVERY_CHANNEL: "" + DANSWER_BOT_DISABLE_COT: "" # Currently unused + NOTIFY_SLACKBOT_NO_ANSWER: "" # Logging # Optional Telemetry, please keep it on (nothing sensitive is collected)? <3 # https://docs.danswer.dev/more/telemetry - #DISABLE_TELEMETRY: "" - #LOG_LEVEL: "" - #LOG_ALL_MODEL_INTERACTIONS: "" - #LOG_VESPA_TIMING_INFORMATION: "" + DISABLE_TELEMETRY: "" + LOG_LEVEL: "" + LOG_ALL_MODEL_INTERACTIONS: "" + LOG_VESPA_TIMING_INFORMATION: "" # Shared or Non-backend Related - #INTERNAL_URL: "http://api-server-service:80" # for web server WEB_DOMAIN: "http://localhost:3000" # for web server and api server - # Other Services - #POSTGRES_HOST: "relational-db-service" - #VESPA_HOST: "document-index-service" \ No newline at end of file + DOMAIN: "localhost" # for nginx diff --git a/deployment/kubernetes/api_server-service-deployment.yaml b/deployment/kubernetes/api_server-service-deployment.yaml index 63d86ded5eb..eeac5fecc96 100644 --- a/deployment/kubernetes/api_server-service-deployment.yaml +++ b/deployment/kubernetes/api_server-service-deployment.yaml @@ -41,18 +41,17 @@ spec: - containerPort: 8080 # There are some extra values since this is shared between services # There are no conflicts though, extra env variables are simply ignored + env: + - name: OAUTH_CLIENT_ID + valueFrom: + secretKeyRef: + name: danswer-secrets + key: google_oauth_client_id + - name: OAUTH_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: danswer-secrets + key: google_oauth_client_secret envFrom: - configMapRef: name: env-configmap - volumeMounts: - - name: dynamic-storage - mountPath: /home/storage - - name: file-connector-storage - mountPath: /home/file_connector_storage - volumes: - - name: dynamic-storage - persistentVolumeClaim: - claimName: dynamic-pvc - - name: file-connector-storage - persistentVolumeClaim: - claimName: file-connector-pvc diff --git a/deployment/kubernetes/background-deployment.yaml b/deployment/kubernetes/background-deployment.yaml index 77bfc65fe09..82369e6d3e8 100644 --- a/deployment/kubernetes/background-deployment.yaml +++ b/deployment/kubernetes/background-deployment.yaml @@ -22,15 +22,3 @@ spec: envFrom: - configMapRef: name: env-configmap - volumeMounts: - - name: dynamic-storage - mountPath: /home/storage - - name: file-connector-storage - mountPath: /home/file_connector_storage - volumes: - - name: dynamic-storage - persistentVolumeClaim: - claimName: dynamic-pvc - - name: file-connector-storage - persistentVolumeClaim: - claimName: file-connector-pvc diff --git a/deployment/kubernetes/charts/danswer-stack/.gitignore b/deployment/kubernetes/charts/danswer-stack/.gitignore deleted file mode 100644 index b442275d6b5..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -### Helm ### -# Chart dependencies -**/charts/*.tgz diff --git a/deployment/kubernetes/charts/danswer-stack/.helmignore b/deployment/kubernetes/charts/danswer-stack/.helmignore deleted file mode 100644 index 0e8a0eb36f4..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/deployment/kubernetes/charts/danswer-stack/Chart.lock b/deployment/kubernetes/charts/danswer-stack/Chart.lock deleted file mode 100644 index c7f54c8c1b9..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/Chart.lock +++ /dev/null @@ -1,12 +0,0 @@ -dependencies: -- name: postgresql - repository: https://charts.bitnami.com/bitnami - version: 14.3.1 -- name: vespa - repository: https://unoplat.github.io/vespa-helm-charts - version: 0.2.2 -- name: nginx - repository: oci://registry-1.docker.io/bitnamicharts - version: 15.14.0 -digest: sha256:53e138c0ab12193f57a76c2f377e2a5d3d11c394b03eef5f6848dfae6705cb61 -generated: "2024-03-27T12:34:11.548396+05:30" diff --git a/deployment/kubernetes/charts/danswer-stack/Chart.yaml b/deployment/kubernetes/charts/danswer-stack/Chart.yaml deleted file mode 100644 index 0819afa8725..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/Chart.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: v2 -name: danswer-stack -description: A Helm chart for Kubernetes -home: https://www.danswer.ai/ -sources: - - "https://github.com/danswer-ai/danswer" -type: application -version: 0.1.0 -appVersion: "v0.3.72" -annotations: - category: Productivity - licenses: MIT - images: | - - name: webserver - image: docker.io/danswer/danswer-web-server:v0.3.72 - - name: background - image: docker.io/danswer/danswer-backend:v0.3.72 - - name: vespa - image: vespaengine/vespa:8.277.17 -dependencies: - - name: postgresql - version: 14.3.1 - repository: https://charts.bitnami.com/bitnami - condition: postgresql.enabled - - name: vespa - version: 0.2.3 - repository: https://unoplat.github.io/vespa-helm-charts - condition: vespa.enabled - - name: nginx - version: 15.14.0 - repository: oci://registry-1.docker.io/bitnamicharts - condition: nginx.enabled - - - \ No newline at end of file diff --git a/deployment/kubernetes/charts/danswer-stack/templates/_helpers.tpl b/deployment/kubernetes/charts/danswer-stack/templates/_helpers.tpl deleted file mode 100644 index 4e6672fd677..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "danswer-stack.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "danswer-stack.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "danswer-stack.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "danswer-stack.labels" -}} -helm.sh/chart: {{ include "danswer-stack.chart" . }} -{{ include "danswer-stack.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "danswer-stack.selectorLabels" -}} -app.kubernetes.io/name: {{ include "danswer-stack.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "danswer-stack.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "danswer-stack.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/api-connector-pvc.yaml b/deployment/kubernetes/charts/danswer-stack/templates/api-connector-pvc.yaml deleted file mode 100644 index 7dee7f368ff..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/api-connector-pvc.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if and .Values.persistence.api.connector.enabled}} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "danswer-stack.fullname" . }}-api-connector - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - accessModes: - {{- range .Values.persistence.api.connector.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.api.connector.size | quote }} - {{- with .Values.persistence.api.connector.storageClassName }} - storageClassName: {{ . }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/deployment/kubernetes/charts/danswer-stack/templates/api-deployment.yaml b/deployment/kubernetes/charts/danswer-stack/templates/api-deployment.yaml deleted file mode 100644 index da43266baaf..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/api-deployment.yaml +++ /dev/null @@ -1,79 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "danswer-stack.fullname" . }}-api - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - {{- if not .Values.api.autoscaling.enabled }} - replicas: {{ .Values.api.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "danswer-stack.selectorLabels" . | nindent 6 }} - {{- if .Values.api.deploymentLabels }} - {{- toYaml .Values.api.deploymentLabels | nindent 6 }} - {{- end }} - template: - metadata: - {{- with .Values.api.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "danswer-stack.labels" . | nindent 8 }} - {{- with .Values.api.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "danswer-stack.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.api.podSecurityContext | nindent 8 }} - containers: - - name: api-server - securityContext: - {{- toYaml .Values.api.securityContext | nindent 12 }} - image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.api.image.pullPolicy }} - command: - - "/bin/sh" - - "-c" - - | - alembic upgrade head && - echo "Starting Danswer Api Server" && - uvicorn danswer.main:app --host 0.0.0.0 --port 8080 - ports: - - name: api-server-port - containerPort: {{ .Values.api.service.port }} - protocol: TCP - resources: - {{- toYaml .Values.api.resources | nindent 12 }} - envFrom: - - configMapRef: - name: {{ .Values.config.envConfigMapName }} - volumeMounts: - - name: dynamic-storage - mountPath: /home/storage - - name: file-connector-storage - mountPath: /home/file_connector_storage - {{- if .Values.api.volumeMounts }} - {{- .Values.api.volumeMounts | toYaml | nindent 12}} - {{- end }} - volumes: - - name: dynamic-storage - persistentVolumeClaim: - claimName: {{ include "danswer-stack.fullname" . }}-api-dynamic - - name: file-connector-storage - {{- if .Values.persistence.api.connector.enabled }} - persistentVolumeClaim: - claimName: {{ include "danswer-stack.fullname" . }}-api-connector - {{- else }} - emptyDir: { } - {{- end }} - {{- if .Values.api.volumes }} - {{- .Values.api.volumes | toYaml | nindent 8}} - {{- end }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/api-hpa.yaml b/deployment/kubernetes/charts/danswer-stack/templates/api-hpa.yaml deleted file mode 100644 index 378c39715ad..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/api-hpa.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.api.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "danswer-stack.fullname" . }}-api - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "danswer-stack.fullname" . }} - minReplicas: {{ .Values.api.autoscaling.minReplicas }} - maxReplicas: {{ .Values.api.autoscaling.maxReplicas }} - metrics: - {{- if .Values.api.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.api.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.api.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.api.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/api-pvc.yaml b/deployment/kubernetes/charts/danswer-stack/templates/api-pvc.yaml deleted file mode 100644 index 8d155d0ca45..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/api-pvc.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if and .Values.persistence.api.dynamic.enabled}} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "danswer-stack.fullname" . }}-api-dynamic - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - accessModes: - {{- range .Values.persistence.api.dynamic.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.api.dynamic.size | quote }} - {{- with .Values.persistence.api.dynamic.storageClassName }} - storageClassName: {{ . }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/deployment/kubernetes/charts/danswer-stack/templates/api-service.yaml b/deployment/kubernetes/charts/danswer-stack/templates/api-service.yaml deleted file mode 100644 index 820e53d1076..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/api-service.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "danswer-stack.fullname" . }}-api - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} - {{- if .Values.api.deploymentLabels }} - {{- toYaml .Values.api.deploymentLabels | nindent 4 }} - {{- end }} -spec: - type: {{ .Values.api.service.type }} - ports: - - port: {{ .Values.api.service.port }} - targetPort: api-server-port - protocol: TCP - name: api-server-port - selector: - {{- include "danswer-stack.selectorLabels" . | nindent 4 }} - {{- if .Values.api.deploymentLabels }} - {{- toYaml .Values.api.deploymentLabels | nindent 4 }} - {{- end }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/background-connector-pvc.yaml b/deployment/kubernetes/charts/danswer-stack/templates/background-connector-pvc.yaml deleted file mode 100644 index 3bf00905769..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/background-connector-pvc.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if and .Values.persistence.api.connector.enabled}} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "danswer-stack.fullname" . }}-bg-connector - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - accessModes: - {{- range .Values.persistence.background.connector.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.background.connector.size | quote }} - {{- with .Values.persistence.background.connector.storageClassName }} - storageClassName: {{ . }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/deployment/kubernetes/charts/danswer-stack/templates/background-deployment.yaml b/deployment/kubernetes/charts/danswer-stack/templates/background-deployment.yaml deleted file mode 100644 index 7e795c2a68c..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/background-deployment.yaml +++ /dev/null @@ -1,76 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "danswer-stack.fullname" . }}-background - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - {{- if not .Values.background.autoscaling.enabled }} - replicas: {{ .Values.background.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "danswer-stack.selectorLabels" . | nindent 6 }} - {{- if .Values.background.deploymentLabels }} - {{- toYaml .Values.background.deploymentLabels | nindent 6 }} - {{- end }} - template: - metadata: - {{- with .Values.background.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "danswer-stack.labels" . | nindent 8 }} - {{- with .Values.background.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "danswer-stack.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.background.podSecurityContext | nindent 8 }} - containers: - - name: background - securityContext: - {{- toYaml .Values.background.securityContext | nindent 12 }} - image: "{{ .Values.background.image.repository }}:{{ .Values.background.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.background.image.pullPolicy }} - command: ["/usr/bin/supervisord"] - resources: - {{- toYaml .Values.background.resources | nindent 12 }} - envFrom: - - configMapRef: - name: {{ .Values.config.envConfigMapName }} - env: - - name: ENABLE_MINI_CHUNK - value: "{{ .Values.background.enableMiniChunk }}" - volumeMounts: - - name: dynamic-storage - mountPath: /home/storage - - name: connector-storage - mountPath: /home/file_connector_storage - {{- if .Values.background.volumeMounts }} - {{- .Values.background.volumeMounts | toYaml | nindent 12}} - {{- end }} - volumes: - - name: dynamic-storage - {{- if .Values.persistence.background.dynamic.enabled }} - persistentVolumeClaim: - claimName: {{ include "danswer-stack.fullname" . }}-bg-dynamic - {{- else }} - emptyDir: { } - {{- end }} - - name: connector-storage - {{- if .Values.persistence.background.connector.enabled }} - persistentVolumeClaim: - claimName: {{ include "danswer-stack.fullname" . }}-bg-connector - {{- else }} - emptyDir: { } - {{- end }} - {{- if .Values.background.volumes }} - {{- .Values.background.volumes | toYaml | nindent 8}} - {{- end }} \ No newline at end of file diff --git a/deployment/kubernetes/charts/danswer-stack/templates/background-dynamic-pvc.yaml b/deployment/kubernetes/charts/danswer-stack/templates/background-dynamic-pvc.yaml deleted file mode 100644 index 8cbadd40ac0..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/background-dynamic-pvc.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if and .Values.persistence.api.dynamic.enabled}} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "danswer-stack.fullname" . }}-bg-dynamic - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - accessModes: - {{- range .Values.persistence.background.dynamic.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.background.dynamic.size | quote }} - {{- with .Values.persistence.background.dynamic.storageClassName }} - storageClassName: {{ . }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/deployment/kubernetes/charts/danswer-stack/templates/background-hpa.yaml b/deployment/kubernetes/charts/danswer-stack/templates/background-hpa.yaml deleted file mode 100644 index 009daf10f05..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/background-hpa.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.background.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "danswer-stack.fullname" . }}-background - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "danswer-stack.fullname" . }} - minReplicas: {{ .Values.background.autoscaling.minReplicas }} - maxReplicas: {{ .Values.background.autoscaling.maxReplicas }} - metrics: - {{- if .Values.background.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.background.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.background.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.background.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/configmap.yaml b/deployment/kubernetes/charts/danswer-stack/templates/configmap.yaml deleted file mode 100755 index b9812342f01..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/configmap.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: env-configmap - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -data: - INTERNAL_URL: "{{ include "danswer-stack.fullname" . }}-api:80" - POSTGRES_HOST: {{ .Release.Name }}-postgresql - VESPA_HOST: "document-index-service" - MODEL_SERVER_HOST: "{{ include "danswer-stack.fullname" . }}-inference-model-service" - INDEXING_MODEL_SERVER_HOST: "{{ include "danswer-stack.fullname" . }}-indexing-model-service" -{{- range $key, $value := .Values.configMap }} - {{ $key }}: "{{ $value }}" -{{- end }} \ No newline at end of file diff --git a/deployment/kubernetes/charts/danswer-stack/templates/danswer-secret.yaml b/deployment/kubernetes/charts/danswer-stack/templates/danswer-secret.yaml deleted file mode 100644 index 2586048f05b..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/danswer-secret.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: danswer-secrets -type: Opaque -data: - postgres_user: cG9zdGdyZXM= # "postgres" base64 encoded - postgres_password: cGFzc3dvcmQ= # "password" base64 encoded - postgres-password: cGFzc3dvcmQ= - google_oauth_client_id: # You will need to provide this, use echo -n "your-client-id" | base64 - google_oauth_client_secret: # You \ No newline at end of file diff --git a/deployment/kubernetes/charts/danswer-stack/templates/serviceaccount.yaml b/deployment/kubernetes/charts/danswer-stack/templates/serviceaccount.yaml deleted file mode 100644 index afd351217ba..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/serviceaccount.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "danswer-stack.serviceAccountName" . }} - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automount }} -{{- end }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/tests/test-connection.yaml b/deployment/kubernetes/charts/danswer-stack/templates/tests/test-connection.yaml deleted file mode 100644 index 60fbd1054c1..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "danswer-stack.fullname" . }}-test-connection" - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "danswer-stack.fullname" . }}:{{ .Values.webserver.service.port }}'] - restartPolicy: Never diff --git a/deployment/kubernetes/charts/danswer-stack/templates/webserver-deployment.yaml b/deployment/kubernetes/charts/danswer-stack/templates/webserver-deployment.yaml deleted file mode 100644 index 20225a3ccd7..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/webserver-deployment.yaml +++ /dev/null @@ -1,58 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "danswer-stack.fullname" . }}-webserver - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - {{- if not .Values.webserver.autoscaling.enabled }} - replicas: {{ .Values.webserver.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "danswer-stack.selectorLabels" . | nindent 6 }} - {{- if .Values.webserver.deploymentLabels }} - {{- toYaml .Values.webserver.deploymentLabels | nindent 6 }} - {{- end }} - template: - metadata: - {{- with .Values.webserver.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "danswer-stack.labels" . | nindent 8 }} - {{- with .Values.webserver.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "danswer-stack.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.webserver.podSecurityContext | nindent 8 }} - containers: - - name: web-server - securityContext: - {{- toYaml .Values.webserver.securityContext | nindent 12 }} - image: "{{ .Values.webserver.image.repository }}:{{ .Values.webserver.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.webserver.image.pullPolicy }} - ports: - - name: http - containerPort: {{ .Values.webserver.service.port }} - protocol: TCP - resources: - {{- toYaml .Values.webserver.resources | nindent 12 }} - envFrom: - - configMapRef: - name: {{ .Values.config.envConfigMapName }} - {{- with .Values.webserver.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.webserver.volumes }} - volumes: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/webserver-hpa.yaml b/deployment/kubernetes/charts/danswer-stack/templates/webserver-hpa.yaml deleted file mode 100644 index b46820a7fac..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/webserver-hpa.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.webserver.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "danswer-stack.fullname" . }}-webserver - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "danswer-stack.fullname" . }} - minReplicas: {{ .Values.webserver.autoscaling.minReplicas }} - maxReplicas: {{ .Values.webserver.autoscaling.maxReplicas }} - metrics: - {{- if .Values.webserver.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.webserver.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.webserver.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.webserver.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/deployment/kubernetes/charts/danswer-stack/templates/webserver-service.yaml b/deployment/kubernetes/charts/danswer-stack/templates/webserver-service.yaml deleted file mode 100644 index 3e33566fce1..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/templates/webserver-service.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "danswer-stack.fullname" . }}-webserver - labels: - {{- include "danswer-stack.labels" . | nindent 4 }} - {{- if .Values.webserver.deploymentLabels }} - {{- toYaml .Values.webserver.deploymentLabels | nindent 4 }} - {{- end }} -spec: - type: {{ .Values.webserver.service.type }} - ports: - - port: {{ .Values.webserver.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include "danswer-stack.selectorLabels" . | nindent 4 }} - {{- if .Values.webserver.deploymentLabels }} - {{- toYaml .Values.webserver.deploymentLabels | nindent 4 }} - {{- end }} diff --git a/deployment/kubernetes/charts/danswer-stack/values.yaml b/deployment/kubernetes/charts/danswer-stack/values.yaml deleted file mode 100644 index 7a825d62367..00000000000 --- a/deployment/kubernetes/charts/danswer-stack/values.yaml +++ /dev/null @@ -1,463 +0,0 @@ -# Default values for danswer-stack. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -inferenceCapability: - service: - name: inference-model-server-service - type: ClusterIP - port: 9000 - pvc: - name: inference-model-pvc - accessModes: - - ReadWriteOnce - storage: 3Gi - deployment: - name: inference-model-server-deployment - replicas: 1 - labels: - - key: app - value: inference-model-server - image: - repository: danswer/danswer-model-server - tag: latest - pullPolicy: IfNotPresent - command: ["uvicorn", "model_server.main:app", "--host", "0.0.0.0", "--port", "9000"] - port: 9000 - volumeMounts: - - name: inference-model-storage - mountPath: /root/.cache - volumes: - - name: inference-model-storage - persistentVolumeClaim: - claimName: inference-model-pvc - podLabels: - - key: app - value: inference-model-server - - - -config: - envConfigMapName: env-configmap - -serviceAccount: - # Specifies whether a service account should be created - create: false - # Automatically mount a ServiceAccount's API credentials? - automount: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -indexCapability: - service: - type: ClusterIP - port: 9000 - name: indexing-model-server-port - deploymentLabels: - app: indexing-model-server - podLabels: - app: indexing-model-server - indexingOnly: "True" - podAnnotations: {} - volumeMounts: - - name: indexing-model-storage - mountPath: /root/.cache - volumes: - - name: indexing-model-storage - persistentVolumeClaim: - claimName: indexing-model-storage - indexingModelPVC: - name: indexing-model-storage - accessMode: "ReadWriteOnce" - storage: "3Gi" - -postgresql: - primary: - persistence: - size: 1Gi - enabled: true - auth: - existingSecret: danswer-secrets - # secretKeys: - # adminPasswordKey: postgres_password - -nginx: - containerPorts: - http: 1024 - extraEnvVars: - - name: DOMAIN - value: localhost - service: - ports: - http: 80 - danswer: 3000 - targetPort: - http: http - danswer: http - - existingServerBlockConfigmap: danswer-nginx-conf - -webserver: - replicaCount: 1 - image: - repository: danswer/danswer-web-server - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "" - deploymentLabels: - app: web-server - podAnnotations: {} - podLabels: - app: web-server - podSecurityContext: {} - # fsGroup: 2000 - - securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - - service: - type: ClusterIP - port: 3000 - - resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - - autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - - # Additional volumes on the output Deployment definition. - volumes: [] - # - name: foo - # secret: - # secretName: mysecret - # optional: false - - # Additional volumeMounts on the output Deployment definition. - volumeMounts: [] - # - name: foo - # mountPath: "/etc/foo" - # readOnly: true - - nodeSelector: {} - tolerations: [] - affinity: {} - -api: - replicaCount: 1 - image: - repository: danswer/danswer-backend - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "" - deploymentLabels: - app: api-server - podAnnotations: {} - podLabels: - scope: danswer-backend - app: api-server - - podSecurityContext: {} - # fsGroup: 2000 - - securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - - service: - type: ClusterIP - port: 8080 - - resources: - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - requests: - cpu: 500m - memory: 128Mi - limits: - cpu: 900m - memory: 512Mi - - autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - - # Additional volumes on the output Deployment definition. - volumes: [] - # - name: foo - # secret: - # secretName: mysecret - # optional: false - - # Additional volumeMounts on the output Deployment definition. - volumeMounts: [] - # - name: foo - # mountPath: "/etc/foo" - # readOnly: true - - nodeSelector: {} - tolerations: [] - - -background: - replicaCount: 1 - image: - repository: danswer/danswer-backend - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: latest - podAnnotations: {} - podLabels: - scope: danswer-backend - app: background - deploymentLabels: - app: background - podSecurityContext: {} - # fsGroup: 2000 - - securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - enableMiniChunk: "true" - resources: - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - requests: - cpu: 500m - memory: 500Mi - limits: - cpu: 900m - memory: 800Mi - - autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - - # Additional volumes on the output Deployment definition. - volumes: [] - # - name: foo - # secret: - # secretName: mysecret - # optional: false - - # Additional volumeMounts on the output Deployment definition. - volumeMounts: [] - # - name: foo - # mountPath: "/etc/foo" - # readOnly: true - - nodeSelector: {} - tolerations: [] - -vespa: - replicaCount: 1 - image: - repository: vespa - pullPolicy: IfNotPresent - tag: "8.277.17" - podAnnotations: {} - podLabels: - app: vespa - app.kubernetes.io/instance: danswer-stack-kn - app.kubernetes.io/name: vespa - enabled: true - - podSecurityContext: {} - # fsGroup: 2000 - - securityContext: - privileged: true - runAsUser: 0 - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - - resources: - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # requests: - # cpu: 1500m - # memory: 4000Mi - # # limits: - # # cpu: 100m - # # memory: 128Mi - - nodeSelector: {} - tolerations: [] - affinity: {} - - -#ingress: -# enabled: false -# className: "" -# annotations: {} -# # kubernetes.io/ingress.class: nginx -# # kubernetes.io/tls-acme: "true" -# hosts: -# - host: chart-example.local -# paths: -# - path: / -# pathType: ImplementationSpecific -# tls: [] -# # - secretName: chart-example-tls -# # hosts: -# # - chart-example.local - -persistence: - vespa: - enabled: true - existingClaim: "" - storageClassName: "" - accessModes: - - ReadWriteOnce - size: 1Gi - connector: - enabled: true - existingClaim: "" - storageClassName: "" - accessModes: - - ReadWriteOnce - size: 1Gi - api: - connector: - enabled: true - storageClassName: "" - accessModes: - - ReadWriteOnce - size: 1Gi - dynamic: - enabled: true - storageClassName: "" - accessModes: - - ReadWriteOnce - size: 1Gi - background: - connector: - enabled: true - storageClassName: "" - accessModes: - - ReadWriteOnce - size: 1Gi - dynamic: - enabled: true - storageClassName: "" - accessModes: - - ReadWriteOnce - size: 1Gi - -configMap: - AUTH_TYPE: "disabled" # Change this for production uses unless Danswer is only accessible behind VPN - SESSION_EXPIRE_TIME_SECONDS: "86400" # 1 Day Default - VALID_EMAIL_DOMAINS: "" # Can be something like danswer.ai, as an extra double-check - SMTP_SERVER: "" # For sending verification emails, if unspecified then defaults to 'smtp.gmail.com' - SMTP_PORT: "" # For sending verification emails, if unspecified then defaults to '587' - SMTP_USER: "" # 'your-email@company.com' - SMTP_PASS: "" # 'your-gmail-password' - EMAIL_FROM: "" # 'your-email@company.com' SMTP_USER missing used instead - # Gen AI Settings - GEN_AI_MODEL_PROVIDER: "" - GEN_AI_MODEL_VERSION: "" - FAST_GEN_AI_MODEL_VERSION: "" - GEN_AI_API_KEY: "" - GEN_AI_API_ENDPOINT: "" - GEN_AI_API_VERSION: "" - GEN_AI_LLM_PROVIDER_TYPE: "" - GEN_AI_MAX_TOKENS: "" - QA_TIMEOUT: "60" - MAX_CHUNKS_FED_TO_CHAT: "" - DISABLE_LLM_FILTER_EXTRACTION: "" - DISABLE_LLM_CHUNK_FILTER: "" - DISABLE_LLM_CHOOSE_SEARCH: "" - DISABLE_LLM_QUERY_REPHRASE: "" - # Query Options - DOC_TIME_DECAY: "" - HYBRID_ALPHA: "" - EDIT_KEYWORD_QUERY: "" - MULTILINGUAL_QUERY_EXPANSION: "" - QA_PROMPT_OVERRIDE: "" - # Don't change the NLP models unless you know what you're doing - DOCUMENT_ENCODER_MODEL: "" - NORMALIZE_EMBEDDINGS: "" - ASYM_QUERY_PREFIX: "" - ASYM_PASSAGE_PREFIX: "" - ENABLE_RERANKING_REAL_TIME_FLOW: "" - ENABLE_RERANKING_ASYNC_FLOW: "" - - MODEL_SERVER_PORT: "" - - MIN_THREADS_ML_MODELS: "" - # Indexing Configs - NUM_INDEXING_WORKERS: "" - DISABLE_INDEX_UPDATE_ON_SWAP: "" - DASK_JOB_CLIENT_ENABLED: "" - CONTINUE_ON_CONNECTOR_FAILURE: "" - EXPERIMENTAL_CHECKPOINTING_ENABLED: "" - CONFLUENCE_CONNECTOR_LABELS_TO_SKIP: "" - JIRA_API_VERSION: "" - GONG_CONNECTOR_START_TIME: "" - NOTION_CONNECTOR_ENABLE_RECURSIVE_PAGE_LOOKUP: "" - # DanswerBot SlackBot Configs - DANSWER_BOT_SLACK_APP_TOKEN: "" - DANSWER_BOT_SLACK_BOT_TOKEN: "" - DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER: "" - DANSWER_BOT_DISPLAY_ERROR_MSGS: "" - DANSWER_BOT_RESPOND_EVERY_CHANNEL: "" - DANSWER_BOT_DISABLE_COT: "" # Currently unused - NOTIFY_SLACKBOT_NO_ANSWER: "" - # Logging - # Optional Telemetry, please keep it on (nothing sensitive is collected)? <3 - # https://docs.danswer.dev/more/telemetry - DISABLE_TELEMETRY: "" - LOG_LEVEL: "" - LOG_ALL_MODEL_INTERACTIONS: "" - LOG_VESPA_TIMING_INFORMATION: "" - # Shared or Non-backend Related - WEB_DOMAIN: "http://localhost:3000" # for web server and api server - DOMAIN: "localhost" # for nginx diff --git a/deployment/kubernetes/persistent-volumes.yaml b/deployment/kubernetes/persistent-volumes.yaml deleted file mode 100644 index 8376b98e697..00000000000 --- a/deployment/kubernetes/persistent-volumes.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: dynamic-pvc -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: file-connector-pvc -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi diff --git a/deployment/kubernetes/postgres-service-deployment.yaml b/deployment/kubernetes/postgres-service-deployment.yaml index f33efa2bafd..17330204c1e 100644 --- a/deployment/kubernetes/postgres-service-deployment.yaml +++ b/deployment/kubernetes/postgres-service-deployment.yaml @@ -54,4 +54,4 @@ spec: resources: requests: # Adjust the storage request size as needed. - storage: 1Gi + storage: 5Gi diff --git a/deployment/kubernetes/secrets.yaml b/deployment/kubernetes/secrets.yaml index 8193092f4a9..c135a29f676 100644 --- a/deployment/kubernetes/secrets.yaml +++ b/deployment/kubernetes/secrets.yaml @@ -7,5 +7,5 @@ type: Opaque data: postgres_user: cG9zdGdyZXM= # "postgres" base64 encoded postgres_password: cGFzc3dvcmQ= # "password" base64 encoded - google_oauth_client_id: # You will need to provide this, use echo -n "your-client-id" | base64 - google_oauth_client_secret: # You will need to provide this, use echo -n "your-client-id" | base64 + google_oauth_client_id: ZXhhbXBsZS1jbGllbnQtaWQ= # "example-client-id" base64 encoded. You will need to provide this, use echo -n "your-client-id" | base64 + google_oauth_client_secret: example_google_oauth_secret # "example-client-secret" base64 encoded. You will need to provide this, use echo -n "your-client-id" | base64 diff --git a/deployment/kubernetes/vespa-service-deployment.yaml b/deployment/kubernetes/vespa-service-deployment.yaml index 4fa5aa9fac5..5016258b757 100644 --- a/deployment/kubernetes/vespa-service-deployment.yaml +++ b/deployment/kubernetes/vespa-service-deployment.yaml @@ -60,4 +60,4 @@ spec: resources: requests: # Adjust the storage request size as needed. - storage: 1Gi + storage: 5Gi diff --git a/web/Dockerfile b/web/Dockerfile index a0b16c75d3b..3d27813c7ae 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -54,6 +54,9 @@ ENV NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PRED ARG NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS ENV NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS} +ARG NEXT_PUBLIC_DISABLE_LOGOUT +ENV NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT} + RUN npm run build # Step 3. Production image, copy all the files and run next @@ -99,6 +102,9 @@ ENV NEXT_PUBLIC_POSITIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_POSITIVE_PRED ARG NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS ENV NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS=${NEXT_PUBLIC_NEGATIVE_PREDEFINED_FEEDBACK_OPTIONS} +ARG NEXT_PUBLIC_DISABLE_LOGOUT +ENV NEXT_PUBLIC_DISABLE_LOGOUT=${NEXT_PUBLIC_DISABLE_LOGOUT} + # Note: Don't expose ports here, Compose will handle that for us if necessary. # If you want to run this without compose, specify the ports to # expose via cli diff --git a/web/public/Dropbox.png b/web/public/Dropbox.png new file mode 100644 index 00000000000..cd83e09eb66 Binary files /dev/null and b/web/public/Dropbox.png differ diff --git a/web/src/app/admin/assistants/AssistantEditor.tsx b/web/src/app/admin/assistants/AssistantEditor.tsx index 8c1f01973b2..d6427a67929 100644 --- a/web/src/app/admin/assistants/AssistantEditor.tsx +++ b/web/src/app/admin/assistants/AssistantEditor.tsx @@ -34,6 +34,9 @@ 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"; +import { checkLLMSupportsImageInput } from "@/lib/llm/utils"; function findSearchTool(tools: ToolSnapshot[]) { return tools.find((tool) => tool.in_code_tool_id === "SearchTool"); @@ -43,12 +46,6 @@ function findImageGenerationTool(tools: ToolSnapshot[]) { return tools.find((tool) => tool.in_code_tool_id === "ImageGenerationTool"); } -function checkLLMSupportsImageGeneration(provider: string, model: string) { - console.log(provider); - console.log(model); - return provider === "openai" && model === "gpt-4-turbo"; -} - function Label({ children }: { children: string | JSX.Element }) { return (
{children}
@@ -68,6 +65,7 @@ export function AssistantEditor({ redirectType, llmProviders, tools, + shouldAddAssistantToUserPreferences, }: { existingPersona?: Persona | null; ccPairs: CCPairBasicInfo[]; @@ -77,6 +75,7 @@ export function AssistantEditor({ redirectType: SuccessfulPersonaUpdateRedirectType; llmProviders: FullLLMProvider[]; tools: ToolSnapshot[]; + shouldAddAssistantToUserPreferences?: boolean; }) { const router = useRouter(); const { popup, setPopup } = usePopup(); @@ -259,7 +258,7 @@ export function AssistantEditor({ if ( values.image_generation_tool_enabled && imageGenerationTool && - checkLLMSupportsImageGeneration( + checkLLMSupportsImageInput( providerDisplayNameToProviderName.get( values.llm_model_provider_override || "" ) || @@ -288,7 +287,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 +296,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 +320,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}` ); } }} @@ -541,7 +563,7 @@ export function AssistantEditor({ )} {imageGenerationTool && - checkLLMSupportsImageGeneration( + checkLLMSupportsImageInput( providerDisplayNameToProviderName.get( values.llm_model_provider_override || "" ) || 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/admin/connectors/axero/page.tsx b/web/src/app/admin/connectors/axero/page.tsx index ccabc380c81..6d4a5af8bcd 100644 --- a/web/src/app/admin/connectors/axero/page.tsx +++ b/web/src/app/admin/connectors/axero/page.tsx @@ -2,7 +2,8 @@ import * as Yup from "yup"; import { AxeroIcon, TrashIcon } from "@/components/icons/icons"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import useSWR, { useSWRConfig } from "swr"; import { LoadingAnimation } from "@/components/Loading"; import { HealthCheckBanner } from "@/components/health/healthcheck"; @@ -17,8 +18,6 @@ import { CredentialForm } from "@/components/admin/connectors/CredentialForm"; import { TextFormField, TextArrayFieldBuilder, - BooleanFormField, - TextArrayField, } from "@/components/admin/connectors/Field"; import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -31,16 +30,16 @@ const MainSection = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -51,12 +50,22 @@ const MainSection = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const axeroConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/bookstack/page.tsx b/web/src/app/admin/connectors/bookstack/page.tsx index f1e8e787402..dbf8bd367b1 100644 --- a/web/src/app/admin/connectors/bookstack/page.tsx +++ b/web/src/app/admin/connectors/bookstack/page.tsx @@ -12,7 +12,8 @@ import { Credential, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -29,15 +30,15 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -48,12 +49,29 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if ( + (!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) || + (!credentialsData && isCredentialsLoading) + ) { + return ; } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); + } + + if (credentialsError || !credentialsData) { + return ( + + ); } const bookstackConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/confluence/page.tsx b/web/src/app/admin/connectors/confluence/page.tsx index 649d8853eb6..25c69cdbd0a 100644 --- a/web/src/app/admin/connectors/confluence/page.tsx +++ b/web/src/app/admin/connectors/confluence/page.tsx @@ -12,7 +12,8 @@ import { Credential, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -63,15 +64,15 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -82,12 +83,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const confluenceConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/discourse/page.tsx b/web/src/app/admin/connectors/discourse/page.tsx index 5a2459760bb..9ba840d3d92 100644 --- a/web/src/app/admin/connectors/discourse/page.tsx +++ b/web/src/app/admin/connectors/discourse/page.tsx @@ -15,7 +15,8 @@ import { DiscourseCredentialJson, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -32,16 +33,16 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -52,12 +53,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const discourseConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/document360/page.tsx b/web/src/app/admin/connectors/document360/page.tsx index 8d7c03190e5..85653c639e3 100644 --- a/web/src/app/admin/connectors/document360/page.tsx +++ b/web/src/app/admin/connectors/document360/page.tsx @@ -2,7 +2,8 @@ import * as Yup from "yup"; import { TrashIcon, Document360Icon } from "@/components/icons/icons"; // Make sure you have a Document360 icon -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import useSWR, { useSWRConfig } from "swr"; import { LoadingAnimation } from "@/components/Loading"; import { HealthCheckBanner } from "@/components/health/healthcheck"; @@ -29,16 +30,16 @@ const MainSection = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -49,12 +50,22 @@ const MainSection = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const document360ConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/dropbox/page.tsx b/web/src/app/admin/connectors/dropbox/page.tsx new file mode 100644 index 00000000000..3e8196f3f8b --- /dev/null +++ b/web/src/app/admin/connectors/dropbox/page.tsx @@ -0,0 +1,222 @@ +"use client"; + +import { AdminPageTitle } from "@/components/admin/Title"; +import { HealthCheckBanner } from "@/components/health/healthcheck"; +import { DropboxIcon } from "@/components/icons/icons"; +import { LoadingAnimation } from "@/components/Loading"; +import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; +import { CredentialForm } from "@/components/admin/connectors/CredentialForm"; +import { TextFormField } from "@/components/admin/connectors/Field"; +import { usePopup } from "@/components/admin/connectors/Popup"; +import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable"; +import { TrashIcon } from "@/components/icons/icons"; +import { adminDeleteCredential, linkCredential } from "@/lib/credential"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; +import { usePublicCredentials } from "@/lib/hooks"; +import { + ConnectorIndexingStatus, + Credential, + DropboxConfig, + DropboxCredentialJson, +} from "@/lib/types"; +import { Card, Text, Title } from "@tremor/react"; +import useSWR, { useSWRConfig } from "swr"; +import * as Yup from "yup"; + +const Main = () => { + const { popup, setPopup } = usePopup(); + + const { mutate } = useSWRConfig(); + const { + data: connectorIndexingStatuses, + isLoading: isConnectorIndexingStatusesLoading, + error: connectorIndexingStatusesError, + } = useSWR[]>( + "/api/manage/admin/connector/indexing-status", + errorHandlingFetcher + ); + const { + data: credentialsData, + isLoading: isCredentialsLoading, + error: credentialsError, + refreshCredentials, + } = usePublicCredentials(); + + if ( + (!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) || + (!credentialsData && isCredentialsLoading) + ) { + return ; + } + + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); + } + + if (credentialsError || !credentialsData) { + return ( + + ); + } + + const dropboxConnectorIndexingStatuses: ConnectorIndexingStatus< + DropboxConfig, + DropboxCredentialJson + >[] = connectorIndexingStatuses.filter( + (connectorIndexingStatus) => + connectorIndexingStatus.connector.source === "dropbox" + ); + const dropboxCredential: Credential | undefined = + credentialsData.find( + (credential) => credential.credential_json?.dropbox_access_token + ); + + return ( + <> + {popup} + + Provide your API details + + + {dropboxCredential ? ( + <> +
+

Existing API Token:

+

+ {dropboxCredential.credential_json?.dropbox_access_token} +

+ +
+ + ) : ( + <> + + 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 + + + Due to Dropbox's access key design, the Dropbox connector will only + re-index files after a new access key is provided and the indexing + process is re-run manually. Check the docs for more information. + +
+ + 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} // disabled re-indexing + credentialId={dropboxCredential.id} + /> +
+ + )} + + ); +}; + +export default function Page() { + return ( +
+
+ +
+ } title="Dropbox" /> +
+
+ ); +} diff --git a/web/src/app/admin/connectors/file/page.tsx b/web/src/app/admin/connectors/file/page.tsx index a8193729ecb..3e4af0a85d4 100644 --- a/web/src/app/admin/connectors/file/page.tsx +++ b/web/src/app/admin/connectors/file/page.tsx @@ -4,7 +4,8 @@ import useSWR, { useSWRConfig } from "swr"; import * as Yup from "yup"; import { FileIcon } from "@/components/icons/icons"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { HealthCheckBanner } from "@/components/health/healthcheck"; import { ConnectorIndexingStatus, FileConfig } from "@/lib/types"; import { createCredential, linkCredential } from "@/lib/credential"; @@ -33,7 +34,7 @@ const Main = () => { isLoading: isConnectorIndexingStatusesLoading, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); if (!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) { diff --git a/web/src/app/admin/connectors/github/page.tsx b/web/src/app/admin/connectors/github/page.tsx index c5346c73a6a..28a918f3b07 100644 --- a/web/src/app/admin/connectors/github/page.tsx +++ b/web/src/app/admin/connectors/github/page.tsx @@ -5,7 +5,8 @@ import { GithubIcon, TrashIcon } from "@/components/icons/icons"; import { TextFormField } from "@/components/admin/connectors/Field"; import { HealthCheckBanner } from "@/components/health/healthcheck"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { GithubConfig, GithubCredentialJson, @@ -26,16 +27,16 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -46,12 +47,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const githubConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/gitlab/page.tsx b/web/src/app/admin/connectors/gitlab/page.tsx index 37eddca30bc..595cd575f76 100644 --- a/web/src/app/admin/connectors/gitlab/page.tsx +++ b/web/src/app/admin/connectors/gitlab/page.tsx @@ -5,7 +5,8 @@ import { GitlabIcon, TrashIcon } from "@/components/icons/icons"; import { TextFormField } from "@/components/admin/connectors/Field"; import { HealthCheckBanner } from "@/components/health/healthcheck"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { GitlabConfig, GitlabCredentialJson, @@ -26,16 +27,16 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -46,12 +47,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const gitlabConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/gmail/page.tsx b/web/src/app/admin/connectors/gmail/page.tsx index 041421a3672..e81db40047c 100644 --- a/web/src/app/admin/connectors/gmail/page.tsx +++ b/web/src/app/admin/connectors/gmail/page.tsx @@ -3,7 +3,8 @@ import * as Yup from "yup"; import { GmailIcon } from "@/components/icons/icons"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup"; import { HealthCheckBanner } from "@/components/health/healthcheck"; @@ -113,7 +114,7 @@ const Main = () => { error: isAppCredentialError, } = useSWR<{ client_id: string }>( "/api/manage/admin/connector/gmail/app-credential", - fetcher + errorHandlingFetcher ); const { data: serviceAccountKeyData, @@ -121,20 +122,20 @@ const Main = () => { error: isServiceAccountKeyError, } = useSWR<{ service_account_email: string }>( "/api/manage/admin/connector/gmail/service-account-key", - fetcher + errorHandlingFetcher ); const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -153,7 +154,7 @@ const Main = () => { ); } - if (isCredentialsError || !credentialsData) { + if (credentialsError || !credentialsData) { return (
Failed to load credentials.
@@ -161,7 +162,7 @@ const Main = () => { ); } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { return (
Failed to load connectors.
diff --git a/web/src/app/admin/connectors/gong/page.tsx b/web/src/app/admin/connectors/gong/page.tsx index e617450d123..5fda45d517e 100644 --- a/web/src/app/admin/connectors/gong/page.tsx +++ b/web/src/app/admin/connectors/gong/page.tsx @@ -15,7 +15,8 @@ import { GongCredentialJson, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -32,16 +33,16 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -52,12 +53,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const gongConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/google-drive/page.tsx b/web/src/app/admin/connectors/google-drive/page.tsx index bd6cb252d3b..2dae6c572de 100644 --- a/web/src/app/admin/connectors/google-drive/page.tsx +++ b/web/src/app/admin/connectors/google-drive/page.tsx @@ -3,7 +3,8 @@ import * as Yup from "yup"; import { GoogleDriveIcon } from "@/components/icons/icons"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup"; import { HealthCheckBanner } from "@/components/health/healthcheck"; @@ -265,7 +266,7 @@ const Main = () => { error: isAppCredentialError, } = useSWR<{ client_id: string }>( "/api/manage/admin/connector/google-drive/app-credential", - fetcher + errorHandlingFetcher ); const { data: serviceAccountKeyData, @@ -273,20 +274,20 @@ const Main = () => { error: isServiceAccountKeyError, } = useSWR<{ service_account_email: string }>( "/api/manage/admin/connector/google-drive/service-account-key", - fetcher + errorHandlingFetcher ); const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -305,7 +306,7 @@ const Main = () => { ); } - if (isCredentialsError || !credentialsData) { + if (credentialsError || !credentialsData) { return (
Failed to load credentials.
@@ -313,7 +314,7 @@ const Main = () => { ); } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { return (
Failed to load connectors.
diff --git a/web/src/app/admin/connectors/google-sites/page.tsx b/web/src/app/admin/connectors/google-sites/page.tsx index 1bca39c0cd7..45ea4bcd1d9 100644 --- a/web/src/app/admin/connectors/google-sites/page.tsx +++ b/web/src/app/admin/connectors/google-sites/page.tsx @@ -5,7 +5,8 @@ import * as Yup from "yup"; import { LoadingAnimation } from "@/components/Loading"; import { GoogleSitesIcon } from "@/components/icons/icons"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { TextFormField } from "@/components/admin/connectors/Field"; import { HealthCheckBanner } from "@/components/health/healthcheck"; import { ConnectorIndexingStatus, GoogleSitesConfig } from "@/lib/types"; @@ -29,10 +30,10 @@ export default function GoogleSites() { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const googleSitesIndexingStatuses: ConnectorIndexingStatus< @@ -211,7 +212,7 @@ export default function GoogleSites() { {isConnectorIndexingStatusesLoading ? ( - ) : isConnectorIndexingStatusesError || !connectorIndexingStatuses ? ( + ) : connectorIndexingStatusesError || !connectorIndexingStatuses ? (
Error loading indexing history
) : googleSitesIndexingStatuses.length > 0 ? ( diff --git a/web/src/app/admin/connectors/guru/page.tsx b/web/src/app/admin/connectors/guru/page.tsx index e302f538918..094bbe7c75b 100644 --- a/web/src/app/admin/connectors/guru/page.tsx +++ b/web/src/app/admin/connectors/guru/page.tsx @@ -12,7 +12,8 @@ import { GuruCredentialJson, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -29,17 +30,17 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, isValidating: isCredentialsValidating, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -51,12 +52,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const guruConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/hubspot/page.tsx b/web/src/app/admin/connectors/hubspot/page.tsx index a75ad71a7b3..199c027b6e7 100644 --- a/web/src/app/admin/connectors/hubspot/page.tsx +++ b/web/src/app/admin/connectors/hubspot/page.tsx @@ -12,7 +12,8 @@ import { HubSpotCredentialJson, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -29,17 +30,17 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, isValidating: isCredentialsValidating, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -51,12 +52,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const hubSpotConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/jira/page.tsx b/web/src/app/admin/connectors/jira/page.tsx index a9449f5f076..f960348e6da 100644 --- a/web/src/app/admin/connectors/jira/page.tsx +++ b/web/src/app/admin/connectors/jira/page.tsx @@ -15,7 +15,8 @@ import { ConnectorIndexingStatus, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -44,15 +45,15 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, isValidating: isCredentialsValidating, refreshCredentials, } = usePublicCredentials(); @@ -65,12 +66,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const jiraConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/linear/page.tsx b/web/src/app/admin/connectors/linear/page.tsx index 1b601ec8edd..6af018729dc 100644 --- a/web/src/app/admin/connectors/linear/page.tsx +++ b/web/src/app/admin/connectors/linear/page.tsx @@ -11,7 +11,8 @@ import { LinearCredentialJson, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -28,15 +29,15 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -47,12 +48,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const linearConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/loopio/page.tsx b/web/src/app/admin/connectors/loopio/page.tsx index 8a9b3c67702..920d15b8246 100644 --- a/web/src/app/admin/connectors/loopio/page.tsx +++ b/web/src/app/admin/connectors/loopio/page.tsx @@ -12,7 +12,8 @@ import { LoopioCredentialJson, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -27,17 +28,17 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, isValidating: isCredentialsValidating, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -49,12 +50,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const loopioConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/mediawiki/page.tsx b/web/src/app/admin/connectors/mediawiki/page.tsx index f4dd04a6bd7..e0c17a6e72d 100644 --- a/web/src/app/admin/connectors/mediawiki/page.tsx +++ b/web/src/app/admin/connectors/mediawiki/page.tsx @@ -16,7 +16,8 @@ import { Credential, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -33,15 +34,15 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -52,12 +53,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const mediawikiConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/notion/page.tsx b/web/src/app/admin/connectors/notion/page.tsx index 4ffdde68a96..aa205dce0ee 100644 --- a/web/src/app/admin/connectors/notion/page.tsx +++ b/web/src/app/admin/connectors/notion/page.tsx @@ -12,7 +12,8 @@ import { ConnectorIndexingStatus, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -29,15 +30,15 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -48,12 +49,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const notionConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/productboard/page.tsx b/web/src/app/admin/connectors/productboard/page.tsx index 7b38ee42aa8..1694baa8ccb 100644 --- a/web/src/app/admin/connectors/productboard/page.tsx +++ b/web/src/app/admin/connectors/productboard/page.tsx @@ -12,7 +12,8 @@ import { Credential, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -29,15 +30,15 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, isValidating: isCredentialsValidating, refreshCredentials, } = usePublicCredentials(); @@ -50,12 +51,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const productboardConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/request-tracker/page.tsx b/web/src/app/admin/connectors/request-tracker/page.tsx index 27962aa21b4..147dd1ae2e6 100644 --- a/web/src/app/admin/connectors/request-tracker/page.tsx +++ b/web/src/app/admin/connectors/request-tracker/page.tsx @@ -2,7 +2,8 @@ import * as Yup from "yup"; import { TrashIcon, RequestTrackerIcon } from "@/components/icons/icons"; // Make sure you have a Document360 icon -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import useSWR, { useSWRConfig } from "swr"; import { LoadingAnimation } from "@/components/Loading"; import { HealthCheckBanner } from "@/components/health/healthcheck"; @@ -29,16 +30,16 @@ const MainSection = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -49,12 +50,22 @@ const MainSection = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const requestTrackerConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/sharepoint/page.tsx b/web/src/app/admin/connectors/sharepoint/page.tsx index 662bf1a443d..77f884675a1 100644 --- a/web/src/app/admin/connectors/sharepoint/page.tsx +++ b/web/src/app/admin/connectors/sharepoint/page.tsx @@ -2,7 +2,8 @@ import * as Yup from "yup"; import { TrashIcon, SharepointIcon } from "@/components/icons/icons"; // Make sure you have a Document360 icon -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import useSWR, { useSWRConfig } from "swr"; import { LoadingAnimation } from "@/components/Loading"; import { HealthCheckBanner } from "@/components/health/healthcheck"; @@ -29,16 +30,16 @@ const MainSection = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -49,12 +50,22 @@ const MainSection = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const sharepointConnectorIndexingStatuses: ConnectorIndexingStatus< @@ -222,11 +233,16 @@ const MainSection = () => { formBodyBuilder={TextArrayFieldBuilder({ name: "sites", label: "Sites:", - subtext: - "Specify 0 or more sites to index. For example, specifying the site " + - "'support' for the 'danswerai' sharepoint will cause us to only index documents " + - "within the 'https://danswerai.sharepoint.com/sites/support' site. " + - "If no sites are specified, all sites in your organization will be indexed.", + subtext: ( + <> +
+
    +
  • • If no sites are specified, all sites in your organization will be indexed (Sites.Read.All permission required).
  • +
  • • Specifying 'https://danswerai.sharepoint.com/sites/support' for example will only index documents within this site.
  • +
  • • Specifying 'https://danswerai.sharepoint.com/sites/support/subfolder' for example will only index documents within this folder.
  • +
+ + ), })} validationSchema={Yup.object().shape({ sites: Yup.array() diff --git a/web/src/app/admin/connectors/slab/page.tsx b/web/src/app/admin/connectors/slab/page.tsx index 0a99ed413fa..11dcd799e46 100644 --- a/web/src/app/admin/connectors/slab/page.tsx +++ b/web/src/app/admin/connectors/slab/page.tsx @@ -12,7 +12,8 @@ import { Credential, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -29,15 +30,15 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, isValidating: isCredentialsValidating, refreshCredentials, } = usePublicCredentials(); @@ -50,12 +51,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const slabConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/slack/page.tsx b/web/src/app/admin/connectors/slack/page.tsx index 989379f52d9..9352b0d776d 100644 --- a/web/src/app/admin/connectors/slack/page.tsx +++ b/web/src/app/admin/connectors/slack/page.tsx @@ -2,7 +2,8 @@ import * as Yup from "yup"; import { SlackIcon, TrashIcon } from "@/components/icons/icons"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import useSWR, { useSWRConfig } from "swr"; import { LoadingAnimation } from "@/components/Loading"; import { HealthCheckBanner } from "@/components/health/healthcheck"; @@ -31,16 +32,16 @@ const MainSection = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -51,12 +52,22 @@ const MainSection = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const slackConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/web/page.tsx b/web/src/app/admin/connectors/web/page.tsx index 60b90dda64d..547a164499a 100644 --- a/web/src/app/admin/connectors/web/page.tsx +++ b/web/src/app/admin/connectors/web/page.tsx @@ -9,7 +9,8 @@ import { GearIcon, ArrowSquareOutIcon, } from "@/components/icons/icons"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { SelectorFormField, TextFormField, @@ -33,10 +34,10 @@ export default function Web() { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const webIndexingStatuses: ConnectorIndexingStatus[] = @@ -125,7 +126,7 @@ export default function Web() { {isConnectorIndexingStatusesLoading ? ( - ) : isConnectorIndexingStatusesError || !connectorIndexingStatuses ? ( + ) : connectorIndexingStatusesError || !connectorIndexingStatuses ? (
Error loading indexing history
) : webIndexingStatuses.length > 0 ? ( diff --git a/web/src/app/admin/connectors/wikipedia/page.tsx b/web/src/app/admin/connectors/wikipedia/page.tsx index 02c89a620cf..f410b209a21 100644 --- a/web/src/app/admin/connectors/wikipedia/page.tsx +++ b/web/src/app/admin/connectors/wikipedia/page.tsx @@ -16,7 +16,8 @@ import { Credential, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -33,15 +34,15 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -52,12 +53,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const wikipediaConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/zendesk/page.tsx b/web/src/app/admin/connectors/zendesk/page.tsx index fe7239efe75..dac1fe76e49 100644 --- a/web/src/app/admin/connectors/zendesk/page.tsx +++ b/web/src/app/admin/connectors/zendesk/page.tsx @@ -12,7 +12,8 @@ import { Credential, } from "@/lib/types"; import useSWR, { useSWRConfig } from "swr"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import { LoadingAnimation } from "@/components/Loading"; import { adminDeleteCredential, linkCredential } from "@/lib/credential"; import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm"; @@ -29,15 +30,15 @@ const Main = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -48,12 +49,22 @@ const Main = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const zendeskConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/connectors/zulip/page.tsx b/web/src/app/admin/connectors/zulip/page.tsx index 33df93185d2..66a35df30a5 100644 --- a/web/src/app/admin/connectors/zulip/page.tsx +++ b/web/src/app/admin/connectors/zulip/page.tsx @@ -2,7 +2,8 @@ import * as Yup from "yup"; import { ZulipIcon, TrashIcon } from "@/components/icons/icons"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; +import { ErrorCallout } from "@/components/ErrorCallout"; import useSWR, { useSWRConfig } from "swr"; import { LoadingAnimation } from "@/components/Loading"; import { HealthCheckBanner } from "@/components/health/healthcheck"; @@ -26,16 +27,16 @@ const MainSection = () => { const { data: connectorIndexingStatuses, isLoading: isConnectorIndexingStatusesLoading, - error: isConnectorIndexingStatusesError, + error: connectorIndexingStatusesError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher + errorHandlingFetcher ); const { data: credentialsData, isLoading: isCredentialsLoading, - error: isCredentialsError, + error: credentialsError, refreshCredentials, } = usePublicCredentials(); @@ -46,12 +47,22 @@ const MainSection = () => { return ; } - if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { - return
Failed to load connectors
; + if (connectorIndexingStatusesError || !connectorIndexingStatuses) { + return ( + + ); } - if (isCredentialsError || !credentialsData) { - return
Failed to load credentials
; + if (credentialsError || !credentialsData) { + return ( + + ); } const zulipConnectorIndexingStatuses: ConnectorIndexingStatus< diff --git a/web/src/app/admin/indexing/status/page.tsx b/web/src/app/admin/indexing/status/page.tsx index 15aa7929af7..8cebea2349a 100644 --- a/web/src/app/admin/indexing/status/page.tsx +++ b/web/src/app/admin/indexing/status/page.tsx @@ -4,7 +4,7 @@ import useSWR from "swr"; import { LoadingAnimation } from "@/components/Loading"; import { NotebookIcon } from "@/components/icons/icons"; -import { fetcher } from "@/lib/fetcher"; +import { errorHandlingFetcher } from "@/lib/fetcher"; import { ConnectorIndexingStatus } from "@/lib/types"; import { CCPairIndexingStatusTable } from "./CCPairIndexingStatusTable"; import { AdminPageTitle } from "@/components/admin/Title"; @@ -15,10 +15,10 @@ function Main() { const { data: indexAttemptData, isLoading: indexAttemptIsLoading, - error: indexAttemptIsError, + error: indexAttemptError, } = useSWR[]>( "/api/manage/admin/connector/indexing-status", - fetcher, + errorHandlingFetcher, { refreshInterval: 10000 } // 10 seconds ); @@ -26,15 +26,19 @@ function Main() { return ; } - if (indexAttemptIsError || !indexAttemptData) { - return
Error loading indexing history.
; + if (indexAttemptError || !indexAttemptData) { + return ( +
+ {indexAttemptError?.info?.detail || "Error loading indexing history."} +
+ ); } if (indexAttemptData.length === 0) { return ( It looks like you don't have any connectors setup yet. Visit the{" "} - + Add Connector {" "} page to get started! 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 && ( + ) : ( + + )} +
+ )} +
+ {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..7ae625820b5 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,17 @@ 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] || availablePersonas[0]; const [chatSessionSharedStatus, setChatSessionSharedStatus] = useState(ChatSessionSharedStatus.Private); @@ -343,7 +346,7 @@ export function ChatPage({ useEffect(() => { if (messageHistory.length === 0 && chatSessionId === null) { setSelectedPersona( - availablePersonas.find( + filteredAssistants.find( (persona) => persona.id === defaultSelectedPersonaId ) ); @@ -519,6 +522,9 @@ export function ChatPage({ messageToResendParent || (currMessageHistory.length > 0 ? currMessageHistory[currMessageHistory.length - 1] + : null) || + (completeMessageMap.size === 1 + ? Array.from(completeMessageMap.values())[0] : null); // if we're resending, set the parent's child to null @@ -681,14 +687,14 @@ export function ChatPage({ message: currMessage, type: "user", files: currentMessageFiles, - parentMessageId: null, + parentMessageId: parentMessage?.messageId || SYSTEM_MESSAGE_ID, }, { messageId: TEMP_ASSISTANT_MESSAGE_ID, message: errorMsg, type: "error", files: aiMessageImages || [], - parentMessageId: null, + parentMessageId: TEMP_USER_MESSAGE_ID, }, ], completeMessageMapOverride: frozenCompleteMessageMap, @@ -878,8 +884,10 @@ export function ChatPage({ setActiveTab={setConfigModalActiveTab} onClose={() => setConfigModalActiveTab(null)} filterManager={filterManager} + availableAssistants={filteredAssistants} selectedAssistant={livePersona} setSelectedAssistant={onPersonaChange} + llmProviders={llmProviders} llmOverrideManager={llmOverrideManager} /> @@ -903,7 +911,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 ( { const input = document.createElement("input"); input.type = "file"; + input.multiple = true; // Allow multiple files input.onchange = (event: any) => { const files = Array.from( event?.target?.files || [] diff --git a/web/src/app/chat/message/CodeBlock.tsx b/web/src/app/chat/message/CodeBlock.tsx index 297ccf49338..5a0cbef2eb3 100644 --- a/web/src/app/chat/message/CodeBlock.tsx +++ b/web/src/app/chat/message/CodeBlock.tsx @@ -2,6 +2,8 @@ import React from "react"; import { useState, ReactNode } from "react"; import { FiCheck, FiCopy } from "react-icons/fi"; +const CODE_BLOCK_PADDING_TYPE = { padding: "1rem" }; + interface CodeBlockProps { className?: string | undefined; children?: ReactNode; @@ -24,9 +26,11 @@ export function CodeBlock({ if (!language) { return ( - - {children} - +
+        
+          {children}
+        
+      
); } @@ -39,15 +43,25 @@ export function CodeBlock({ props.node.position.start.offset, props.node.position.end.offset ); + codeText = codeText.trim(); // Remove the language declaration and trailing backticks const codeLines = codeText.split("\n"); - if (codeLines.length > 1 && codeLines[0].startsWith("```")) { + if ( + codeLines.length > 1 && + (codeLines[0].startsWith("```") || codeLines[0].trim().startsWith("```")) + ) { codeLines.shift(); // Remove the first line with the language declaration - if (codeLines[codeLines.length - 1] === "```") { + if ( + codeLines[codeLines.length - 1] === "```" || + codeLines[codeLines.length - 1]?.trim() === "```" + ) { codeLines.pop(); // Remove the last line with the trailing backticks } - codeText = codeLines.join("\n"); + + // remove leading whitespace from each line for nicer copy/paste experience + const trimmedCodeLines = codeLines.map((line) => line.trimStart()); + codeText = trimmedCodeLines.join("\n"); } } diff --git a/web/src/app/chat/message/Messages.tsx b/web/src/app/chat/message/Messages.tsx index 1002f98f879..b188c128ea7 100644 --- a/web/src/app/chat/message/Messages.tsx +++ b/web/src/app/chat/message/Messages.tsx @@ -113,7 +113,6 @@ export const AIMessage = ({ if (!isReady) { return
; } - console.log(content); if (!isComplete) { const trimIncompleteCodeSection = ( @@ -519,22 +518,9 @@ export const HumanMessage = ({
) : typeof content === "string" ? ( - ( - - ), - }} - remarkPlugins={[remarkGfm]} - > +
{content} - +
) : ( content )} 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/modal/configuration/AssistantsTab.tsx b/web/src/app/chat/modal/configuration/AssistantsTab.tsx index 8bbda0c8e02..d5a6b4d4441 100644 --- a/web/src/app/chat/modal/configuration/AssistantsTab.tsx +++ b/web/src/app/chat/modal/configuration/AssistantsTab.tsx @@ -1,49 +1,89 @@ import { Persona } from "@/app/admin/assistants/interfaces"; +import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces"; import { Bubble } from "@/components/Bubble"; -import { useChatContext } from "@/components/context/ChatContext"; +import { AssistantIcon } from "@/components/assistants/AssistantIcon"; import { getFinalLLM } from "@/lib/llm/utils"; import React from "react"; import { FiBookmark, FiImage, FiSearch } from "react-icons/fi"; +interface AssistantsTabProps { + selectedAssistant: Persona; + availableAssistants: Persona[]; + llmProviders: LLMProviderDescriptor[]; + onSelect: (assistant: Persona) => void; +} + export function AssistantsTab({ selectedAssistant, + availableAssistants, + llmProviders, onSelect, -}: { - selectedAssistant: Persona; - onSelect: (assistant: Persona) => void; -}) { - const { availablePersonas, llmProviders } = useChatContext(); +}: AssistantsTabProps) { const [_, llmName] = getFinalLLM(llmProviders, null); return ( -
+ <>

Choose Assistant

-
- {availablePersonas.map((assistant) => ( +
+ {availableAssistants.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 +91,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 +99,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..ae2eaeaf9d1 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"; @@ -10,6 +10,7 @@ import { FaBrain } from "react-icons/fa"; import { AssistantsTab } from "./AssistantsTab"; import { Persona } from "@/app/admin/assistants/interfaces"; import { LlmTab } from "./LlmTab"; +import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces"; const TabButton = ({ label, @@ -48,17 +49,21 @@ export function ConfigurationModal({ activeTab, setActiveTab, onClose, + availableAssistants, selectedAssistant, setSelectedAssistant, filterManager, + llmProviders, llmOverrideManager, }: { activeTab: string | null; setActiveTab: (tab: string | null) => void; onClose: () => void; + availableAssistants: Persona[]; selectedAssistant: Persona; setSelectedAssistant: (assistant: Persona) => void; filterManager: FilterManager; + llmProviders: LLMProviderDescriptor[]; llmOverrideManager: LlmOverrideManager; }) { useEffect(() => { @@ -152,6 +157,8 @@ export function ConfigurationModal({ {activeTab === "assistants" && (
{ setSelectedAssistant(assistant); 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 788dd3ff975..e631d99b0cc 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, () => void, - number, + number ] { const [selectedDocuments, setSelectedDocuments] = useState( [] diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 894ac888512..3df3dab58af 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -50,3 +50,8 @@ position: relative; vertical-align: baseline; } + +/* Used to create alternatie to React Markdown */ +.preserve-lines { + white-space: pre-wrap; /* Preserves whitespace and wraps text */ +} diff --git a/web/src/components/Bubble.tsx b/web/src/components/Bubble.tsx index 9bc28d68430..8bf6f19771d 100644 --- a/web/src/components/Bubble.tsx +++ b/web/src/components/Bubble.tsx @@ -5,11 +5,13 @@ export function Bubble({ onClick, children, showCheckbox = false, + notSelectable = false, }: { isSelected: boolean; onClick?: () => 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({ ) : (