Support more Slack Config Options (#494)

---------

Co-authored-by: Weves <chrisweaver101@gmail.com>
This commit is contained in:
Yuhong Sun
2023-10-03 14:55:29 -07:00
committed by GitHub
parent c2721c7889
commit 59bac1ca8f
14 changed files with 399 additions and 91 deletions

View File

@@ -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( def build_documents_blocks(
documents: list[SearchDoc], documents: list[SearchDoc],
query_event_id: int, query_event_id: int,

View File

@@ -4,6 +4,13 @@ from danswer.db.models import SlackBotConfig
from danswer.db.slack_bot_config import fetch_slack_bot_configs 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( def get_slack_bot_config_for_channel(
channel_name: str, db_session: Session channel_name: str, db_session: Session
) -> SlackBotConfig | None: ) -> SlackBotConfig | None:

View File

@@ -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_documents_blocks
from danswer.bots.slack.blocks import build_qa_response_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.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 get_channel_name_from_id
from danswer.bots.slack.utils import respond_in_thread from danswer.bots.slack.utils import respond_in_thread
from danswer.configs.app_configs import DANSWER_BOT_ANSWER_GENERATION_TIMEOUT from danswer.configs.app_configs import DANSWER_BOT_ANSWER_GENERATION_TIMEOUT
@@ -25,9 +27,12 @@ from danswer.server.models import QuestionRequest
def handle_message( def handle_message(
msg: str, msg: str,
channel: str, channel: str,
message_ts_to_respond_to: str, message_ts_to_respond_to: str | None,
sender_id: str | None,
client: WebClient, client: WebClient,
logger: logging.Logger, logger: logging.Logger,
skip_filters: bool = False,
is_bot_msg: bool = False,
num_retries: int = DANSWER_BOT_NUM_RETRIES, num_retries: int = DANSWER_BOT_NUM_RETRIES,
answer_generation_timeout: int = DANSWER_BOT_ANSWER_GENERATION_TIMEOUT, answer_generation_timeout: int = DANSWER_BOT_ANSWER_GENERATION_TIMEOUT,
should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS, 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 channel_name=channel_name, db_session=db_session
) )
document_set_names: list[str] | None = None document_set_names: list[str] | None = None
validity_check_enabled = ENABLE_DANSWERBOT_REFLEXION
if slack_bot_config and slack_bot_config.persona: if slack_bot_config and slack_bot_config.persona:
document_set_names = [ document_set_names = [
document_set.name document_set.name
for document_set in slack_bot_config.persona.document_sets 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( logger.info(
"Found slack bot config for channel. Restricting bot to use document " "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( @retry(
tries=num_retries, tries=num_retries,
delay=0.25, delay=0.25,
@@ -70,7 +115,7 @@ def handle_message(
db_session=db_session, db_session=db_session,
answer_generation_timeout=answer_generation_timeout, answer_generation_timeout=answer_generation_timeout,
real_time_flow=False, real_time_flow=False,
enable_reflexion=validity_check_enabled, enable_reflexion=reflexion,
) )
if not answer.error_msg: if not answer.error_msg:
return answer return answer
@@ -100,6 +145,7 @@ def handle_message(
respond_in_thread( respond_in_thread(
client=client, client=client,
channel=channel, channel=channel,
receiver_ids=None,
text=f"Encountered exception when trying to answer: \n\n```{e}```", text=f"Encountered exception when trying to answer: \n\n```{e}```",
thread_ts=message_ts_to_respond_to, thread_ts=message_ts_to_respond_to,
) )
@@ -121,6 +167,7 @@ def handle_message(
respond_in_thread( respond_in_thread(
client=client, client=client,
channel=channel, channel=channel,
receiver_ids=None,
text="Found no documents when trying to answer. Did you index any documents?", text="Found no documents when trying to answer. Did you index any documents?",
thread_ts=message_ts_to_respond_to, thread_ts=message_ts_to_respond_to,
) )
@@ -134,6 +181,10 @@ def handle_message(
return return
# convert raw response into "nicely" formatted Slack message # 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( answer_blocks = build_qa_response_blocks(
query_event_id=answer.query_event_id, query_event_id=answer.query_event_id,
answer=answer.answer, answer=answer.answer,
@@ -148,12 +199,33 @@ def handle_message(
respond_in_thread( respond_in_thread(
client=client, client=client,
channel=channel, 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, thread_ts=message_ts_to_respond_to,
# don't unfurl, since otherwise we will have 5+ previews which makes the message very long # don't unfurl, since otherwise we will have 5+ previews which makes the message very long
unfurl=False, 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: except Exception:
logger.exception( logger.exception(
f"Unable to process message - could not respond in slack in {num_retries} attempts" f"Unable to process message - could not respond in slack in {num_retries} attempts"

View File

@@ -1,4 +1,5 @@
import logging import logging
import re
from collections.abc import MutableMapping from collections.abc import MutableMapping
from typing import Any from typing import Any
from typing import cast from typing import cast
@@ -51,37 +52,39 @@ def _get_socket_client() -> SocketModeClient:
def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None: def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None:
logger.info(f"Received Slack request of type: '{req.type}'") logger.info(f"Received Slack request of type: '{req.type}'")
if req.type == "events_api": if req.type == "events_api":
# Acknowledge the request immediately # Acknowledge the request immediately
response = SocketModeResponse(envelope_id=req.envelope_id) response = SocketModeResponse(envelope_id=req.envelope_id)
client.send_socket_mode_response(response) client.send_socket_mode_response(response)
# Verify channel is valid
event = cast(dict[str, Any], req.payload.get("event", {})) event = cast(dict[str, Any], req.payload.get("event", {}))
channel = cast(str | None, event.get("channel")) channel = cast(str | None, event.get("channel"))
channel_specific_logger = _ChannelIdAdapter( channel_specific_logger = _ChannelIdAdapter(
logger, extra={_CHANNEL_ID: channel} 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 # this should never happen, but we can't continue without a channel since
# we can't send a response without it # we can't send a response without it
if not channel: if not channel:
channel_specific_logger.error("Found message without channel - skipping") channel_specific_logger.error("Found message without channel - skipping")
return return
message_subtype = event.get("subtype") event = cast(dict[str, Any], req.payload.get("event", {}))
# ignore things like channel_join, channel_leave, etc.
# NOTE: "file_share" is just a message with a file attachment, so we # Ensure that the message is a new message + of expected type
# should not ignore it event_type = event.get("type")
if message_subtype not in [None, "file_share"]: if event_type not in ["app_mention", "message"]:
channel_specific_logger.info( 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 return
@@ -89,6 +92,16 @@ def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> No
channel_specific_logger.info("Ignoring message from bot") channel_specific_logger.info("Ignoring message from bot")
return 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") message_ts = event.get("ts")
thread_ts = event.get("thread_ts") thread_ts = event.get("thread_ts")
# Pick the root of the thread (if a thread exists) # Pick the root of the thread (if a thread exists)
@@ -99,25 +112,68 @@ def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> No
) )
return return
msg = cast(str | None, event.get("text")) msg = cast(str, event.get("text", ""))
if not msg: if not msg:
channel_specific_logger.error("Unable to process empty message") channel_specific_logger.error("Unable to process empty message")
return 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, # TODO: message should be enqueued and processed elsewhere,
# but doing it here for now for simplicity # but doing it here for now for simplicity
handle_message( handle_message(
msg=msg, msg=msg,
channel=channel, channel=channel,
message_ts_to_respond_to=message_ts_to_respond_to, message_ts_to_respond_to=message_ts_to_respond_to,
sender_id=event.get("user") or None,
client=client.web_client, client=client.web_client,
skip_filters=tagged,
logger=cast(logging.Logger, channel_specific_logger), logger=cast(logging.Logger, channel_specific_logger),
) )
channel_specific_logger.info( channel_specific_logger.info(
f"Successfully processed message with ts: '{message_ts}'" 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 # Handle button clicks
if req.type == "interactive" and req.payload.get("type") == "block_actions": if req.type == "interactive" and req.payload.get("type") == "block_actions":
# Acknowledge the request immediately # Acknowledge the request immediately

View File

@@ -35,27 +35,47 @@ def get_web_client() -> WebClient:
def respond_in_thread( def respond_in_thread(
client: WebClient, client: WebClient,
channel: str, channel: str,
thread_ts: str, thread_ts: str | None,
text: str | None = None, text: str | None = None,
blocks: list[Block] | None = None, blocks: list[Block] | None = None,
receiver_ids: list[str] | None = None,
metadata: Metadata | None = None, metadata: Metadata | None = None,
unfurl: bool = True, unfurl: bool = True,
) -> None: ) -> None:
if not text and not blocks: if not text and not blocks:
raise ValueError("One of `text` or `blocks` must be provided") raise ValueError("One of `text` or `blocks` must be provided")
slack_call = make_slack_api_rate_limited(client.chat_postMessage) if not receiver_ids:
response = slack_call( slack_call = make_slack_api_rate_limited(client.chat_postMessage)
channel=channel, else:
text=text, slack_call = make_slack_api_rate_limited(client.chat_postEphemeral)
blocks=blocks,
thread_ts=thread_ts, if not receiver_ids:
metadata=metadata, response = slack_call(
unfurl_links=unfurl, channel=channel,
unfurl_media=unfurl, text=text,
) blocks=blocks,
if not response.get("ok"): thread_ts=thread_ts,
raise RuntimeError(f"Unable to post message: {response}") 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( 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: def get_channel_name_from_id(client: WebClient, channel_id: str) -> str:
return get_channel_from_id(client, channel_id)["name"] 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

View File

@@ -232,6 +232,7 @@ DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER = os.environ.get(
).lower() not in ["false", ""] ).lower() not in ["false", ""]
# Add a second LLM call post Answer to verify if the Answer is valid # 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 # 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 = ( ENABLE_DANSWERBOT_REFLEXION = (
os.environ.get("ENABLE_DANSWERBOT_REFLEXION", "").lower() == "true" os.environ.get("ENABLE_DANSWERBOT_REFLEXION", "").lower() == "true"
) )

View File

@@ -482,8 +482,9 @@ class ChannelConfig(TypedDict):
in Postgres""" in Postgres"""
channel_names: list[str] channel_names: list[str]
answer_validity_check_enabled: NotRequired[bool] # not specified => False respond_sender_only: NotRequired[bool] # defaults to False
team_members: NotRequired[list[str]] respond_team_member_list: NotRequired[list[str]]
answer_filters: NotRequired[list[str]]
class SlackBotConfig(Base): class SlackBotConfig(Base):

View File

@@ -6,9 +6,11 @@ from typing import TypeVar
from uuid import UUID from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
from pydantic import validator
from pydantic.generics import GenericModel from pydantic.generics import GenericModel
from danswer.auth.schemas import UserRole 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.app_configs import MASK_CREDENTIAL_PREFIX
from danswer.configs.constants import AuthType from danswer.configs.constants import AuthType
from danswer.configs.constants import DocumentSource from danswer.configs.constants import DocumentSource
@@ -434,7 +436,18 @@ class SlackBotConfigCreationRequest(BaseModel):
# for now for simplicity / speed of development # for now for simplicity / speed of development
document_sets: list[int] document_sets: list[int]
channel_names: list[str] 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): class SlackBotConfig(BaseModel):

View File

@@ -23,13 +23,19 @@ from danswer.server.models import SlackBotTokens
router = APIRouter(prefix="/manage") router = APIRouter(prefix="/manage")
@router.post("/admin/slack-bot/config") def _form_channel_config(
def create_slack_bot_config(
slack_bot_config_creation_request: SlackBotConfigCreationRequest, slack_bot_config_creation_request: SlackBotConfigCreationRequest,
db_session: Session = Depends(get_session), current_slack_bot_config_id: int | None,
_: User | None = Depends(current_admin_user), db_session: Session,
) -> SlackBotConfig: ) -> ChannelConfig:
if not slack_bot_config_creation_request.channel_names: 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( raise HTTPException(
status_code=400, status_code=400,
detail="Must provide at least one channel name", detail="Must provide at least one channel name",
@@ -37,8 +43,8 @@ def create_slack_bot_config(
try: try:
cleaned_channel_names = validate_channel_names( cleaned_channel_names = validate_channel_names(
channel_names=slack_bot_config_creation_request.channel_names, channel_names=raw_channel_names,
current_slack_bot_config_id=None, current_slack_bot_config_id=current_slack_bot_config_id,
db_session=db_session, db_session=db_session,
) )
except ValueError as e: except ValueError as e:
@@ -47,10 +53,35 @@ def create_slack_bot_config(
detail=str(e), 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_config: ChannelConfig = {
"channel_names": cleaned_channel_names, "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( slack_bot_config_model = insert_slack_bot_config(
document_sets=slack_bot_config_creation_request.document_sets, document_sets=slack_bot_config_creation_request.document_sets,
channel_config=channel_config, channel_config=channel_config,
@@ -75,31 +106,14 @@ def patch_slack_bot_config(
db_session: Session = Depends(get_session), db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user), _: User | None = Depends(current_admin_user),
) -> SlackBotConfig: ) -> SlackBotConfig:
if not slack_bot_config_creation_request.channel_names: channel_config = _form_channel_config(
raise HTTPException( slack_bot_config_creation_request, slack_bot_config_id, db_session
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),
)
slack_bot_config_model = update_slack_bot_config( slack_bot_config_model = update_slack_bot_config(
slack_bot_config_id=slack_bot_config_id, slack_bot_config_id=slack_bot_config_id,
document_sets=slack_bot_config_creation_request.document_sets, document_sets=slack_bot_config_creation_request.document_sets,
channel_config={ channel_config=channel_config,
"channel_names": cleaned_channel_names,
"answer_validity_check_enabled": slack_bot_config_creation_request.answer_validity_check_enabled,
},
db_session=db_session, db_session=db_session,
) )
return SlackBotConfig( return SlackBotConfig(

View File

@@ -7,7 +7,6 @@ import {
TextArrayField, TextArrayField,
} from "@/components/admin/connectors/Field"; } from "@/components/admin/connectors/Field";
import { createSlackBotConfig, updateSlackBotConfig } from "./lib"; import { createSlackBotConfig, updateSlackBotConfig } from "./lib";
import { channel } from "diagnostics_channel";
interface SetCreationPopupProps { interface SetCreationPopupProps {
onClose: () => void; onClose: () => void;
@@ -39,9 +38,18 @@ export const SlackBotCreationForm = ({
channel_names: existingSlackBotConfig channel_names: existingSlackBotConfig
? existingSlackBotConfig.channel_config.channel_names ? existingSlackBotConfig.channel_config.channel_names
: ([] as string[]), : ([] 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 existingSlackBotConfig?.channel_config
?.answer_validity_check_enabled || false, ?.respond_team_member_list || ([] as string[]),
document_sets: existingSlackBotConfig document_sets: existingSlackBotConfig
? existingSlackBotConfig.document_sets.map( ? existingSlackBotConfig.document_sets.map(
(documentSet) => documentSet.id (documentSet) => documentSet.id
@@ -51,6 +59,9 @@ export const SlackBotCreationForm = ({
validationSchema={Yup.object().shape({ validationSchema={Yup.object().shape({
channel_names: Yup.array().of(Yup.string()), channel_names: Yup.array().of(Yup.string()),
answer_validity_check_enabled: Yup.boolean().required(), 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()), document_sets: Yup.array().of(Yup.number()),
})} })}
onSubmit={async (values, formikHelpers) => { onSubmit={async (values, formikHelpers) => {
@@ -62,6 +73,10 @@ export const SlackBotCreationForm = ({
channel_names: values.channel_names.filter( channel_names: values.channel_names.filter(
(channelName) => channelName !== "" (channelName) => channelName !== ""
), ),
respond_team_member_list:
values.respond_team_member_list.filter(
(teamMemberEmail) => teamMemberEmail !== ""
),
}; };
let response; let response;
@@ -124,6 +139,30 @@ export const SlackBotCreationForm = ({
subtext="If set, will only answer questions that the model determines it can answer" subtext="If set, will only answer questions that the model determines it can answer"
/> />
<div className="border-t border-gray-700 py-2" /> <div className="border-t border-gray-700 py-2" />
<BooleanFormField
name="questionmark_prefilter_enabled"
label="Only respond to questions"
subtext="If set, will only respond to messages that contain a question mark"
/>
<div className="border-t border-gray-700 py-2" />
<BooleanFormField
name="respond_sender_only"
label="Respond to Sender Only"
subtext="If set, will respond with a message that is only visible to the sender"
/>
<div className="border-t border-gray-700 py-2" />
<TextArrayField
name="respond_team_member_list"
label="Team Members Emails:"
subtext={`If specified, DanswerBot responses will only be
visible to members in this list. This is
useful if you want DanswerBot to operate in an
"assistant" mode, where it helps the team members find
answers, but let's them build on top of DanswerBot's response / throw
out the occasional incorrect answer.`}
values={values}
/>
<div className="border-t border-gray-700 py-2" />
<FieldArray <FieldArray
name="document_sets" name="document_sets"
render={(arrayHelpers: ArrayHelpers) => ( render={(arrayHelpers: ArrayHelpers) => (

View File

@@ -4,8 +4,36 @@ interface SlackBotConfigCreationRequest {
document_sets: number[]; document_sets: number[];
channel_names: string[]; channel_names: string[];
answer_validity_check_enabled: boolean; 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 ( export const createSlackBotConfig = async (
creationRequest: SlackBotConfigCreationRequest creationRequest: SlackBotConfigCreationRequest
) => { ) => {
@@ -14,7 +42,7 @@ export const createSlackBotConfig = async (
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(creationRequest), body: buildRequestBodyFromCreationRequest(creationRequest),
}); });
}; };
@@ -27,7 +55,7 @@ export const updateSlackBotConfig = async (
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(creationRequest), body: buildRequestBodyFromCreationRequest(creationRequest),
}); });
}; };

View File

@@ -4,12 +4,7 @@ import { Button } from "@/components/Button";
import { ThreeDotsLoader } from "@/components/Loading"; import { ThreeDotsLoader } from "@/components/Loading";
import { PageSelector } from "@/components/PageSelector"; import { PageSelector } from "@/components/PageSelector";
import { BasicTable } from "@/components/admin/connectors/BasicTable"; import { BasicTable } from "@/components/admin/connectors/BasicTable";
import { import { CPUIcon, EditIcon, TrashIcon } from "@/components/icons/icons";
BookmarkIcon,
CPUIcon,
EditIcon,
TrashIcon,
} from "@/components/icons/icons";
import { DocumentSet, SlackBotConfig } from "@/lib/types"; import { DocumentSet, SlackBotConfig } from "@/lib/types";
import { useState } from "react"; import { useState } from "react";
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks"; import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
@@ -94,10 +89,18 @@ const SlackBotConfigsTable = ({
header: "Document Sets", header: "Document Sets",
key: "document_sets", key: "document_sets",
}, },
{
header: "Team Members",
key: "team_members",
},
{ {
header: "Hide Non-Answers", header: "Hide Non-Answers",
key: "answer_validity_check_enabled", key: "answer_validity_check_enabled",
}, },
{
header: "Questions Only",
key: "question_mark_only",
},
{ {
header: "Delete", header: "Delete",
key: "delete", key: "delete",
@@ -130,8 +133,23 @@ const SlackBotConfigsTable = ({
.join(", ")} .join(", ")}
</div> </div>
), ),
answer_validity_check_enabled: slackBotConfig.channel_config team_members: (
.answer_validity_check_enabled ? ( <div>
{(
slackBotConfig.channel_config.respond_team_member_list || []
).join(", ")}
</div>
),
answer_validity_check_enabled: (
slackBotConfig.channel_config.answer_filters || []
).includes("well_answered_postfilter") ? (
<div className="text-gray-300">Yes</div>
) : (
<div className="text-gray-300">No</div>
),
question_mark_only: (
slackBotConfig.channel_config.answer_filters || []
).includes("questionmark_prefilter") ? (
<div className="text-gray-300">Yes</div> <div className="text-gray-300">Yes</div>
) : ( ) : (
<div className="text-gray-300">No</div> <div className="text-gray-300">No</div>

View File

@@ -64,9 +64,10 @@ const Main = () => {
(connectorIndexingStatus) => (connectorIndexingStatus) =>
connectorIndexingStatus.connector.source === "hubspot" connectorIndexingStatus.connector.source === "hubspot"
); );
const hubSpotCredential: Credential<HubSpotCredentialJson> = credentialsData.filter( const hubSpotCredential: Credential<HubSpotCredentialJson> =
(credential) => credential.credential_json?.hubspot_access_token credentialsData.filter(
)[0]; (credential) => credential.credential_json?.hubspot_access_token
)[0];
return ( return (
<> <>
@@ -124,7 +125,7 @@ const Main = () => {
validationSchema={Yup.object().shape({ validationSchema={Yup.object().shape({
hubspot_access_token: Yup.string().required( hubspot_access_token: Yup.string().required(
"Please enter your HubSpot Access Token" "Please enter your HubSpot Access Token"
) ),
})} })}
initialValues={{ initialValues={{
hubspot_access_token: "", hubspot_access_token: "",
@@ -170,8 +171,8 @@ const Main = () => {
) : ( ) : (
<> <>
<p className="text-sm mb-2"> <p className="text-sm mb-2">
HubSpot connector is setup! We are pulling the latest tickets from HubSpot HubSpot connector is setup! We are pulling the latest tickets from
every <b>10</b> minutes. HubSpot every <b>10</b> minutes.
</p> </p>
<ConnectorsTable<HubSpotConfig, HubSpotCredentialJson> <ConnectorsTable<HubSpotConfig, HubSpotCredentialJson>
connectorIndexingStatuses={hubSpotConnectorIndexingStatuses} connectorIndexingStatuses={hubSpotConnectorIndexingStatuses}

View File

@@ -238,10 +238,16 @@ export interface DocumentSet {
} }
// SLACK BOT CONFIGS // SLACK BOT CONFIGS
export type AnswerFilterOption =
| "well_answered_postfilter"
| "questionmark_prefilter";
export interface ChannelConfig { export interface ChannelConfig {
channel_names: string[]; channel_names: string[];
answer_validity_check_enabled?: boolean; respond_sender_only?: boolean;
team_members?: string[]; respond_team_member_list?: string[];
answer_filters?: AnswerFilterOption[];
} }
export interface SlackBotConfig { export interface SlackBotConfig {