From 59bac1ca8f17961cabdc07ceb7f49f3c53ecd416 Mon Sep 17 00:00:00 2001 From: Yuhong Sun Date: Tue, 3 Oct 2023 14:55:29 -0700 Subject: [PATCH] Support more Slack Config Options (#494) --------- Co-authored-by: Weves --- backend/danswer/bots/slack/blocks.py | 14 +++ backend/danswer/bots/slack/config.py | 7 ++ .../bots/slack/handlers/handle_message.py | 88 +++++++++++++++++-- backend/danswer/bots/slack/listener.py | 88 +++++++++++++++---- backend/danswer/bots/slack/utils.py | 64 +++++++++++--- backend/danswer/configs/app_configs.py | 1 + backend/danswer/db/models.py | 5 +- backend/danswer/server/models.py | 15 +++- .../danswer/server/slack_bot_management.py | 74 +++++++++------- .../admin/bot/SlackBotConfigCreationForm.tsx | 45 +++++++++- web/src/app/admin/bot/lib.ts | 32 ++++++- web/src/app/admin/bot/page.tsx | 34 +++++-- web/src/app/admin/connectors/hubspot/page.tsx | 13 +-- web/src/lib/types.ts | 10 ++- 14 files changed, 399 insertions(+), 91 deletions(-) diff --git a/backend/danswer/bots/slack/blocks.py b/backend/danswer/bots/slack/blocks.py index cab1fff6e..b06570edd 100644 --- a/backend/danswer/bots/slack/blocks.py +++ b/backend/danswer/bots/slack/blocks.py @@ -71,6 +71,20 @@ def build_doc_feedback_block( ) +def get_restate_blocks( + msg: str, + is_bot_msg: bool, +) -> list[Block]: + # Only the slash command needs this context because the user doesnt see their own input + if not is_bot_msg: + return [] + + return [ + HeaderBlock(text="Responding to the Query"), + SectionBlock(text=f"```{msg}```"), + ] + + def build_documents_blocks( documents: list[SearchDoc], query_event_id: int, diff --git a/backend/danswer/bots/slack/config.py b/backend/danswer/bots/slack/config.py index 8b8465fa8..876dabff6 100644 --- a/backend/danswer/bots/slack/config.py +++ b/backend/danswer/bots/slack/config.py @@ -4,6 +4,13 @@ from danswer.db.models import SlackBotConfig from danswer.db.slack_bot_config import fetch_slack_bot_configs +VALID_SLACK_FILTERS = [ + "answerable_prefilter", + "well_answered_postfilter", + "questionmark_prefilter", +] + + def get_slack_bot_config_for_channel( channel_name: str, db_session: Session ) -> SlackBotConfig | None: diff --git a/backend/danswer/bots/slack/handlers/handle_message.py b/backend/danswer/bots/slack/handlers/handle_message.py index 0278a7f07..ae3f14920 100644 --- a/backend/danswer/bots/slack/handlers/handle_message.py +++ b/backend/danswer/bots/slack/handlers/handle_message.py @@ -6,7 +6,9 @@ from sqlalchemy.orm import Session from danswer.bots.slack.blocks import build_documents_blocks from danswer.bots.slack.blocks import build_qa_response_blocks +from danswer.bots.slack.blocks import get_restate_blocks from danswer.bots.slack.config import get_slack_bot_config_for_channel +from danswer.bots.slack.utils import fetch_userids_from_emails from danswer.bots.slack.utils import get_channel_name_from_id from danswer.bots.slack.utils import respond_in_thread from danswer.configs.app_configs import DANSWER_BOT_ANSWER_GENERATION_TIMEOUT @@ -25,9 +27,12 @@ from danswer.server.models import QuestionRequest def handle_message( msg: str, channel: str, - message_ts_to_respond_to: str, + message_ts_to_respond_to: str | None, + sender_id: str | None, client: WebClient, logger: logging.Logger, + skip_filters: bool = False, + is_bot_msg: bool = False, num_retries: int = DANSWER_BOT_NUM_RETRIES, answer_generation_timeout: int = DANSWER_BOT_ANSWER_GENERATION_TIMEOUT, should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS, @@ -40,20 +45,60 @@ def handle_message( channel_name=channel_name, db_session=db_session ) document_set_names: list[str] | None = None - validity_check_enabled = ENABLE_DANSWERBOT_REFLEXION if slack_bot_config and slack_bot_config.persona: document_set_names = [ document_set.name for document_set in slack_bot_config.persona.document_sets ] - validity_check_enabled = slack_bot_config.channel_config.get( - "answer_validity_check_enabled", validity_check_enabled - ) + + reflexion = ENABLE_DANSWERBOT_REFLEXION + + # List of user id to send message to, if None, send to everyone in channel + send_to: list[str] | None = None + respond_sender_only = False + respond_team_member_list = None + if slack_bot_config and slack_bot_config.channel_config: + channel_conf = slack_bot_config.channel_config + if not skip_filters and "answer_filters" in channel_conf: + reflexion = "well_answered_postfilter" in channel_conf["answer_filters"] + + if ( + "questionmark_prefilter" in channel_conf["answer_filters"] + and "?" not in msg + ): + logger.info( + "Skipping message since it does not contain a question mark" + ) + return + logger.info( "Found slack bot config for channel. Restricting bot to use document " - f"sets: {document_set_names}, validity check enabled: {validity_check_enabled}" + f"sets: {document_set_names}, " + f"validity checks enabled: {channel_conf['answer_filters']}" ) + respond_sender_only = channel_conf.get("respond_sender_only") or False + respond_team_member_list = ( + channel_conf.get("respond_team_member_list") or None + ) + + if sender_id and (respond_sender_only or is_bot_msg): + send_to = [sender_id] + elif respond_team_member_list: + send_to = fetch_userids_from_emails(respond_team_member_list, client) + + # If configured to respond to team members only, then cannot be used with a /danswerbot command + # which would just respond to the sender + if respond_team_member_list and is_bot_msg: + if sender_id: + respond_in_thread( + client=client, + channel=channel, + receiver_ids=[sender_id], + text="The DanswerBot slash command is not enabled for this channel", + thread_ts=None, + ) + @retry( tries=num_retries, delay=0.25, @@ -70,7 +115,7 @@ def handle_message( db_session=db_session, answer_generation_timeout=answer_generation_timeout, real_time_flow=False, - enable_reflexion=validity_check_enabled, + enable_reflexion=reflexion, ) if not answer.error_msg: return answer @@ -100,6 +145,7 @@ def handle_message( respond_in_thread( client=client, channel=channel, + receiver_ids=None, text=f"Encountered exception when trying to answer: \n\n```{e}```", thread_ts=message_ts_to_respond_to, ) @@ -121,6 +167,7 @@ def handle_message( respond_in_thread( client=client, channel=channel, + receiver_ids=None, text="Found no documents when trying to answer. Did you index any documents?", thread_ts=message_ts_to_respond_to, ) @@ -134,6 +181,10 @@ def handle_message( return # convert raw response into "nicely" formatted Slack message + + # If called with the DanswerBot slash command, the question is lost so we have to reshow it + restate_question_block = get_restate_blocks(msg, is_bot_msg) + answer_blocks = build_qa_response_blocks( query_event_id=answer.query_event_id, answer=answer.answer, @@ -148,12 +199,33 @@ def handle_message( respond_in_thread( client=client, channel=channel, - blocks=answer_blocks + document_blocks, + receiver_ids=send_to, + blocks=restate_question_block + answer_blocks + document_blocks, thread_ts=message_ts_to_respond_to, # don't unfurl, since otherwise we will have 5+ previews which makes the message very long unfurl=False, ) + # For DM (ephemeral message), we need to create a thread via a normal message so the user can see + # the ephemeral message. This also will give the user a notification which ephemeral message does not. + if respond_sender_only: + respond_in_thread( + client=client, + channel=channel, + text="We've just DM-ed you the answer, hope you find it useful! 💃", + thread_ts=message_ts_to_respond_to, + ) + elif respond_team_member_list: + respond_in_thread( + client=client, + channel=channel, + text=( + "👋 Hi, we've just gathered and forwarded the relevant " + + "information to the team. They'll get back to you shortly!" + ), + thread_ts=message_ts_to_respond_to, + ) + except Exception: logger.exception( f"Unable to process message - could not respond in slack in {num_retries} attempts" diff --git a/backend/danswer/bots/slack/listener.py b/backend/danswer/bots/slack/listener.py index 086bcb375..41fbe9fe2 100644 --- a/backend/danswer/bots/slack/listener.py +++ b/backend/danswer/bots/slack/listener.py @@ -1,4 +1,5 @@ import logging +import re from collections.abc import MutableMapping from typing import Any from typing import cast @@ -51,37 +52,39 @@ def _get_socket_client() -> SocketModeClient: def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None: logger.info(f"Received Slack request of type: '{req.type}'") + if req.type == "events_api": # Acknowledge the request immediately response = SocketModeResponse(envelope_id=req.envelope_id) client.send_socket_mode_response(response) + # Verify channel is valid event = cast(dict[str, Any], req.payload.get("event", {})) channel = cast(str | None, event.get("channel")) channel_specific_logger = _ChannelIdAdapter( logger, extra={_CHANNEL_ID: channel} ) - - # Ensure that the message is a new message + of expected type - event_type = event.get("type") - if event_type != "message": - channel_specific_logger.info( - f"Ignoring non-message event of type '{event_type}' for channel '{channel}'" - ) - # this should never happen, but we can't continue without a channel since # we can't send a response without it if not channel: channel_specific_logger.error("Found message without channel - skipping") return - message_subtype = event.get("subtype") - # ignore things like channel_join, channel_leave, etc. - # NOTE: "file_share" is just a message with a file attachment, so we - # should not ignore it - if message_subtype not in [None, "file_share"]: + event = cast(dict[str, Any], req.payload.get("event", {})) + + # Ensure that the message is a new message + of expected type + event_type = event.get("type") + if event_type not in ["app_mention", "message"]: channel_specific_logger.info( - f"Ignoring message with subtype '{message_subtype}' since is is a special message type" + f"Ignoring non-message event of type '{event_type}' for channel '{channel}'" + ) + return + + # Don't insert Danswer thoughts if there's already a long conversation + # Or if a bunch of blocks already came through from responding to the @DanswerBot tag + if len(event.get("blocks", [])) > 10: + channel_specific_logger.debug( + "Ignoring message because thread is already long or has been answered to." ) return @@ -89,6 +92,16 @@ def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> No channel_specific_logger.info("Ignoring message from bot") return + # Ignore things like channel_join, channel_leave, etc. + # NOTE: "file_share" is just a message with a file attachment, so we + # should not ignore it + message_subtype = event.get("subtype") + if message_subtype not in [None, "file_share"]: + channel_specific_logger.info( + f"Ignoring message with subtype '{message_subtype}' since is is a special message type" + ) + return + message_ts = event.get("ts") thread_ts = event.get("thread_ts") # Pick the root of the thread (if a thread exists) @@ -99,25 +112,68 @@ def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> No ) return - msg = cast(str | None, event.get("text")) + msg = cast(str, event.get("text", "")) if not msg: channel_specific_logger.error("Unable to process empty message") return + tagged = event_type == "app_mention" + if tagged: + logger.info("User tagged DanswerBot") + msg = re.sub(r"<@\w+>\s", "", msg) + # TODO: message should be enqueued and processed elsewhere, # but doing it here for now for simplicity handle_message( msg=msg, channel=channel, message_ts_to_respond_to=message_ts_to_respond_to, + sender_id=event.get("user") or None, client=client.web_client, + skip_filters=tagged, logger=cast(logging.Logger, channel_specific_logger), ) - channel_specific_logger.info( f"Successfully processed message with ts: '{message_ts}'" ) + if req.type == "slash_commands": + # Acknowledge the request immediately + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) + + # Verify that there's an associated channel + channel = req.payload.get("channel_id") + channel_specific_logger = _ChannelIdAdapter( + logger, extra={_CHANNEL_ID: channel} + ) + if not channel: + channel_specific_logger.error( + "Received DanswerBot command without channel - skipping" + ) + return + + msg = req.payload.get("text", "") + sender = req.payload.get("user_id") + if not sender: + raise ValueError( + "Cannot respond to DanswerBot command without sender to respond to." + ) + + handle_message( + msg=msg, + channel=channel, + message_ts_to_respond_to=None, + sender_id=sender, + client=client.web_client, + skip_filters=True, + is_bot_msg=True, + logger=cast(logging.Logger, channel_specific_logger), + ) + channel_specific_logger.info( + f"Successfully processed DanswerBot request in channel: {req.payload.get('channel_name')}" + ) + # Handle button clicks if req.type == "interactive" and req.payload.get("type") == "block_actions": # Acknowledge the request immediately diff --git a/backend/danswer/bots/slack/utils.py b/backend/danswer/bots/slack/utils.py index 2ee950aa8..9f79d0315 100644 --- a/backend/danswer/bots/slack/utils.py +++ b/backend/danswer/bots/slack/utils.py @@ -35,27 +35,47 @@ def get_web_client() -> WebClient: def respond_in_thread( client: WebClient, channel: str, - thread_ts: str, + thread_ts: str | None, text: str | None = None, blocks: list[Block] | None = None, + receiver_ids: list[str] | None = None, metadata: Metadata | None = None, unfurl: bool = True, ) -> None: if not text and not blocks: raise ValueError("One of `text` or `blocks` must be provided") - slack_call = make_slack_api_rate_limited(client.chat_postMessage) - response = slack_call( - channel=channel, - text=text, - blocks=blocks, - thread_ts=thread_ts, - metadata=metadata, - unfurl_links=unfurl, - unfurl_media=unfurl, - ) - if not response.get("ok"): - raise RuntimeError(f"Unable to post message: {response}") + if not receiver_ids: + slack_call = make_slack_api_rate_limited(client.chat_postMessage) + else: + slack_call = make_slack_api_rate_limited(client.chat_postEphemeral) + + if not receiver_ids: + response = slack_call( + channel=channel, + text=text, + blocks=blocks, + thread_ts=thread_ts, + metadata=metadata, + unfurl_links=unfurl, + unfurl_media=unfurl, + ) + if not response.get("ok"): + raise RuntimeError(f"Failed to post message: {response}") + else: + for receiver in receiver_ids: + response = slack_call( + channel=channel, + user=receiver, + text=text, + blocks=blocks, + thread_ts=thread_ts, + metadata=metadata, + unfurl_links=unfurl, + unfurl_media=unfurl, + ) + if not response.get("ok"): + raise RuntimeError(f"Failed to post message: {response}") def build_feedback_block_id( @@ -136,3 +156,21 @@ def get_channel_from_id(client: WebClient, channel_id: str) -> dict[str, Any]: def get_channel_name_from_id(client: WebClient, channel_id: str) -> str: return get_channel_from_id(client, channel_id)["name"] + + +def fetch_userids_from_emails(user_emails: list[str], client: WebClient) -> list[str]: + user_ids: list[str] = [] + for email in user_emails: + try: + user = client.users_lookupByEmail(email=email) + user_ids.append(user.data["user"]["id"]) # type: ignore + except Exception: + logger.error(f"Was not able to find slack user by email: {email}") + + if not user_ids: + raise RuntimeError( + "Was not able to find any Slack users to respond to. " + "No email was parsed into a valid slack account." + ) + + return user_ids diff --git a/backend/danswer/configs/app_configs.py b/backend/danswer/configs/app_configs.py index 777061348..070eb2324 100644 --- a/backend/danswer/configs/app_configs.py +++ b/backend/danswer/configs/app_configs.py @@ -232,6 +232,7 @@ DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER = os.environ.get( ).lower() not in ["false", ""] # Add a second LLM call post Answer to verify if the Answer is valid # Throws out answers that don't directly or fully answer the user query +# This is the default for all DanswerBot channels unless the bot is configured individually ENABLE_DANSWERBOT_REFLEXION = ( os.environ.get("ENABLE_DANSWERBOT_REFLEXION", "").lower() == "true" ) diff --git a/backend/danswer/db/models.py b/backend/danswer/db/models.py index dfa90a8df..c78abf8a1 100644 --- a/backend/danswer/db/models.py +++ b/backend/danswer/db/models.py @@ -482,8 +482,9 @@ class ChannelConfig(TypedDict): in Postgres""" channel_names: list[str] - answer_validity_check_enabled: NotRequired[bool] # not specified => False - team_members: NotRequired[list[str]] + respond_sender_only: NotRequired[bool] # defaults to False + respond_team_member_list: NotRequired[list[str]] + answer_filters: NotRequired[list[str]] class SlackBotConfig(Base): diff --git a/backend/danswer/server/models.py b/backend/danswer/server/models.py index 8c45d55f8..fdc8e5b95 100644 --- a/backend/danswer/server/models.py +++ b/backend/danswer/server/models.py @@ -6,9 +6,11 @@ from typing import TypeVar from uuid import UUID from pydantic import BaseModel +from pydantic import validator from pydantic.generics import GenericModel from danswer.auth.schemas import UserRole +from danswer.bots.slack.config import VALID_SLACK_FILTERS from danswer.configs.app_configs import MASK_CREDENTIAL_PREFIX from danswer.configs.constants import AuthType from danswer.configs.constants import DocumentSource @@ -434,7 +436,18 @@ class SlackBotConfigCreationRequest(BaseModel): # for now for simplicity / speed of development document_sets: list[int] channel_names: list[str] - answer_validity_check_enabled: bool + # If not responder_sender_only and no team members, assume respond in the channel to everyone + respond_sender_only: bool = False + respond_team_member_list: list[str] = [] + answer_filters: list[str] = [] + + @validator("answer_filters", pre=True) + def validate_filters(cls, value: list[str]) -> list[str]: + if any(test not in VALID_SLACK_FILTERS for test in value): + raise ValueError( + f"Slack Answer filters must be one of {VALID_SLACK_FILTERS}" + ) + return value class SlackBotConfig(BaseModel): diff --git a/backend/danswer/server/slack_bot_management.py b/backend/danswer/server/slack_bot_management.py index 4e2b8d267..cb91a9706 100644 --- a/backend/danswer/server/slack_bot_management.py +++ b/backend/danswer/server/slack_bot_management.py @@ -23,13 +23,19 @@ from danswer.server.models import SlackBotTokens router = APIRouter(prefix="/manage") -@router.post("/admin/slack-bot/config") -def create_slack_bot_config( +def _form_channel_config( slack_bot_config_creation_request: SlackBotConfigCreationRequest, - db_session: Session = Depends(get_session), - _: User | None = Depends(current_admin_user), -) -> SlackBotConfig: - if not slack_bot_config_creation_request.channel_names: + current_slack_bot_config_id: int | None, + db_session: Session, +) -> ChannelConfig: + raw_channel_names = slack_bot_config_creation_request.channel_names + respond_sender_only = slack_bot_config_creation_request.respond_sender_only + respond_team_member_list = ( + slack_bot_config_creation_request.respond_team_member_list + ) + answer_filters = slack_bot_config_creation_request.answer_filters + + if not raw_channel_names: raise HTTPException( status_code=400, detail="Must provide at least one channel name", @@ -37,8 +43,8 @@ def create_slack_bot_config( try: cleaned_channel_names = validate_channel_names( - channel_names=slack_bot_config_creation_request.channel_names, - current_slack_bot_config_id=None, + channel_names=raw_channel_names, + current_slack_bot_config_id=current_slack_bot_config_id, db_session=db_session, ) except ValueError as e: @@ -47,10 +53,35 @@ def create_slack_bot_config( detail=str(e), ) + if respond_sender_only and respond_team_member_list: + raise ValueError( + "Cannot set DanswerBot to only respond to sender and " + "also respond to a predetermined set of users. This is not logically possible..." + ) + channel_config: ChannelConfig = { "channel_names": cleaned_channel_names, - "answer_validity_check_enabled": slack_bot_config_creation_request.answer_validity_check_enabled, } + if respond_sender_only is not None: + channel_config["respond_sender_only"] = respond_sender_only + if respond_team_member_list: + channel_config["respond_team_member_list"] = respond_team_member_list + if answer_filters: + channel_config["answer_filters"] = answer_filters + + return channel_config + + +@router.post("/admin/slack-bot/config") +def create_slack_bot_config( + slack_bot_config_creation_request: SlackBotConfigCreationRequest, + db_session: Session = Depends(get_session), + _: User | None = Depends(current_admin_user), +) -> SlackBotConfig: + channel_config = _form_channel_config( + slack_bot_config_creation_request, None, db_session + ) + slack_bot_config_model = insert_slack_bot_config( document_sets=slack_bot_config_creation_request.document_sets, channel_config=channel_config, @@ -75,31 +106,14 @@ def patch_slack_bot_config( db_session: Session = Depends(get_session), _: User | None = Depends(current_admin_user), ) -> SlackBotConfig: - if not slack_bot_config_creation_request.channel_names: - raise HTTPException( - status_code=400, - detail="Must provide at least one channel name", - ) - - try: - cleaned_channel_names = validate_channel_names( - channel_names=slack_bot_config_creation_request.channel_names, - current_slack_bot_config_id=slack_bot_config_id, - db_session=db_session, - ) - except ValueError as e: - raise HTTPException( - status_code=400, - detail=str(e), - ) + channel_config = _form_channel_config( + slack_bot_config_creation_request, slack_bot_config_id, db_session + ) slack_bot_config_model = update_slack_bot_config( slack_bot_config_id=slack_bot_config_id, document_sets=slack_bot_config_creation_request.document_sets, - channel_config={ - "channel_names": cleaned_channel_names, - "answer_validity_check_enabled": slack_bot_config_creation_request.answer_validity_check_enabled, - }, + channel_config=channel_config, db_session=db_session, ) return SlackBotConfig( diff --git a/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx b/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx index 916fca142..00c01a4e2 100644 --- a/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx +++ b/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx @@ -7,7 +7,6 @@ import { TextArrayField, } from "@/components/admin/connectors/Field"; import { createSlackBotConfig, updateSlackBotConfig } from "./lib"; -import { channel } from "diagnostics_channel"; interface SetCreationPopupProps { onClose: () => void; @@ -39,9 +38,18 @@ export const SlackBotCreationForm = ({ channel_names: existingSlackBotConfig ? existingSlackBotConfig.channel_config.channel_names : ([] as string[]), - answer_validity_check_enabled: + answer_validity_check_enabled: ( + existingSlackBotConfig?.channel_config?.answer_filters || [] + ).includes("well_answered_postfilter"), + questionmark_prefilter_enabled: ( + existingSlackBotConfig?.channel_config?.answer_filters || [] + ).includes("questionmark_prefilter"), + respond_sender_only: + existingSlackBotConfig?.channel_config?.respond_sender_only || + false, + respond_team_member_list: existingSlackBotConfig?.channel_config - ?.answer_validity_check_enabled || false, + ?.respond_team_member_list || ([] as string[]), document_sets: existingSlackBotConfig ? existingSlackBotConfig.document_sets.map( (documentSet) => documentSet.id @@ -51,6 +59,9 @@ export const SlackBotCreationForm = ({ validationSchema={Yup.object().shape({ channel_names: Yup.array().of(Yup.string()), answer_validity_check_enabled: Yup.boolean().required(), + questionmark_prefilter_enabled: Yup.boolean().required(), + respond_sender_only: Yup.boolean().required(), + respond_team_member_list: Yup.array().of(Yup.string()).required(), document_sets: Yup.array().of(Yup.number()), })} onSubmit={async (values, formikHelpers) => { @@ -62,6 +73,10 @@ export const SlackBotCreationForm = ({ channel_names: values.channel_names.filter( (channelName) => channelName !== "" ), + respond_team_member_list: + values.respond_team_member_list.filter( + (teamMemberEmail) => teamMemberEmail !== "" + ), }; let response; @@ -124,6 +139,30 @@ export const SlackBotCreationForm = ({ subtext="If set, will only answer questions that the model determines it can answer" />
+ +
+ +
+ +
( diff --git a/web/src/app/admin/bot/lib.ts b/web/src/app/admin/bot/lib.ts index 57f940dbc..6c7a00921 100644 --- a/web/src/app/admin/bot/lib.ts +++ b/web/src/app/admin/bot/lib.ts @@ -4,8 +4,36 @@ interface SlackBotConfigCreationRequest { document_sets: number[]; channel_names: string[]; answer_validity_check_enabled: boolean; + questionmark_prefilter_enabled: boolean; + respond_sender_only: boolean; + respond_team_member_list: string[]; } +const buildFiltersFromCreationRequest = ( + creationRequest: SlackBotConfigCreationRequest +): string[] => { + const answerFilters = [] as string[]; + if (creationRequest.answer_validity_check_enabled) { + answerFilters.push("well_answered_postfilter"); + } + if (creationRequest.questionmark_prefilter_enabled) { + answerFilters.push("questionmark_prefilter"); + } + return answerFilters; +}; + +const buildRequestBodyFromCreationRequest = ( + creationRequest: SlackBotConfigCreationRequest +) => { + return JSON.stringify({ + channel_names: creationRequest.channel_names, + respond_sender_only: creationRequest.respond_sender_only, + respond_team_member_list: creationRequest.respond_team_member_list, + document_sets: creationRequest.document_sets, + answer_filters: buildFiltersFromCreationRequest(creationRequest), + }); +}; + export const createSlackBotConfig = async ( creationRequest: SlackBotConfigCreationRequest ) => { @@ -14,7 +42,7 @@ export const createSlackBotConfig = async ( headers: { "Content-Type": "application/json", }, - body: JSON.stringify(creationRequest), + body: buildRequestBodyFromCreationRequest(creationRequest), }); }; @@ -27,7 +55,7 @@ export const updateSlackBotConfig = async ( headers: { "Content-Type": "application/json", }, - body: JSON.stringify(creationRequest), + body: buildRequestBodyFromCreationRequest(creationRequest), }); }; diff --git a/web/src/app/admin/bot/page.tsx b/web/src/app/admin/bot/page.tsx index e84e1c475..08d80cc99 100644 --- a/web/src/app/admin/bot/page.tsx +++ b/web/src/app/admin/bot/page.tsx @@ -4,12 +4,7 @@ import { Button } from "@/components/Button"; import { ThreeDotsLoader } from "@/components/Loading"; import { PageSelector } from "@/components/PageSelector"; import { BasicTable } from "@/components/admin/connectors/BasicTable"; -import { - BookmarkIcon, - CPUIcon, - EditIcon, - TrashIcon, -} from "@/components/icons/icons"; +import { CPUIcon, EditIcon, TrashIcon } from "@/components/icons/icons"; import { DocumentSet, SlackBotConfig } from "@/lib/types"; import { useState } from "react"; import { useSlackBotConfigs, useSlackBotTokens } from "./hooks"; @@ -94,10 +89,18 @@ const SlackBotConfigsTable = ({ header: "Document Sets", key: "document_sets", }, + { + header: "Team Members", + key: "team_members", + }, { header: "Hide Non-Answers", key: "answer_validity_check_enabled", }, + { + header: "Questions Only", + key: "question_mark_only", + }, { header: "Delete", key: "delete", @@ -130,8 +133,23 @@ const SlackBotConfigsTable = ({ .join(", ")}
), - answer_validity_check_enabled: slackBotConfig.channel_config - .answer_validity_check_enabled ? ( + team_members: ( +
+ {( + slackBotConfig.channel_config.respond_team_member_list || [] + ).join(", ")} +
+ ), + answer_validity_check_enabled: ( + slackBotConfig.channel_config.answer_filters || [] + ).includes("well_answered_postfilter") ? ( +
Yes
+ ) : ( +
No
+ ), + question_mark_only: ( + slackBotConfig.channel_config.answer_filters || [] + ).includes("questionmark_prefilter") ? (
Yes
) : (
No
diff --git a/web/src/app/admin/connectors/hubspot/page.tsx b/web/src/app/admin/connectors/hubspot/page.tsx index 04559e432..41853359a 100644 --- a/web/src/app/admin/connectors/hubspot/page.tsx +++ b/web/src/app/admin/connectors/hubspot/page.tsx @@ -64,9 +64,10 @@ const Main = () => { (connectorIndexingStatus) => connectorIndexingStatus.connector.source === "hubspot" ); - const hubSpotCredential: Credential = credentialsData.filter( - (credential) => credential.credential_json?.hubspot_access_token - )[0]; + const hubSpotCredential: Credential = + credentialsData.filter( + (credential) => credential.credential_json?.hubspot_access_token + )[0]; return ( <> @@ -124,7 +125,7 @@ const Main = () => { validationSchema={Yup.object().shape({ hubspot_access_token: Yup.string().required( "Please enter your HubSpot Access Token" - ) + ), })} initialValues={{ hubspot_access_token: "", @@ -170,8 +171,8 @@ const Main = () => { ) : ( <>

- HubSpot connector is setup! We are pulling the latest tickets from HubSpot - every 10 minutes. + HubSpot connector is setup! We are pulling the latest tickets from + HubSpot every 10 minutes.

connectorIndexingStatuses={hubSpotConnectorIndexingStatuses} diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index cfaf565d1..3b62dff2f 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -238,10 +238,16 @@ export interface DocumentSet { } // SLACK BOT CONFIGS + +export type AnswerFilterOption = + | "well_answered_postfilter" + | "questionmark_prefilter"; + export interface ChannelConfig { channel_names: string[]; - answer_validity_check_enabled?: boolean; - team_members?: string[]; + respond_sender_only?: boolean; + respond_team_member_list?: string[]; + answer_filters?: AnswerFilterOption[]; } export interface SlackBotConfig {