From e46f3bb801aa92d04b9f20d5f4f1e06f22bff552 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Sun, 24 Nov 2024 06:44:22 -0800 Subject: [PATCH 01/12] all done except routing --- ...1b118_add_web_ui_option_to_slack_config.py | 35 ++++++++++++++ backend/danswer/danswerbot/slack/blocks.py | 46 +++++++++++++++---- backend/danswer/danswerbot/slack/constants.py | 1 + .../slack/handlers/handle_buttons.py | 4 +- .../slack/handlers/handle_message.py | 4 +- .../slack/handlers/handle_regular_answer.py | 20 +++++++- backend/danswer/danswerbot/slack/utils.py | 13 ++++-- backend/danswer/db/chat.py | 22 +++++++++ backend/danswer/db/models.py | 1 + backend/danswer/server/manage/models.py | 1 + backend/ee/danswer/db/document.py | 1 - .../SlackChannelConfigCreationForm.tsx | 12 ++++- web/src/app/admin/bots/[bot-id]/lib.ts | 2 + web/src/lib/types.ts | 1 + 14 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 backend/alembic/versions/93560ba1b118_add_web_ui_option_to_slack_config.py diff --git a/backend/alembic/versions/93560ba1b118_add_web_ui_option_to_slack_config.py b/backend/alembic/versions/93560ba1b118_add_web_ui_option_to_slack_config.py new file mode 100644 index 00000000000..ab084aee314 --- /dev/null +++ b/backend/alembic/versions/93560ba1b118_add_web_ui_option_to_slack_config.py @@ -0,0 +1,35 @@ +"""add web ui option to slack config + +Revision ID: 93560ba1b118 +Revises: 6d562f86c78b +Create Date: 2024-11-24 06:36:17.490612 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "93560ba1b118" +down_revision = "6d562f86c78b" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Add show_continue_in_web_ui with default False to all existing channel_configs + op.execute( + """ + UPDATE slack_channel_config + SET channel_config = channel_config || '{"show_continue_in_web_ui": false}'::jsonb + WHERE NOT channel_config ? 'show_continue_in_web_ui' + """ + ) + + +def downgrade() -> None: + # Remove show_continue_in_web_ui from all channel_configs + op.execute( + """ + UPDATE slack_channel_config + SET channel_config = channel_config - 'show_continue_in_web_ui' + """ + ) diff --git a/backend/danswer/danswerbot/slack/blocks.py b/backend/danswer/danswerbot/slack/blocks.py index 4107a381554..94f6d238f74 100644 --- a/backend/danswer/danswerbot/slack/blocks.py +++ b/backend/danswer/danswerbot/slack/blocks.py @@ -18,9 +18,11 @@ from danswer.chat.models import DanswerQuote from danswer.configs.app_configs import DISABLE_GENERATIVE_AI +from danswer.configs.app_configs import WEB_DOMAIN from danswer.configs.constants import DocumentSource from danswer.configs.constants import SearchFeedbackType from danswer.configs.danswerbot_configs import DANSWER_BOT_NUM_DOCS_TO_DISPLAY +from danswer.danswerbot.slack.constants import CONTINUE_IN_WEB_UI_ACTION_ID from danswer.danswerbot.slack.constants import DISLIKE_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_ACTION_ID @@ -28,9 +30,12 @@ from danswer.danswerbot.slack.constants import IMMEDIATE_RESOLVED_BUTTON_ACTION_ID from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID from danswer.danswerbot.slack.icons import source_to_github_img_link +from danswer.danswerbot.slack.utils import build_continue_in_web_ui_id from danswer.danswerbot.slack.utils import build_feedback_id from danswer.danswerbot.slack.utils import remove_slack_text_interactions from danswer.danswerbot.slack.utils import translate_vespa_highlight_to_slack +from danswer.db.chat import get_chat_session_by_message_id +from danswer.db.engine import get_session_with_tenant from danswer.search.models import SavedSearchDoc from danswer.utils.text_processing import decode_escapes from danswer.utils.text_processing import replace_whitespaces_w_space @@ -101,12 +106,12 @@ def _split_text(text: str, limit: int = 3000) -> list[str]: return chunks -def clean_markdown_link_text(text: str) -> str: +def _clean_markdown_link_text(text: str) -> str: # Remove any newlines within the text return text.replace("\n", " ").strip() -def build_qa_feedback_block( +def _build_qa_feedback_block( message_id: int, feedback_reminder_id: str | None = None ) -> Block: return ActionsBlock( @@ -115,7 +120,6 @@ def build_qa_feedback_block( ButtonElement( action_id=LIKE_BLOCK_ACTION_ID, text="👍 Helpful", - style="primary", value=feedback_reminder_id, ), ButtonElement( @@ -155,7 +159,7 @@ def get_document_feedback_blocks() -> Block: ) -def build_doc_feedback_block( +def _build_doc_feedback_block( message_id: int, document_id: str, document_rank: int, @@ -223,7 +227,7 @@ def build_documents_blocks( feedback: ButtonElement | dict = {} if message_id is not None: - feedback = build_doc_feedback_block( + feedback = _build_doc_feedback_block( message_id=message_id, document_id=d.document_id, document_rank=rank, @@ -286,7 +290,7 @@ def build_sources_blocks( + ([days_ago_str] if days_ago_str else []) ) - document_title = clean_markdown_link_text(doc_sem_id) + document_title = _clean_markdown_link_text(doc_sem_id) img_link = source_to_github_img_link(d.source_type) section_blocks.append( @@ -317,7 +321,7 @@ def build_sources_blocks( return section_blocks -def build_quotes_block( +def _build_quotes_block( quotes: list[DanswerQuote], ) -> list[Block]: quote_lines: list[str] = [] @@ -408,9 +412,9 @@ def build_qa_response_blocks( SectionBlock(text=text) for text in _split_text(answer_processed) ] if quotes: - quotes_blocks = build_quotes_block(quotes) + quotes_blocks = _build_quotes_block(quotes) - # if no quotes OR `build_quotes_block()` did not give back any blocks + # if no quotes OR `_build_quotes_block()` did not give back any blocks if not quotes_blocks: quotes_blocks = [ SectionBlock( @@ -427,7 +431,7 @@ def build_qa_response_blocks( if message_id is not None and not skip_ai_feedback: response_blocks.append( - build_qa_feedback_block( + _build_qa_feedback_block( message_id=message_id, feedback_reminder_id=feedback_reminder_id ) ) @@ -438,6 +442,28 @@ def build_qa_response_blocks( return response_blocks +def build_continue_in_web_ui_block( + tenant_id: str | None, + message_id: int, +) -> Block: + with get_session_with_tenant(tenant_id) as db_session: + chat_session = get_chat_session_by_message_id( + db_session=db_session, + message_id=message_id, + ) + return ActionsBlock( + block_id=build_continue_in_web_ui_id(message_id), + elements=[ + ButtonElement( + action_id=CONTINUE_IN_WEB_UI_ACTION_ID, + text="Continue in Web UI", + style="primary", + url=f"{WEB_DOMAIN}/chat?slackChatId={chat_session.id}", + ), + ], + ) + + def build_follow_up_block(message_id: int | None) -> ActionsBlock: return ActionsBlock( block_id=build_feedback_id(message_id) if message_id is not None else None, diff --git a/backend/danswer/danswerbot/slack/constants.py b/backend/danswer/danswerbot/slack/constants.py index cf2b38032c3..6a5b3ed43ed 100644 --- a/backend/danswer/danswerbot/slack/constants.py +++ b/backend/danswer/danswerbot/slack/constants.py @@ -2,6 +2,7 @@ LIKE_BLOCK_ACTION_ID = "feedback-like" DISLIKE_BLOCK_ACTION_ID = "feedback-dislike" +CONTINUE_IN_WEB_UI_ACTION_ID = "continue-in-web-ui" FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID = "feedback-doc-button" IMMEDIATE_RESOLVED_BUTTON_ACTION_ID = "immediate-resolved-button" FOLLOWUP_BUTTON_ACTION_ID = "followup-button" diff --git a/backend/danswer/danswerbot/slack/handlers/handle_buttons.py b/backend/danswer/danswerbot/slack/handlers/handle_buttons.py index ec423979941..9335b96874f 100644 --- a/backend/danswer/danswerbot/slack/handlers/handle_buttons.py +++ b/backend/danswer/danswerbot/slack/handlers/handle_buttons.py @@ -28,7 +28,7 @@ from danswer.danswerbot.slack.utils import build_feedback_id from danswer.danswerbot.slack.utils import decompose_action_id from danswer.danswerbot.slack.utils import fetch_group_ids_from_names -from danswer.danswerbot.slack.utils import fetch_user_ids_from_emails +from danswer.danswerbot.slack.utils import fetch_slack_user_ids_from_emails from danswer.danswerbot.slack.utils import get_channel_name_from_id from danswer.danswerbot.slack.utils import get_feedback_visibility from danswer.danswerbot.slack.utils import read_slack_thread @@ -267,7 +267,7 @@ def handle_followup_button( tag_names = slack_channel_config.channel_config.get("follow_up_tags") remaining = None if tag_names: - tag_ids, remaining = fetch_user_ids_from_emails( + tag_ids, remaining = fetch_slack_user_ids_from_emails( tag_names, client.web_client ) if remaining: diff --git a/backend/danswer/danswerbot/slack/handlers/handle_message.py b/backend/danswer/danswerbot/slack/handlers/handle_message.py index 6bec83def4b..1f19d0a70a6 100644 --- a/backend/danswer/danswerbot/slack/handlers/handle_message.py +++ b/backend/danswer/danswerbot/slack/handlers/handle_message.py @@ -13,7 +13,7 @@ handle_standard_answers, ) from danswer.danswerbot.slack.models import SlackMessageInfo -from danswer.danswerbot.slack.utils import fetch_user_ids_from_emails +from danswer.danswerbot.slack.utils import fetch_slack_user_ids_from_emails from danswer.danswerbot.slack.utils import fetch_user_ids_from_groups from danswer.danswerbot.slack.utils import respond_in_thread from danswer.danswerbot.slack.utils import slack_usage_report @@ -184,7 +184,7 @@ def handle_message( send_to: list[str] | None = None missing_users: list[str] | None = None if respond_member_group_list: - send_to, missing_ids = fetch_user_ids_from_emails( + send_to, missing_ids = fetch_slack_user_ids_from_emails( respond_member_group_list, client ) diff --git a/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py b/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py index 9026638adc4..a46de1364a9 100644 --- a/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py +++ b/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py @@ -21,6 +21,7 @@ from danswer.configs.danswerbot_configs import DANSWER_FOLLOWUP_EMOJI from danswer.configs.danswerbot_configs import DANSWER_REACT_EMOJI from danswer.configs.danswerbot_configs import ENABLE_DANSWERBOT_REFLEXION +from danswer.danswerbot.slack.blocks import build_continue_in_web_ui_block from danswer.danswerbot.slack.blocks import build_documents_blocks from danswer.danswerbot.slack.blocks import build_follow_up_block from danswer.danswerbot.slack.blocks import build_qa_response_blocks @@ -429,6 +430,19 @@ def _get_answer(new_message_request: DirectQARequest) -> OneShotQAResponse | Non feedback_reminder_id=feedback_reminder_id, ) + web_follow_up_block = [] + if channel_conf and channel_conf.get("show_continue_in_web_ui") is True: + if answer.chat_message_id is None: + raise ValueError( + "Unable to find chat session associated with answer: " f"{answer}" + ) + web_follow_up_block.append( + build_continue_in_web_ui_block( + tenant_id=tenant_id, + message_id=answer.chat_message_id, + ) + ) + # Get the chunks fed to the LLM only, then fill with other docs llm_doc_inds = answer.llm_selected_doc_indices or [] llm_docs = [top_docs[i] for i in llm_doc_inds] @@ -461,7 +475,11 @@ def _get_answer(new_message_request: DirectQARequest) -> OneShotQAResponse | Non document_blocks = [DividerBlock()] + document_blocks all_blocks = ( - restate_question_block + answer_blocks + citations_block + document_blocks + restate_question_block + + answer_blocks + + citations_block + + document_blocks + + web_follow_up_block ) if channel_conf and channel_conf.get("follow_up_tags") is not None: diff --git a/backend/danswer/danswerbot/slack/utils.py b/backend/danswer/danswerbot/slack/utils.py index e19ce8b688c..cf6f1e1bfc8 100644 --- a/backend/danswer/danswerbot/slack/utils.py +++ b/backend/danswer/danswerbot/slack/utils.py @@ -3,9 +3,9 @@ import re import string import time +import uuid from typing import Any from typing import cast -from typing import Optional from retry import retry from slack_sdk import WebClient @@ -216,6 +216,13 @@ def build_feedback_id( return unique_prefix + ID_SEPARATOR + feedback_id +def build_continue_in_web_ui_id( + message_id: int, +) -> str: + unique_prefix = str(uuid.uuid4())[:10] + return unique_prefix + ID_SEPARATOR + str(message_id) + + def decompose_action_id(feedback_id: str) -> tuple[int, str | None, int | None]: """Decompose into query_id, document_id, document_rank, see above function""" try: @@ -313,7 +320,7 @@ def get_channel_name_from_id( raise e -def fetch_user_ids_from_emails( +def fetch_slack_user_ids_from_emails( user_emails: list[str], client: WebClient ) -> tuple[list[str], list[str]]: user_ids: list[str] = [] @@ -522,7 +529,7 @@ def refill(self) -> None: self.last_reset_time = time.time() def notify( - self, client: WebClient, channel: str, position: int, thread_ts: Optional[str] + self, client: WebClient, channel: str, position: int, thread_ts: str | None ) -> None: respond_in_thread( client=client, diff --git a/backend/danswer/db/chat.py b/backend/danswer/db/chat.py index 4aaee092972..048cc851918 100644 --- a/backend/danswer/db/chat.py +++ b/backend/danswer/db/chat.py @@ -336,6 +336,28 @@ def get_chat_message( return chat_message +def get_chat_session_by_message_id( + db_session: Session, + message_id: int, +) -> ChatSession: + """ + Should only be used for Slack + Get the chat session associated with a specific message ID + Note: this ignores permission checks. + """ + stmt = select(ChatMessage).where(ChatMessage.id == message_id) + + result = db_session.execute(stmt) + chat_message = result.scalar_one_or_none() + + if chat_message is None: + raise ValueError( + f"Unable to find chat session associated with message ID: {message_id}" + ) + + return chat_message.chat_session + + def get_chat_messages_by_sessions( chat_session_ids: list[UUID], user_id: UUID | None, diff --git a/backend/danswer/db/models.py b/backend/danswer/db/models.py index 1513fc5fc81..d538a546126 100644 --- a/backend/danswer/db/models.py +++ b/backend/danswer/db/models.py @@ -1480,6 +1480,7 @@ class ChannelConfig(TypedDict): # If None then no follow up # If empty list, follow up with no tags follow_up_tags: NotRequired[list[str]] + show_continue_in_web_ui: NotRequired[bool] # defaults to False class SlackBotResponseType(str, PyEnum): diff --git a/backend/danswer/server/manage/models.py b/backend/danswer/server/manage/models.py index 4ece3c7f169..e7383a1b1a9 100644 --- a/backend/danswer/server/manage/models.py +++ b/backend/danswer/server/manage/models.py @@ -156,6 +156,7 @@ class SlackChannelConfigCreationRequest(BaseModel): channel_name: str respond_tag_only: bool = False respond_to_bots: bool = False + show_continue_in_web_ui: bool = False enable_auto_filters: bool = False # If no team members, assume respond in the channel to everyone respond_member_group_list: list[str] = Field(default_factory=list) diff --git a/backend/ee/danswer/db/document.py b/backend/ee/danswer/db/document.py index e061db6c75b..3707b6ac74f 100644 --- a/backend/ee/danswer/db/document.py +++ b/backend/ee/danswer/db/document.py @@ -78,7 +78,6 @@ def upsert_document_external_perms( # The upsert function in the indexing pipeline does not overwrite the permissions fields document = DbDocument( id=doc_id, - semantic_id="", external_user_emails=external_access.external_user_emails, external_user_group_ids=prefixed_external_groups, is_public=external_access.is_public, diff --git a/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx b/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx index 9a8caad2ad5..9507ac7af5f 100644 --- a/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx +++ b/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx @@ -81,6 +81,9 @@ export const SlackChannelConfigCreationForm = ({ respond_to_bots: existingSlackChannelConfig?.channel_config?.respond_to_bots || false, + show_continue_in_web_ui: + existingSlackChannelConfig?.channel_config + ?.show_continue_in_web_ui || true, enable_auto_filters: existingSlackChannelConfig?.enable_auto_filters || false, respond_member_group_list: @@ -119,6 +122,7 @@ export const SlackChannelConfigCreationForm = ({ questionmark_prefilter_enabled: Yup.boolean().required(), respond_tag_only: Yup.boolean().required(), respond_to_bots: Yup.boolean().required(), + show_continue_in_web_ui: Yup.boolean().required(), enable_auto_filters: Yup.boolean().required(), respond_member_group_list: Yup.array().of(Yup.string()).required(), still_need_help_enabled: Yup.boolean().required(), @@ -270,7 +274,13 @@ export const SlackChannelConfigCreationForm = ({ {showAdvancedOptions && (
-
+ +
Date: Sun, 24 Nov 2024 07:29:43 -0800 Subject: [PATCH 02/12] fixed initial changes --- .../danswerbot/slack/handlers/handle_regular_answer.py | 2 +- backend/danswer/server/manage/slack_bot.py | 4 ++++ .../bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx | 4 +++- web/src/app/admin/bots/[bot-id]/page.tsx | 1 - 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py b/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py index 4aec0faf252..9f8cffa66e9 100644 --- a/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py +++ b/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py @@ -21,11 +21,11 @@ from danswer.configs.danswerbot_configs import DANSWER_FOLLOWUP_EMOJI from danswer.configs.danswerbot_configs import DANSWER_REACT_EMOJI from danswer.configs.danswerbot_configs import ENABLE_DANSWERBOT_REFLEXION -from danswer.danswerbot.slack.blocks import build_continue_in_web_ui_block from danswer.context.search.enums import OptionalSearchSetting from danswer.context.search.models import BaseFilters from danswer.context.search.models import RerankingDetails from danswer.context.search.models import RetrievalDetails +from danswer.danswerbot.slack.blocks import build_continue_in_web_ui_block from danswer.danswerbot.slack.blocks import build_documents_blocks from danswer.danswerbot.slack.blocks import build_follow_up_block from danswer.danswerbot.slack.blocks import build_qa_response_blocks diff --git a/backend/danswer/server/manage/slack_bot.py b/backend/danswer/server/manage/slack_bot.py index 036f2fca0dd..60a7edaaed0 100644 --- a/backend/danswer/server/manage/slack_bot.py +++ b/backend/danswer/server/manage/slack_bot.py @@ -80,6 +80,10 @@ def _form_channel_config( if follow_up_tags is not None: channel_config["follow_up_tags"] = follow_up_tags + channel_config[ + "show_continue_in_web_ui" + ] = slack_channel_config_creation_request.show_continue_in_web_ui + channel_config[ "respond_to_bots" ] = slack_channel_config_creation_request.respond_to_bots diff --git a/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx b/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx index 9507ac7af5f..5b51e8cf61d 100644 --- a/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx +++ b/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx @@ -82,8 +82,10 @@ export const SlackChannelConfigCreationForm = ({ existingSlackChannelConfig?.channel_config?.respond_to_bots || false, show_continue_in_web_ui: + // If we're updating, we want to keep the existing value + // Otherwise, we want to default to true existingSlackChannelConfig?.channel_config - ?.show_continue_in_web_ui || true, + ?.show_continue_in_web_ui ?? !isUpdate, enable_auto_filters: existingSlackChannelConfig?.enable_auto_filters || false, respond_member_group_list: diff --git a/web/src/app/admin/bots/[bot-id]/page.tsx b/web/src/app/admin/bots/[bot-id]/page.tsx index 414e90f2932..f99e877137a 100644 --- a/web/src/app/admin/bots/[bot-id]/page.tsx +++ b/web/src/app/admin/bots/[bot-id]/page.tsx @@ -22,7 +22,6 @@ function SlackBotEditPage({ const unwrappedParams = use(params); const { popup, setPopup } = usePopup(); - console.log("unwrappedParams", unwrappedParams); const { data: slackBot, isLoading: isSlackBotLoading, From 144d6b2a931c67fa6210acd1e1f77f5b4cb2a751 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Sun, 24 Nov 2024 08:54:57 -0800 Subject: [PATCH 03/12] added backend endpoint for duplicating a chat session from Slack --- backend/danswer/db/chat.py | 103 ++++++++++++++++++ backend/danswer/db/persona.py | 24 ++++ .../server/query_and_chat/chat_backend.py | 34 ++++++ 3 files changed, 161 insertions(+) diff --git a/backend/danswer/db/chat.py b/backend/danswer/db/chat.py index 44d94e9593a..c55c5baa774 100644 --- a/backend/danswer/db/chat.py +++ b/backend/danswer/db/chat.py @@ -3,6 +3,7 @@ from datetime import timedelta from uuid import UUID +from fastapi import HTTPException from sqlalchemy import delete from sqlalchemy import desc from sqlalchemy import func @@ -30,6 +31,7 @@ from danswer.db.models import SearchDoc as DBSearchDoc from danswer.db.models import ToolCall from danswer.db.models import User +from danswer.db.persona import get_best_persona_id_for_user from danswer.db.pg_file_store import delete_lobj_by_name from danswer.file_store.models import FileDescriptor from danswer.llm.override_models import LLMOverride @@ -250,6 +252,43 @@ def create_chat_session( return chat_session +def duplicate_chat_session_for_user_from_slack( + db_session: Session, + user: User | None, + chat_session_id: UUID, +) -> ChatSession: + chat_session = get_chat_session_by_id( + chat_session_id=chat_session_id, + user_id=None, # Ignore user permissions for this + db_session=db_session, + ) + if not chat_session: + raise HTTPException(status_code=400, detail="Invalid Chat Session ID provided") + + # This enforces permissions and sets a default + new_persona_id = get_best_persona_id_for_user( + db_session=db_session, + user=user, + persona_id=chat_session.persona_id, + ) + + return create_chat_session( + db_session=db_session, + user_id=user.id if user else None, + persona_id=new_persona_id, + # This will likely be empty but the frontend will force a rename + description=chat_session.description, + llm_override=chat_session.llm_override, + prompt_override=chat_session.prompt_override, + # Chat sessions from Slack should put people in the chat UI, not the search + one_shot=False, + # Chat is in UI now so this is false + danswerbot_flow=False, + # Maybe we want this in the future to track if it was created from Slack + slack_thread_id=None, + ) + + def update_chat_session( db_session: Session, user_id: UUID | None, @@ -377,6 +416,70 @@ def get_chat_messages_by_sessions( return db_session.execute(stmt).scalars().all() +def add_chats_to_session_from_slack_thread( + db_session: Session, + slack_chat_session_id: UUID, + new_chat_session_id: UUID, +) -> None: + new_root_message = ChatMessage( + chat_session_id=new_chat_session_id, + prompt_id=None, + parent_message=None, + latest_child_message=None, + message="", + token_count=0, + message_type=MessageType.SYSTEM, + ) + db_session.add(new_root_message) + db_session.commit() + + user_message = None + assistant_message = None + for chat_message in get_chat_messages_by_sessions( + chat_session_ids=[slack_chat_session_id], + user_id=None, # Ignore user permissions for this + db_session=db_session, + skip_permission_check=True, + ): + # Should only be 3 messages in a Slack chat session + if chat_message.message_type == MessageType.SYSTEM: + continue + elif chat_message.message_type == MessageType.USER: + user_message = chat_message + elif chat_message.message_type == MessageType.ASSISTANT: + assistant_message = chat_message + + if user_message is None or assistant_message is None: + raise HTTPException( + status_code=500, + detail="Couldnt find all messages in Slack chat session", + ) + + new_user_message = create_new_chat_message( + db_session=db_session, + chat_session_id=new_chat_session_id, + parent_message=new_root_message, + message=user_message.message, + prompt_id=user_message.prompt_id, + token_count=user_message.token_count, + message_type=MessageType.USER, + ) + db_session.add(new_user_message) + db_session.commit() + + new_assistant_message = create_new_chat_message( + db_session=db_session, + chat_session_id=new_chat_session_id, + parent_message=new_user_message, + message=assistant_message.message, + prompt_id=assistant_message.prompt_id, + token_count=assistant_message.token_count, + message_type=MessageType.ASSISTANT, + ) + db_session.add(new_assistant_message) + db_session.commit() + + def get_search_docs_for_chat_message( chat_message_id: int, db_session: Session ) -> list[SearchDoc]: diff --git a/backend/danswer/db/persona.py b/backend/danswer/db/persona.py index 328979784ca..705cfdf29f7 100644 --- a/backend/danswer/db/persona.py +++ b/backend/danswer/db/persona.py @@ -113,6 +113,30 @@ def fetch_persona_by_id( return persona +def get_best_persona_id_for_user( + db_session: Session, user: User | None, persona_id: int | None = None +) -> int | None: + if persona_id: + stmt = select(Persona).where(Persona.id == persona_id).distinct() + stmt = _add_user_filters( + stmt=stmt, + user=user, + # We don't want to filter by editable here, we just want to see if the + # persona is usable by the user + get_editable=False, + ) + persona = db_session.scalars(stmt).one_or_none() + if persona: + return persona.id + + # If the persona is not found, we need to find the best persona for the user + # This is the persona with the highest display priority that the user has access to + stmt = select(Persona).order_by(Persona.display_priority.desc()).distinct() + stmt = _add_user_filters(stmt=stmt, user=user, get_editable=True) + persona = db_session.scalars(stmt).one_or_none() + return persona.id if persona else None + + def _get_persona_by_name( persona_name: str, user: User | None, db_session: Session ) -> Persona | None: diff --git a/backend/danswer/server/query_and_chat/chat_backend.py b/backend/danswer/server/query_and_chat/chat_backend.py index c4728336c86..954728c32a3 100644 --- a/backend/danswer/server/query_and_chat/chat_backend.py +++ b/backend/danswer/server/query_and_chat/chat_backend.py @@ -27,9 +27,11 @@ from danswer.configs.constants import FileOrigin from danswer.configs.constants import MessageType from danswer.configs.model_configs import LITELLM_PASS_THROUGH_HEADERS +from danswer.db.chat import add_chats_to_session_from_slack_thread from danswer.db.chat import create_chat_session from danswer.db.chat import create_new_chat_message from danswer.db.chat import delete_chat_session +from danswer.db.chat import duplicate_chat_session_for_user_from_slack from danswer.db.chat import get_chat_message from danswer.db.chat import get_chat_messages_by_session from danswer.db.chat import get_chat_session_by_id @@ -532,6 +534,38 @@ def seed_chat( ) +class SeedChatFromSlackRequest(BaseModel): + chat_session_id: UUID + + +class SeedChatFromSlackResponse(BaseModel): + redirect_url: str + + +@router.post("/seed-chat-session-from-slack") +def seed_chat_from_slack( + chat_seed_request: SeedChatFromSlackRequest, + user: User | None = Depends(current_user), + db_session: Session = Depends(get_session), +) -> SeedChatFromSlackResponse: + slack_chat_session_id = chat_seed_request.chat_session_id + new_chat_session = duplicate_chat_session_for_user_from_slack( + db_session=db_session, + user=user, + chat_session_id=slack_chat_session_id, + ) + + add_chats_to_session_from_slack_thread( + db_session=db_session, + slack_chat_session_id=slack_chat_session_id, + new_chat_session_id=new_chat_session.id, + ) + + return SeedChatFromSlackResponse( + redirect_url=f"{WEB_DOMAIN}/chat?chatId={new_chat_session.id}" + ) + + """File upload""" From ec73a3cfab43379e7908c27c89972e27e6925890 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Sun, 24 Nov 2024 09:31:36 -0800 Subject: [PATCH 04/12] got chat duplication routing done --- backend/danswer/db/chat.py | 4 +-- web/src/app/chat/ChatPage.tsx | 47 +++++++++++++++++++++++++++++++++-- web/src/app/chat/lib.tsx | 3 +-- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/backend/danswer/db/chat.py b/backend/danswer/db/chat.py index c55c5baa774..23c392a1c9f 100644 --- a/backend/danswer/db/chat.py +++ b/backend/danswer/db/chat.py @@ -276,8 +276,8 @@ def duplicate_chat_session_for_user_from_slack( db_session=db_session, user_id=user.id if user else None, persona_id=new_persona_id, - # This will likely be empty but the frontend will force a rename - description=chat_session.description, + # Set this to empty string so the frontend will force a rename + description="", llm_override=chat_session.llm_override, prompt_override=chat_session.prompt_override, # Chat sessions from Slack should put people in the chat UI, not the search diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 0bb3ebfa965..1b4cddb5a90 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -161,6 +161,8 @@ export function ChatPage({ const { user, isAdmin, isLoadingUser, refreshUser } = useUser(); + const slackChatId = searchParams.get("slackChatId"); + const existingChatIdRaw = searchParams.get("chatId"); const [sendOnLoad, setSendOnLoad] = useState( searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD) @@ -403,6 +405,7 @@ export function ChatPage({ } return; } + setIsReady(true); const shouldScrollToBottom = visibleRange.get(existingChatSessionId) === undefined || visibleRange.get(existingChatSessionId)?.end == 0; @@ -453,6 +456,7 @@ export function ChatPage({ } } setIsFetchingChatMessages(false); + console.log("stuff", chatSession); // if this is a seeded chat, then kick off the AI message generation if ( @@ -468,9 +472,12 @@ export function ChatPage({ }); // force re-name if the chat session doesn't have one if (!chatSession.description) { - await nameChatSession(existingChatSessionId, seededMessage); + await nameChatSession(existingChatSessionId); refreshChatSessions(); } + } else if (newMessageHistory.length === 2 && !chatSession.description) { + await nameChatSession(existingChatSessionId); + refreshChatSessions(); } } @@ -1428,7 +1435,7 @@ export function ChatPage({ if (!searchParamBasedChatSessionName) { await new Promise((resolve) => setTimeout(resolve, 200)); - await nameChatSession(currChatSessionId, currMessage); + await nameChatSession(currChatSessionId); refreshChatSessions(); } @@ -1810,6 +1817,42 @@ export function ChatPage({ }; } + // Add this near the top of the file where other useEffect hooks are + useEffect(() => { + const handleSlackChatRedirect = async () => { + if (!slackChatId) return; + + setIsReady(false); // Set isReady to false before starting retrieval + + try { + const response = await fetch("/api/chat/seed-chat-session-from-slack", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + chat_session_id: slackChatId, + }), + }); + + if (!response.ok) { + throw new Error("Failed to seed chat from Slack"); + } + + const data = await response.json(); + router.push(data.redirect_url); + } catch (error) { + console.error("Error seeding chat from Slack:", error); + setPopup({ + message: "Failed to load chat from Slack", + type: "error", + }); + } + }; + + handleSlackChatRedirect(); + }, [searchParams, router]); // Add any other dependencies needed + return ( <> diff --git a/web/src/app/chat/lib.tsx b/web/src/app/chat/lib.tsx index a64c605a095..cb7e441baf6 100644 --- a/web/src/app/chat/lib.tsx +++ b/web/src/app/chat/lib.tsx @@ -203,7 +203,7 @@ export async function* sendMessage({ yield* handleSSEStream(response); } -export async function nameChatSession(chatSessionId: string, message: string) { +export async function nameChatSession(chatSessionId: string) { const response = await fetch("/api/chat/rename-chat-session", { method: "PUT", headers: { @@ -212,7 +212,6 @@ export async function nameChatSession(chatSessionId: string, message: string) { body: JSON.stringify({ chat_session_id: chatSessionId, name: null, - first_message: message, }), }); return response; From 27a65755ffd60180e4dd3ec3f3338ce78eea8e28 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Sun, 24 Nov 2024 10:17:03 -0800 Subject: [PATCH 05/12] got login routing working --- web/src/app/auth/login/EmailPasswordForm.tsx | 4 +++- web/src/app/auth/login/page.tsx | 21 ++++++++++++-------- web/src/app/auth/signup/page.tsx | 19 ++++++++++++++++-- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/web/src/app/auth/login/EmailPasswordForm.tsx b/web/src/app/auth/login/EmailPasswordForm.tsx index 06053fae5f2..5802f0d35fe 100644 --- a/web/src/app/auth/login/EmailPasswordForm.tsx +++ b/web/src/app/auth/login/EmailPasswordForm.tsx @@ -15,10 +15,12 @@ export function EmailPasswordForm({ isSignup = false, shouldVerify, referralSource, + nextUrl, }: { isSignup?: boolean; shouldVerify?: boolean; referralSource?: string; + nextUrl?: string | null; }) { const router = useRouter(); const { popup, setPopup } = usePopup(); @@ -69,7 +71,7 @@ export function EmailPasswordForm({ await requestEmailVerification(values.email); router.push("/auth/waiting-on-verification"); } else { - router.push("/"); + router.push(nextUrl ? encodeURI(nextUrl) : "/"); } } else { setIsWorking(false); diff --git a/web/src/app/auth/login/page.tsx b/web/src/app/auth/login/page.tsx index fc72c41719f..7a6459655ca 100644 --- a/web/src/app/auth/login/page.tsx +++ b/web/src/app/auth/login/page.tsx @@ -22,6 +22,9 @@ const Page = async (props: { }) => { const searchParams = await props.searchParams; const autoRedirectDisabled = searchParams?.disableAutoRedirect === "true"; + const nextUrl = Array.isArray(searchParams?.next) + ? searchParams?.next[0] + : searchParams?.next || null; // catch cases where the backend is completely unreachable here // without try / catch, will just raise an exception and the page @@ -37,10 +40,6 @@ const Page = async (props: { console.log(`Some fetch failed for the login page - ${e}`); } - const nextUrl = Array.isArray(searchParams?.next) - ? searchParams?.next[0] - : searchParams?.next || null; - // simply take the user to the home page if Auth is disabled if (authTypeMetadata?.authType === "disabled") { return redirect("/"); @@ -100,12 +99,15 @@ const Page = async (props: { or
- +
Don't have an account?{" "} - + Create an account @@ -120,11 +122,14 @@ const Page = async (props: {
- +
Don't have an account?{" "} - + Create an account diff --git a/web/src/app/auth/signup/page.tsx b/web/src/app/auth/signup/page.tsx index 223faff331d..105c79bae60 100644 --- a/web/src/app/auth/signup/page.tsx +++ b/web/src/app/auth/signup/page.tsx @@ -15,7 +15,15 @@ import AuthFlowContainer from "@/components/auth/AuthFlowContainer"; import ReferralSourceSelector from "./ReferralSourceSelector"; import { Separator } from "@/components/ui/separator"; -const Page = async () => { +const Page = async ({ + searchParams, +}: { + searchParams: { [key: string]: string | string[] | undefined }; +}) => { + const nextUrl = Array.isArray(searchParams?.next) + ? searchParams?.next[0] + : searchParams?.next || null; + // catch cases where the backend is completely unreachable here // without try / catch, will just raise an exception and the page // will not render @@ -86,12 +94,19 @@ const Page = async () => {
Already have an account?{" "} - + Log In From b92f093081ee19d8c3c5f1cd1efbc3d87d96a1a6 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Sun, 24 Nov 2024 10:34:49 -0800 Subject: [PATCH 06/12] improved answer handling --- backend/danswer/db/chat.py | 6 ++++++ backend/danswer/db/persona.py | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/danswer/db/chat.py b/backend/danswer/db/chat.py index 23c392a1c9f..30480dc6d63 100644 --- a/backend/danswer/db/chat.py +++ b/backend/danswer/db/chat.py @@ -467,6 +467,11 @@ def add_chats_to_session_from_slack_thread( db_session.add(new_user_message) db_session.commit() + search_docs = get_search_docs_for_chat_message( + chat_message_id=user_message.id, + db_session=db_session, + ) + new_assistant_message = create_new_chat_message( db_session=db_session, chat_session_id=new_chat_session_id, @@ -475,6 +480,7 @@ def add_chats_to_session_from_slack_thread( prompt_id=assistant_message.prompt_id, token_count=assistant_message.token_count, message_type=MessageType.ASSISTANT, + reference_docs=search_docs, ) db_session.add(new_assistant_message) db_session.commit() diff --git a/backend/danswer/db/persona.py b/backend/danswer/db/persona.py index 705cfdf29f7..debbc4b75e5 100644 --- a/backend/danswer/db/persona.py +++ b/backend/danswer/db/persona.py @@ -116,7 +116,7 @@ def fetch_persona_by_id( def get_best_persona_id_for_user( db_session: Session, user: User | None, persona_id: int | None = None ) -> int | None: - if persona_id: + if persona_id is not None: stmt = select(Persona).where(Persona.id == persona_id).distinct() stmt = _add_user_filters( stmt=stmt, @@ -129,7 +129,8 @@ def get_best_persona_id_for_user( if persona: return persona.id - # If the persona is not found, we need to find the best persona for the user + # If the persona is not found, or the slack bot is using doc sets instead of personas, + # we need to find the best persona for the user # This is the persona with the highest display priority that the user has access to stmt = select(Persona).order_by(Persona.display_priority.desc()).distinct() stmt = _add_user_filters(stmt=stmt, user=user, get_editable=True) From ed20693d3b6b2d09c455540730216c8a02325b01 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Sun, 24 Nov 2024 11:39:14 -0800 Subject: [PATCH 07/12] finished all checks --- backend/danswer/danswerbot/slack/blocks.py | 147 +++++++++++++++--- .../slack/handlers/handle_regular_answer.py | 86 +--------- .../[bot-id]/SlackChannelConfigsTable.tsx | 22 +-- web/src/app/auth/signup/page.tsx | 7 +- 4 files changed, 152 insertions(+), 110 deletions(-) diff --git a/backend/danswer/danswerbot/slack/blocks.py b/backend/danswer/danswerbot/slack/blocks.py index e10e70973a2..a4bdf73a891 100644 --- a/backend/danswer/danswerbot/slack/blocks.py +++ b/backend/danswer/danswerbot/slack/blocks.py @@ -22,21 +22,26 @@ from danswer.configs.constants import DocumentSource from danswer.configs.constants import SearchFeedbackType from danswer.configs.danswerbot_configs import DANSWER_BOT_NUM_DOCS_TO_DISPLAY -from danswer.danswerbot.slack.constants import CONTINUE_IN_WEB_UI_ACTION_ID from danswer.context.search.models import SavedSearchDoc +from danswer.danswerbot.slack.constants import CONTINUE_IN_WEB_UI_ACTION_ID from danswer.danswerbot.slack.constants import DISLIKE_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_ACTION_ID from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_RESOLVED_ACTION_ID from danswer.danswerbot.slack.constants import IMMEDIATE_RESOLVED_BUTTON_ACTION_ID from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID +from danswer.danswerbot.slack.formatting import format_slack_message from danswer.danswerbot.slack.icons import source_to_github_img_link +from danswer.danswerbot.slack.models import SlackMessageInfo from danswer.danswerbot.slack.utils import build_continue_in_web_ui_id from danswer.danswerbot.slack.utils import build_feedback_id from danswer.danswerbot.slack.utils import remove_slack_text_interactions from danswer.danswerbot.slack.utils import translate_vespa_highlight_to_slack from danswer.db.chat import get_chat_session_by_message_id from danswer.db.engine import get_session_with_tenant +from danswer.db.models import ChannelConfig +from danswer.db.models import Persona +from danswer.one_shot_answer.models import OneShotQAResponse from danswer.utils.text_processing import decode_escapes from danswer.utils.text_processing import replace_whitespaces_w_space @@ -186,7 +191,7 @@ def get_restate_blocks( ] -def build_documents_blocks( +def _build_documents_blocks( documents: list[SavedSearchDoc], message_id: int | None, num_docs_to_display: int = DANSWER_BOT_NUM_DOCS_TO_DISPLAY, @@ -245,7 +250,7 @@ def build_documents_blocks( return section_blocks -def build_sources_blocks( +def _build_sources_blocks( cited_documents: list[tuple[int, SavedSearchDoc]], num_docs_to_display: int = DANSWER_BOT_NUM_DOCS_TO_DISPLAY, ) -> list[Block]: @@ -363,8 +368,7 @@ def _build_quotes_block( return [SectionBlock(text="*Relevant Snippets*\n" + "\n".join(quote_lines))] -def build_qa_response_blocks( - message_id: int | None, +def _build_qa_response_blocks( answer: str | None, quotes: list[DanswerQuote] | None, source_filters: list[DocumentSource] | None, @@ -372,9 +376,8 @@ def build_qa_response_blocks( favor_recent: bool, skip_quotes: bool = False, process_message_for_citations: bool = False, - skip_ai_feedback: bool = False, - feedback_reminder_id: str | None = None, ) -> list[Block]: + formatted_answer = format_slack_message(answer) if answer else None if DISABLE_GENERATIVE_AI: return [] @@ -398,14 +401,16 @@ def build_qa_response_blocks( filter_block = SectionBlock(text=f"_{filter_text}_") - if not answer: + if not formatted_answer: answer_blocks = [ SectionBlock( text="Sorry, I was unable to find an answer, but I did find some potentially relevant docs 🤓" ) ] else: - answer_processed = decode_escapes(remove_slack_text_interactions(answer)) + answer_processed = decode_escapes( + remove_slack_text_interactions(formatted_answer) + ) if process_message_for_citations: answer_processed = _process_citations_for_slack(answer_processed) answer_blocks = [ @@ -429,20 +434,13 @@ def build_qa_response_blocks( response_blocks.extend(answer_blocks) - if message_id is not None and not skip_ai_feedback: - response_blocks.append( - _build_qa_feedback_block( - message_id=message_id, feedback_reminder_id=feedback_reminder_id - ) - ) - if not skip_quotes: response_blocks.extend(quotes_blocks) return response_blocks -def build_continue_in_web_ui_block( +def _build_continue_in_web_ui_block( tenant_id: str | None, message_id: int, ) -> Block: @@ -456,7 +454,7 @@ def build_continue_in_web_ui_block( elements=[ ButtonElement( action_id=CONTINUE_IN_WEB_UI_ACTION_ID, - text="Continue in Web UI", + text="Continue Chat in Danswer!", style="primary", url=f"{WEB_DOMAIN}/chat?slackChatId={chat_session.id}", ), @@ -464,7 +462,7 @@ def build_continue_in_web_ui_block( ) -def build_follow_up_block(message_id: int | None) -> ActionsBlock: +def _build_follow_up_block(message_id: int | None) -> ActionsBlock: return ActionsBlock( block_id=build_feedback_id(message_id) if message_id is not None else None, elements=[ @@ -509,3 +507,114 @@ def build_follow_up_resolved_blocks( ] ) return [text_block, button_block] + + +def block_builder( + tenant_id: str | None, + message_info: SlackMessageInfo, + answer: OneShotQAResponse, + persona: Persona | None, + channel_conf: ChannelConfig | None, + use_citations: bool, + feedback_reminder_id: str | None, + skip_ai_feedback: bool = False, +) -> list[Block]: + retrieval_info = answer.docs + if not retrieval_info: + # This should not happen, even with no docs retrieved, there is still info returned + raise RuntimeError("Failed to retrieve docs, cannot answer question.") + + top_docs = retrieval_info.top_documents + messages = message_info.thread_messages + + # If called with the DanswerBot slash command, the question is lost so we have to reshow it + restate_question_block = get_restate_blocks( + messages[-1].message, message_info.is_bot_msg + ) + + answer_blocks = _build_qa_response_blocks( + answer=answer.answer, + quotes=answer.quotes.quotes if answer.quotes else None, + source_filters=retrieval_info.applied_source_filters, + time_cutoff=retrieval_info.applied_time_cutoff, + favor_recent=retrieval_info.recency_bias_multiplier > 1, + # currently Personas don't support quotes + # if citations are enabled, also don't use quotes + skip_quotes=persona is not None or use_citations, + process_message_for_citations=use_citations, + ) + + web_follow_up_block = [] + if channel_conf and channel_conf.get("show_continue_in_web_ui"): + if answer.chat_message_id is None: + raise ValueError( + "Unable to find chat session associated with answer: " f"{answer}" + ) + web_follow_up_block.append( + _build_continue_in_web_ui_block( + tenant_id=tenant_id, + message_id=answer.chat_message_id, + ) + ) + + # Get the chunks fed to the LLM only, then fill with other docs + llm_doc_inds = answer.llm_selected_doc_indices or [] + llm_docs = [top_docs[i] for i in llm_doc_inds] + remaining_docs = [ + doc for idx, doc in enumerate(top_docs) if idx not in llm_doc_inds + ] + priority_ordered_docs = llm_docs + remaining_docs + + document_blocks = [] + citations_block = [] + # if citations are enabled, only show cited documents + if use_citations: + citations = answer.citations or [] + cited_docs = [] + for citation in citations: + matching_doc = next( + (d for d in top_docs if d.document_id == citation.document_id), + None, + ) + if matching_doc: + cited_docs.append((citation.citation_num, matching_doc)) + + cited_docs.sort() + citations_block = _build_sources_blocks(cited_documents=cited_docs) + elif priority_ordered_docs: + document_blocks = _build_documents_blocks( + documents=priority_ordered_docs, + message_id=answer.chat_message_id, + ) + document_blocks = [DividerBlock()] + document_blocks + + follow_up_block = [] + if channel_conf and channel_conf.get("follow_up_tags") is not None: + follow_up_block.append( + _build_follow_up_block(message_id=answer.chat_message_id) + ) + + ai_feedback_block = [] + if answer.chat_message_id is not None and not skip_ai_feedback: + ai_feedback_block.append( + _build_qa_feedback_block( + message_id=answer.chat_message_id, + feedback_reminder_id=feedback_reminder_id, + ) + ) + + citations_divider = [DividerBlock()] if citations_block else [] + buttons_divider = [DividerBlock()] if web_follow_up_block or follow_up_block else [] + + all_blocks = ( + restate_question_block + + answer_blocks + + ai_feedback_block + + citations_divider + + citations_block + + document_blocks + + buttons_divider + + web_follow_up_block + + follow_up_block + ) + return all_blocks diff --git a/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py b/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py index 9f8cffa66e9..6f4cc86dd16 100644 --- a/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py +++ b/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py @@ -7,7 +7,6 @@ from retry import retry from slack_sdk import WebClient -from slack_sdk.models.blocks import DividerBlock from slack_sdk.models.blocks import SectionBlock from danswer.configs.app_configs import DISABLE_GENERATIVE_AI @@ -25,13 +24,7 @@ from danswer.context.search.models import BaseFilters from danswer.context.search.models import RerankingDetails from danswer.context.search.models import RetrievalDetails -from danswer.danswerbot.slack.blocks import build_continue_in_web_ui_block -from danswer.danswerbot.slack.blocks import build_documents_blocks -from danswer.danswerbot.slack.blocks import build_follow_up_block -from danswer.danswerbot.slack.blocks import build_qa_response_blocks -from danswer.danswerbot.slack.blocks import build_sources_blocks -from danswer.danswerbot.slack.blocks import get_restate_blocks -from danswer.danswerbot.slack.formatting import format_slack_message +from danswer.danswerbot.slack.blocks import block_builder from danswer.danswerbot.slack.handlers.utils import send_team_member_message from danswer.danswerbot.slack.models import SlackMessageInfo from danswer.danswerbot.slack.utils import respond_in_thread @@ -412,79 +405,16 @@ def _get_answer(new_message_request: DirectQARequest) -> OneShotQAResponse | Non ) return True - # If called with the DanswerBot slash command, the question is lost so we have to reshow it - restate_question_block = get_restate_blocks(messages[-1].message, is_bot_msg) - formatted_answer = format_slack_message(answer.answer) if answer.answer else None - - answer_blocks = build_qa_response_blocks( - message_id=answer.chat_message_id, - answer=formatted_answer, - quotes=answer.quotes.quotes if answer.quotes else None, - source_filters=retrieval_info.applied_source_filters, - time_cutoff=retrieval_info.applied_time_cutoff, - favor_recent=retrieval_info.recency_bias_multiplier > 1, - # currently Personas don't support quotes - # if citations are enabled, also don't use quotes - skip_quotes=persona is not None or use_citations, - process_message_for_citations=use_citations, + all_blocks = block_builder( + tenant_id=tenant_id, + message_info=message_info, + answer=answer, + persona=persona, + channel_conf=channel_conf, + use_citations=use_citations, feedback_reminder_id=feedback_reminder_id, ) - web_follow_up_block = [] - if channel_conf and channel_conf.get("show_continue_in_web_ui") is True: - if answer.chat_message_id is None: - raise ValueError( - "Unable to find chat session associated with answer: " f"{answer}" - ) - web_follow_up_block.append( - build_continue_in_web_ui_block( - tenant_id=tenant_id, - message_id=answer.chat_message_id, - ) - ) - - # Get the chunks fed to the LLM only, then fill with other docs - llm_doc_inds = answer.llm_selected_doc_indices or [] - llm_docs = [top_docs[i] for i in llm_doc_inds] - remaining_docs = [ - doc for idx, doc in enumerate(top_docs) if idx not in llm_doc_inds - ] - priority_ordered_docs = llm_docs + remaining_docs - - document_blocks = [] - citations_block = [] - # if citations are enabled, only show cited documents - if use_citations: - citations = answer.citations or [] - cited_docs = [] - for citation in citations: - matching_doc = next( - (d for d in top_docs if d.document_id == citation.document_id), - None, - ) - if matching_doc: - cited_docs.append((citation.citation_num, matching_doc)) - - cited_docs.sort() - citations_block = build_sources_blocks(cited_documents=cited_docs) - elif priority_ordered_docs: - document_blocks = build_documents_blocks( - documents=priority_ordered_docs, - message_id=answer.chat_message_id, - ) - document_blocks = [DividerBlock()] + document_blocks - - all_blocks = ( - restate_question_block - + answer_blocks - + citations_block - + document_blocks - + web_follow_up_block - ) - - if channel_conf and channel_conf.get("follow_up_tags") is not None: - all_blocks.append(build_follow_up_block(message_id=answer.chat_message_id)) - try: respond_in_thread( client=client, diff --git a/web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx b/web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx index 632e41aa375..1f99b7ca214 100644 --- a/web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx +++ b/web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx @@ -60,21 +60,24 @@ export function SlackChannelConfigsTable({ .slice(numToDisplay * (page - 1), numToDisplay * page) .map((slackChannelConfig) => { return ( - + { + window.location.href = `/admin/bots/${slackBotId}/channels/${slackChannelConfig.id}`; + }} + >
- +
- +
{"#" + slackChannelConfig.channel_config.channel_name}
- + e.stopPropagation()}> {slackChannelConfig.persona && !isPersonaASlackBotPersona(slackChannelConfig.persona) ? ( - + e.stopPropagation()}>
{ + onClick={async (e) => { + e.stopPropagation(); const response = await deleteSlackChannelConfig( slackChannelConfig.id ); diff --git a/web/src/app/auth/signup/page.tsx b/web/src/app/auth/signup/page.tsx index 105c79bae60..94a7d1967bb 100644 --- a/web/src/app/auth/signup/page.tsx +++ b/web/src/app/auth/signup/page.tsx @@ -15,11 +15,10 @@ import AuthFlowContainer from "@/components/auth/AuthFlowContainer"; import ReferralSourceSelector from "./ReferralSourceSelector"; import { Separator } from "@/components/ui/separator"; -const Page = async ({ - searchParams, -}: { - searchParams: { [key: string]: string | string[] | undefined }; +const Page = async (props: { + searchParams?: Promise<{ [key: string]: string | string[] | undefined }>; }) => { + const searchParams = await props.searchParams; const nextUrl = Array.isArray(searchParams?.next) ? searchParams?.next[0] : searchParams?.next || null; From aed2cf0142974f4b2a884f6075fb660e3273a382 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Mon, 25 Nov 2024 12:47:58 -0800 Subject: [PATCH 08/12] finished all! --- backend/danswer/danswerbot/slack/blocks.py | 158 ++++++++++-------- .../slack/handlers/handle_regular_answer.py | 4 +- backend/danswer/db/chat.py | 83 ++++----- .../one_shot_answer/answer_question.py | 16 +- backend/danswer/one_shot_answer/qa_utils.py | 28 ++++ web/src/app/chat/ChatPage.tsx | 4 +- web/src/app/chat/lib.tsx | 1 - 7 files changed, 163 insertions(+), 131 deletions(-) diff --git a/backend/danswer/danswerbot/slack/blocks.py b/backend/danswer/danswerbot/slack/blocks.py index a4bdf73a891..a5e6868fd37 100644 --- a/backend/danswer/danswerbot/slack/blocks.py +++ b/backend/danswer/danswerbot/slack/blocks.py @@ -326,6 +326,49 @@ def _build_sources_blocks( return section_blocks +def _priority_ordered_documents_blocks( + answer: OneShotQAResponse, +) -> list[Block]: + docs_response = answer.docs if answer.docs else None + top_docs = docs_response.top_documents if docs_response else [] + llm_doc_inds = answer.llm_selected_doc_indices or [] + llm_docs = [top_docs[i] for i in llm_doc_inds] + remaining_docs = [ + doc for idx, doc in enumerate(top_docs) if idx not in llm_doc_inds + ] + priority_ordered_docs = llm_docs + remaining_docs + if not priority_ordered_docs: + return [] + + document_blocks = _build_documents_blocks( + documents=priority_ordered_docs, + message_id=answer.chat_message_id, + ) + if document_blocks: + document_blocks = [DividerBlock()] + document_blocks + return document_blocks + + +def _build_citations_blocks( + answer: OneShotQAResponse, +) -> list[Block]: + docs_response = answer.docs if answer.docs else None + top_docs = docs_response.top_documents if docs_response else [] + citations = answer.citations or [] + cited_docs = [] + for citation in citations: + matching_doc = next( + (d for d in top_docs if d.document_id == citation.document_id), + None, + ) + if matching_doc: + cited_docs.append((citation.citation_num, matching_doc)) + + cited_docs.sort() + citations_block = _build_sources_blocks(cited_documents=cited_docs) + return citations_block + + def _build_quotes_block( quotes: list[DanswerQuote], ) -> list[Block]: @@ -369,33 +412,45 @@ def _build_quotes_block( def _build_qa_response_blocks( - answer: str | None, - quotes: list[DanswerQuote] | None, - source_filters: list[DocumentSource] | None, - time_cutoff: datetime | None, - favor_recent: bool, + answer: OneShotQAResponse, skip_quotes: bool = False, process_message_for_citations: bool = False, ) -> list[Block]: - formatted_answer = format_slack_message(answer) if answer else None + retrieval_info = answer.docs + if not retrieval_info: + # This should not happen, even with no docs retrieved, there is still info returned + raise RuntimeError("Failed to retrieve docs, cannot answer question.") + + formatted_answer = format_slack_message(answer.answer) if answer.answer else None + quotes = answer.quotes.quotes if answer.quotes else None + if DISABLE_GENERATIVE_AI: return [] quotes_blocks: list[Block] = [] filter_block: Block | None = None - if time_cutoff or favor_recent or source_filters: + if ( + retrieval_info.applied_time_cutoff + or retrieval_info.recency_bias_multiplier > 1 + or retrieval_info.applied_source_filters + ): filter_text = "Filters: " - if source_filters: - sources_str = ", ".join([s.value for s in source_filters]) + if retrieval_info.applied_source_filters: + sources_str = ", ".join( + [s.value for s in retrieval_info.applied_source_filters] + ) filter_text += f"`Sources in [{sources_str}]`" - if time_cutoff or favor_recent: + if ( + retrieval_info.applied_time_cutoff + or retrieval_info.recency_bias_multiplier > 1 + ): filter_text += " and " - if time_cutoff is not None: - time_str = time_cutoff.strftime("%b %d, %Y") + if retrieval_info.applied_time_cutoff is not None: + time_str = retrieval_info.applied_time_cutoff.strftime("%b %d, %Y") filter_text += f"`Docs Updated >= {time_str}` " - if favor_recent: - if time_cutoff is not None: + if retrieval_info.recency_bias_multiplier > 1: + if retrieval_info.applied_time_cutoff is not None: filter_text += "+ " filter_text += "`Prioritize Recently Updated Docs`" @@ -442,8 +497,10 @@ def _build_qa_response_blocks( def _build_continue_in_web_ui_block( tenant_id: str | None, - message_id: int, + message_id: int | None, ) -> Block: + if message_id is None: + raise ValueError("No message id provided to build continue in web ui block") with get_session_with_tenant(tenant_id) as db_session: chat_session = get_chat_session_by_message_id( db_session=db_session, @@ -509,7 +566,7 @@ def build_follow_up_resolved_blocks( return [text_block, button_block] -def block_builder( +def build_slack_response_blocks( tenant_id: str | None, message_info: SlackMessageInfo, answer: OneShotQAResponse, @@ -519,37 +576,23 @@ def block_builder( feedback_reminder_id: str | None, skip_ai_feedback: bool = False, ) -> list[Block]: - retrieval_info = answer.docs - if not retrieval_info: - # This should not happen, even with no docs retrieved, there is still info returned - raise RuntimeError("Failed to retrieve docs, cannot answer question.") - - top_docs = retrieval_info.top_documents - messages = message_info.thread_messages - + """ + This function is a top level function that builds all the blocks for the Slack response. + It also handles combining all the blocks together. + """ # If called with the DanswerBot slash command, the question is lost so we have to reshow it restate_question_block = get_restate_blocks( - messages[-1].message, message_info.is_bot_msg + message_info.thread_messages[-1].message, message_info.is_bot_msg ) answer_blocks = _build_qa_response_blocks( - answer=answer.answer, - quotes=answer.quotes.quotes if answer.quotes else None, - source_filters=retrieval_info.applied_source_filters, - time_cutoff=retrieval_info.applied_time_cutoff, - favor_recent=retrieval_info.recency_bias_multiplier > 1, - # currently Personas don't support quotes - # if citations are enabled, also don't use quotes + answer=answer, skip_quotes=persona is not None or use_citations, process_message_for_citations=use_citations, ) web_follow_up_block = [] if channel_conf and channel_conf.get("show_continue_in_web_ui"): - if answer.chat_message_id is None: - raise ValueError( - "Unable to find chat session associated with answer: " f"{answer}" - ) web_follow_up_block.append( _build_continue_in_web_ui_block( tenant_id=tenant_id, @@ -557,37 +600,6 @@ def block_builder( ) ) - # Get the chunks fed to the LLM only, then fill with other docs - llm_doc_inds = answer.llm_selected_doc_indices or [] - llm_docs = [top_docs[i] for i in llm_doc_inds] - remaining_docs = [ - doc for idx, doc in enumerate(top_docs) if idx not in llm_doc_inds - ] - priority_ordered_docs = llm_docs + remaining_docs - - document_blocks = [] - citations_block = [] - # if citations are enabled, only show cited documents - if use_citations: - citations = answer.citations or [] - cited_docs = [] - for citation in citations: - matching_doc = next( - (d for d in top_docs if d.document_id == citation.document_id), - None, - ) - if matching_doc: - cited_docs.append((citation.citation_num, matching_doc)) - - cited_docs.sort() - citations_block = _build_sources_blocks(cited_documents=cited_docs) - elif priority_ordered_docs: - document_blocks = _build_documents_blocks( - documents=priority_ordered_docs, - message_id=answer.chat_message_id, - ) - document_blocks = [DividerBlock()] + document_blocks - follow_up_block = [] if channel_conf and channel_conf.get("follow_up_tags") is not None: follow_up_block.append( @@ -603,7 +615,15 @@ def block_builder( ) ) - citations_divider = [DividerBlock()] if citations_block else [] + citations_blocks = [] + document_blocks = [] + if use_citations: + # if citations are enabled, only show cited documents + citations_blocks = _build_citations_blocks(answer) + else: + document_blocks = _priority_ordered_documents_blocks(answer) + + citations_divider = [DividerBlock()] if citations_blocks else [] buttons_divider = [DividerBlock()] if web_follow_up_block or follow_up_block else [] all_blocks = ( @@ -611,7 +631,7 @@ def block_builder( + answer_blocks + ai_feedback_block + citations_divider - + citations_block + + citations_blocks + document_blocks + buttons_divider + web_follow_up_block diff --git a/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py b/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py index 6f4cc86dd16..926fd858243 100644 --- a/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py +++ b/backend/danswer/danswerbot/slack/handlers/handle_regular_answer.py @@ -24,7 +24,7 @@ from danswer.context.search.models import BaseFilters from danswer.context.search.models import RerankingDetails from danswer.context.search.models import RetrievalDetails -from danswer.danswerbot.slack.blocks import block_builder +from danswer.danswerbot.slack.blocks import build_slack_response_blocks from danswer.danswerbot.slack.handlers.utils import send_team_member_message from danswer.danswerbot.slack.models import SlackMessageInfo from danswer.danswerbot.slack.utils import respond_in_thread @@ -405,7 +405,7 @@ def _get_answer(new_message_request: DirectQARequest) -> OneShotQAResponse | Non ) return True - all_blocks = block_builder( + all_blocks = build_slack_response_blocks( tenant_id=tenant_id, message_info=message_info, answer=answer, diff --git a/backend/danswer/db/chat.py b/backend/danswer/db/chat.py index 30480dc6d63..dd12193fe7f 100644 --- a/backend/danswer/db/chat.py +++ b/backend/danswer/db/chat.py @@ -257,6 +257,13 @@ def duplicate_chat_session_for_user_from_slack( user: User | None, chat_session_id: UUID, ) -> ChatSession: + """ + This takes a chat session id for a session in Slack and: + - Creates a new chat session in the DB + - Tries to copy the persona from the original chat session + (if it is available to the user clicking the button) + - Sets the user to the given user (if provided) + """ chat_session = get_chat_session_by_id( chat_session_id=chat_session_id, user_id=None, # Ignore user permissions for this @@ -421,70 +428,38 @@ def add_chats_to_session_from_slack_thread( slack_chat_session_id: UUID, new_chat_session_id: UUID, ) -> None: - new_root_message = ChatMessage( + new_root_message = get_or_create_root_message( chat_session_id=new_chat_session_id, - prompt_id=None, - parent_message=None, - latest_child_message=None, - message="", - token_count=0, - message_type=MessageType.SYSTEM, + db_session=db_session, ) - db_session.add(new_root_message) - db_session.commit() - user_message = None - assistant_message = None for chat_message in get_chat_messages_by_sessions( chat_session_ids=[slack_chat_session_id], user_id=None, # Ignore user permissions for this db_session=db_session, skip_permission_check=True, ): - # Should only be 3 messages in a Slack chat session if chat_message.message_type == MessageType.SYSTEM: continue - elif chat_message.message_type == MessageType.USER: - user_message = chat_message - elif chat_message.message_type == MessageType.ASSISTANT: - assistant_message = chat_message - - if user_message is None or assistant_message is None: - raise HTTPException( - status_code=500, - detail="Couldnt find all messages in Slack chat session", + # Duplicate the message + new_root_message = create_new_chat_message( + db_session=db_session, + chat_session_id=new_chat_session_id, + parent_message=new_root_message, + message=chat_message.message, + files=chat_message.files, + rephrased_query=chat_message.rephrased_query, + error=chat_message.error, + citations=chat_message.citations, + search_docs=chat_message.search_docs, + tool_call=chat_message.tool_call, + prompt_id=chat_message.prompt_id, + token_count=chat_message.token_count, + message_type=chat_message.message_type, + alternate_assistant_id=chat_message.alternate_assistant_id, + overridden_model=chat_message.overridden_model, ) - new_user_message = create_new_chat_message( - db_session=db_session, - chat_session_id=new_chat_session_id, - parent_message=new_root_message, - message=user_message.message, - prompt_id=user_message.prompt_id, - token_count=user_message.token_count, - message_type=MessageType.USER, - ) - db_session.add(new_user_message) - db_session.commit() - - search_docs = get_search_docs_for_chat_message( - chat_message_id=user_message.id, - db_session=db_session, - ) - - new_assistant_message = create_new_chat_message( - db_session=db_session, - chat_session_id=new_chat_session_id, - parent_message=new_user_message, - message=assistant_message.message, - prompt_id=assistant_message.prompt_id, - token_count=assistant_message.token_count, - message_type=MessageType.ASSISTANT, - reference_docs=search_docs, - ) - db_session.add(new_assistant_message) - db_session.commit() - def get_search_docs_for_chat_message( chat_message_id: int, db_session: Session @@ -601,7 +576,7 @@ def create_new_chat_message( files: list[FileDescriptor] | None = None, rephrased_query: str | None = None, error: str | None = None, - reference_docs: list[DBSearchDoc] | None = None, + search_docs: list[DBSearchDoc] | None = None, alternate_assistant_id: int | None = None, # Maps the citation number [n] to the DB SearchDoc citations: dict[int, int] | None = None, @@ -652,8 +627,8 @@ def create_new_chat_message( db_session.add(new_chat_message) # SQL Alchemy will propagate this to update the reference_docs' foreign keys - if reference_docs: - new_chat_message.search_docs = reference_docs + if search_docs: + new_chat_message.search_docs = search_docs # Flush the session to get an ID for the new chat message db_session.flush() diff --git a/backend/danswer/one_shot_answer/answer_question.py b/backend/danswer/one_shot_answer/answer_question.py index 9f8ce99231b..2178c3a2d0c 100644 --- a/backend/danswer/one_shot_answer/answer_question.py +++ b/backend/danswer/one_shot_answer/answer_question.py @@ -47,6 +47,7 @@ from danswer.one_shot_answer.models import OneShotQAResponse from danswer.one_shot_answer.models import QueryRephrase from danswer.one_shot_answer.qa_utils import combine_message_thread +from danswer.one_shot_answer.qa_utils import slackify_message_thread 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 @@ -194,13 +195,22 @@ def stream_answer_objects( ) prompt = persona.prompts[0] + user_message_str = query_msg.message + # For this endpoint, we only save one user message to the chat session + # However, for slackbot, we want to include the history of the entire thread + if danswerbot_flow: + # Right now, we only support bringing over citations and search docs + # from the last message in the thread, not the entire thread + # in the future, we may want to retrieve the entire thread + user_message_str = slackify_message_thread(query_req.messages) + # Create the first User query message new_user_message = create_new_chat_message( chat_session_id=chat_session.id, parent_message=root_message, prompt_id=query_req.prompt_id, - message=query_msg.message, - token_count=len(llm_tokenizer.encode(query_msg.message)), + message=user_message_str, + token_count=len(llm_tokenizer.encode(user_message_str)), message_type=MessageType.USER, db_session=db_session, commit=True, @@ -339,7 +349,7 @@ def stream_answer_objects( token_count=len(llm_tokenizer.encode(answer.llm_answer)), message_type=MessageType.ASSISTANT, error=None, - reference_docs=reference_db_search_docs, + search_docs=reference_db_search_docs, db_session=db_session, commit=True, ) diff --git a/backend/danswer/one_shot_answer/qa_utils.py b/backend/danswer/one_shot_answer/qa_utils.py index 6fbad99eff1..8770a3b1413 100644 --- a/backend/danswer/one_shot_answer/qa_utils.py +++ b/backend/danswer/one_shot_answer/qa_utils.py @@ -51,3 +51,31 @@ def combine_message_thread( total_token_count += message_token_count return "\n\n".join(message_strs) + + +def slackify_message(message: ThreadMessage) -> str: + if message.role != MessageType.USER: + return message.message + + return f"{message.sender or 'Unknown User'} said in Slack:\n{message.message}" + + +def slackify_message_thread(messages: list[ThreadMessage]) -> str: + if not messages: + return "" + + message_strs: list[str] = [] + for message in messages: + if message.role == MessageType.USER: + message_text = ( + f"{message.sender or 'Unknown User'} said in Slack:\n{message.message}" + ) + elif message.role == MessageType.ASSISTANT: + message_text = f"DanswerBot said in Slack:\n{message.message}" + else: + message_text = ( + f"{message.role.value.upper()} said in Slack:\n{message.message}" + ) + message_strs.append(message_text) + + return "\n\n".join(message_strs) diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 1b4cddb5a90..47cbd917c7f 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -1817,12 +1817,12 @@ export function ChatPage({ }; } - // Add this near the top of the file where other useEffect hooks are useEffect(() => { const handleSlackChatRedirect = async () => { if (!slackChatId) return; - setIsReady(false); // Set isReady to false before starting retrieval + // Set isReady to false before starting retrieval to display loading text + setIsReady(false); try { const response = await fetch("/api/chat/seed-chat-session-from-slack", { diff --git a/web/src/app/chat/lib.tsx b/web/src/app/chat/lib.tsx index cb7e441baf6..00529776407 100644 --- a/web/src/app/chat/lib.tsx +++ b/web/src/app/chat/lib.tsx @@ -262,7 +262,6 @@ export async function renameChatSession( body: JSON.stringify({ chat_session_id: chatSessionId, name: newName, - first_message: null, }), }); return response; From bfb7e9dfd8da87a66d172a7f6865305a0f7df291 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Mon, 25 Nov 2024 13:47:35 -0800 Subject: [PATCH 09/12] made sure it works with google oauth --- backend/danswer/main.py | 3 ++- web/src/lib/userSS.ts | 31 ++++++++++++++++++++----------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/backend/danswer/main.py b/backend/danswer/main.py index 3fd7072bb9a..a8fe531f7d5 100644 --- a/backend/danswer/main.py +++ b/backend/danswer/main.py @@ -26,6 +26,7 @@ from danswer.auth.schemas import UserUpdate from danswer.auth.users import auth_backend from danswer.auth.users import BasicAuthenticationError +from danswer.auth.users import create_danswer_oauth_router from danswer.auth.users import fastapi_users from danswer.configs.app_configs import APP_API_PREFIX from danswer.configs.app_configs import APP_HOST @@ -323,7 +324,7 @@ def get_application() -> FastAPI: oauth_client = GoogleOAuth2(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET) include_router_with_global_prefix_prepended( application, - fastapi_users.get_oauth_router( + create_danswer_oauth_router( oauth_client, auth_backend, USER_AUTH_SECRET, diff --git a/web/src/lib/userSS.ts b/web/src/lib/userSS.ts index 906f23fa8b2..b0c9609391f 100644 --- a/web/src/lib/userSS.ts +++ b/web/src/lib/userSS.ts @@ -62,12 +62,17 @@ const getOIDCAuthUrlSS = async (nextUrl: string | null): Promise => { return data.authorization_url; }; -const getGoogleOAuthUrlSS = async (): Promise => { - const res = await fetch(buildUrl(`/auth/oauth/authorize`), { - headers: { - cookie: processCookies(await cookies()), - }, - }); +const getGoogleOAuthUrlSS = async (nextUrl: string | null): Promise => { + const res = await fetch( + buildUrl( + `/auth/oauth/authorize${nextUrl ? `?next=${encodeURIComponent(nextUrl)}` : ""}` + ), + { + headers: { + cookie: processCookies(await cookies()), + }, + } + ); if (!res.ok) { throw new Error("Failed to fetch data"); } @@ -76,8 +81,12 @@ const getGoogleOAuthUrlSS = async (): Promise => { return data.authorization_url; }; -const getSAMLAuthUrlSS = async (): Promise => { - const res = await fetch(buildUrl("/auth/saml/authorize")); +const getSAMLAuthUrlSS = async (nextUrl: string | null): Promise => { + const res = await fetch( + buildUrl( + `/auth/saml/authorize${nextUrl ? `?next=${encodeURIComponent(nextUrl)}` : ""}` + ) + ); if (!res.ok) { throw new Error("Failed to fetch data"); } @@ -97,13 +106,13 @@ export const getAuthUrlSS = async ( case "basic": return ""; case "google_oauth": { - return await getGoogleOAuthUrlSS(); + return await getGoogleOAuthUrlSS(nextUrl); } case "cloud": { - return await getGoogleOAuthUrlSS(); + return await getGoogleOAuthUrlSS(nextUrl); } case "saml": { - return await getSAMLAuthUrlSS(); + return await getSAMLAuthUrlSS(nextUrl); } case "oidc": { return await getOIDCAuthUrlSS(nextUrl); From 4c772a6cecc2c4745b6ab31d72d9b898374c6aed Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Tue, 26 Nov 2024 08:13:05 -0800 Subject: [PATCH 10/12] dont remove that lol --- backend/ee/danswer/db/document.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/ee/danswer/db/document.py b/backend/ee/danswer/db/document.py index 3707b6ac74f..e061db6c75b 100644 --- a/backend/ee/danswer/db/document.py +++ b/backend/ee/danswer/db/document.py @@ -78,6 +78,7 @@ def upsert_document_external_perms( # The upsert function in the indexing pipeline does not overwrite the permissions fields document = DbDocument( id=doc_id, + semantic_id="", external_user_emails=external_access.external_user_emails, external_user_group_ids=prefixed_external_groups, is_public=external_access.is_public, From 7cf2d8f6b3c44bb8e8494e5512051bd7b71b6ff3 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Tue, 26 Nov 2024 10:48:57 -0800 Subject: [PATCH 11/12] fixed weird thing --- backend/danswer/db/chat.py | 8 ++++---- backend/danswer/one_shot_answer/answer_question.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/danswer/db/chat.py b/backend/danswer/db/chat.py index dd12193fe7f..73d0a886f45 100644 --- a/backend/danswer/db/chat.py +++ b/backend/danswer/db/chat.py @@ -451,7 +451,7 @@ def add_chats_to_session_from_slack_thread( rephrased_query=chat_message.rephrased_query, error=chat_message.error, citations=chat_message.citations, - search_docs=chat_message.search_docs, + reference_docs=chat_message.search_docs, tool_call=chat_message.tool_call, prompt_id=chat_message.prompt_id, token_count=chat_message.token_count, @@ -576,7 +576,7 @@ def create_new_chat_message( files: list[FileDescriptor] | None = None, rephrased_query: str | None = None, error: str | None = None, - search_docs: list[DBSearchDoc] | None = None, + reference_docs: list[DBSearchDoc] | None = None, alternate_assistant_id: int | None = None, # Maps the citation number [n] to the DB SearchDoc citations: dict[int, int] | None = None, @@ -627,8 +627,8 @@ def create_new_chat_message( db_session.add(new_chat_message) # SQL Alchemy will propagate this to update the reference_docs' foreign keys - if search_docs: - new_chat_message.search_docs = search_docs + if reference_docs: + new_chat_message.search_docs = reference_docs # Flush the session to get an ID for the new chat message db_session.flush() diff --git a/backend/danswer/one_shot_answer/answer_question.py b/backend/danswer/one_shot_answer/answer_question.py index 2178c3a2d0c..826673acb0d 100644 --- a/backend/danswer/one_shot_answer/answer_question.py +++ b/backend/danswer/one_shot_answer/answer_question.py @@ -349,7 +349,7 @@ def stream_answer_objects( token_count=len(llm_tokenizer.encode(answer.llm_answer)), message_type=MessageType.ASSISTANT, error=None, - search_docs=reference_db_search_docs, + reference_docs=reference_db_search_docs, db_session=db_session, commit=True, ) From b9dfa261f0254fb1f764c7c7f3d1d95411987f25 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Tue, 26 Nov 2024 10:50:14 -0800 Subject: [PATCH 12/12] bad comments --- web/src/app/chat/ChatPage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 47cbd917c7f..94f336ba885 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -456,7 +456,6 @@ export function ChatPage({ } } setIsFetchingChatMessages(false); - console.log("stuff", chatSession); // if this is a seeded chat, then kick off the AI message generation if ( @@ -1851,7 +1850,7 @@ export function ChatPage({ }; handleSlackChatRedirect(); - }, [searchParams, router]); // Add any other dependencies needed + }, [searchParams, router]); return ( <>