Officially support Slack DMs to DanswerBot (#556)

This commit is contained in:
Yuhong Sun 2023-10-10 18:07:29 -07:00 committed by GitHub
parent fa460f4da1
commit 31d5fc6d31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 58 additions and 34 deletions

View File

@ -12,8 +12,11 @@ VALID_SLACK_FILTERS = [
def get_slack_bot_config_for_channel( def get_slack_bot_config_for_channel(
channel_name: str, db_session: Session channel_name: str | None, db_session: Session
) -> SlackBotConfig | None: ) -> SlackBotConfig | None:
if not channel_name:
return None
slack_bot_configs = fetch_slack_bot_configs(db_session=db_session) slack_bot_configs = fetch_slack_bot_configs(db_session=db_session)
for config in slack_bot_configs: for config in slack_bot_configs:
if channel_name in config.channel_config["channel_names"]: if channel_name in config.channel_config["channel_names"]:

View File

@ -103,7 +103,7 @@ def handle_message(
if respond_team_member_list: if respond_team_member_list:
send_to = fetch_userids_from_emails(respond_team_member_list, client) 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 # If configured to respond to team members only, then cannot be used with a /DanswerBot command
# which would just respond to the sender # which would just respond to the sender
if respond_team_member_list and is_bot_msg: if respond_team_member_list and is_bot_msg:
if sender_id: if sender_id:

View File

@ -22,6 +22,7 @@ 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_RESPOND_EVERY_CHANNEL from danswer.configs.app_configs import DANSWER_BOT_RESPOND_EVERY_CHANNEL
from danswer.configs.app_configs import DANSWER_REACT_EMOJI from danswer.configs.app_configs import DANSWER_REACT_EMOJI
from danswer.configs.app_configs import NOTIFY_SLACKBOT_NO_ANSWER
from danswer.connectors.slack.utils import make_slack_api_rate_limited from danswer.connectors.slack.utils import make_slack_api_rate_limited
from danswer.db.engine import get_sqlalchemy_engine from danswer.db.engine import get_sqlalchemy_engine
from danswer.dynamic_configs.interface import ConfigNotFoundError from danswer.dynamic_configs.interface import ConfigNotFoundError
@ -80,7 +81,9 @@ def prefilter_requests(req: SocketModeRequest, client: SocketModeClient) -> bool
if event_type == "message": if event_type == "message":
bot_tag_id = client.web_client.auth_test().get("user_id") bot_tag_id = client.web_client.auth_test().get("user_id")
if bot_tag_id and bot_tag_id in msg: # DMs with the bot don't pick up the @DanswerBot so we have to keep the
# caught events_api
if bot_tag_id and bot_tag_id in msg and event.get("channel_type") != "im":
# Let the tag flow handle this case, don't reply twice # Let the tag flow handle this case, don't reply twice
return False return False
@ -170,10 +173,13 @@ def build_request_details(
tagged = event.get("type") == "app_mention" tagged = event.get("type") == "app_mention"
message_ts = event.get("ts") message_ts = event.get("ts")
thread_ts = event.get("thread_ts") thread_ts = event.get("thread_ts")
bot_tag_id = client.web_client.auth_test().get("user_id")
# Might exist even if not tagged, specifically in the case of @DanswerBot
# in DanswerBot DM channel
msg = re.sub(rf"<@{bot_tag_id}>\s", "", msg)
if tagged: if tagged:
logger.info("User tagged DanswerBot") logger.info("User tagged DanswerBot")
bot_tag_id = client.web_client.auth_test().get("user_id")
msg = re.sub(rf"<@{bot_tag_id}>\s", "", msg)
return SlackMessageInfo( return SlackMessageInfo(
msg_content=msg, msg_content=msg,
@ -248,8 +254,9 @@ def process_message(
req: SocketModeRequest, req: SocketModeRequest,
client: SocketModeClient, client: SocketModeClient,
respond_every_channel: bool = DANSWER_BOT_RESPOND_EVERY_CHANNEL, respond_every_channel: bool = DANSWER_BOT_RESPOND_EVERY_CHANNEL,
notify_no_answer: bool = NOTIFY_SLACKBOT_NO_ANSWER,
) -> None: ) -> None:
logger.info(f"Received Slack request of type: '{req.type}'") logger.debug(f"Received Slack request of type: '{req.type}'")
# Throw out requests that can't or shouldn't be handled # Throw out requests that can't or shouldn't be handled
if not prefilter_requests(req, client): if not prefilter_requests(req, client):
@ -267,32 +274,37 @@ def process_message(
channel_name=channel_name, db_session=db_session channel_name=channel_name, db_session=db_session
) )
# Be careful about this default, don't want to accidentally spam every channel # Be careful about this default, don't want to accidentally spam every channel
if slack_bot_config is None and not respond_every_channel: # Users should be able to DM slack bot in their private channels though
logger.info( if (
"Skipping message since the channel is not configured to use DanswerBot" slack_bot_config is None
and not respond_every_channel
# DMs are unnamed, don't filter those out
and channel_name is not None
# If @DanswerBot or /DanswerBot, always respond with the default configs
and not (details.is_bot_msg or details.bipass_filters)
):
return
try:
send_msg_ack_to_user(details, client)
except SlackApiError as e:
logger.error(f"Was not able to react to user message due to: {e}")
failed = handle_message(
message_info=details,
channel_config=slack_bot_config,
client=client.web_client,
) )
return
try: # Skipping answering due to pre-filtering is not considered a failure
send_msg_ack_to_user(details, client) if failed and notify_no_answer:
except SlackApiError as e: apologize_for_fail(details, client)
logger.error(f"Was not able to react to user message due to: {e}")
failed = handle_message( try:
message_info=details, remove_react(details, client)
channel_config=slack_bot_config, except SlackApiError as e:
client=client.web_client, logger.error(f"Failed to remove Reaction due to: {e}")
)
# Skipping answering due to pre-filtering is not considered a failure
if failed:
apologize_for_fail(details, client)
try:
remove_react(details, client)
except SlackApiError as e:
logger.error(f"Failed to remove Reaction due to: {e}")
def acknowledge_message(req: SocketModeRequest, client: SocketModeClient) -> None: def acknowledge_message(req: SocketModeRequest, client: SocketModeClient) -> None:

View File

@ -8,6 +8,7 @@ from typing import cast
from retry import retry from retry import retry
from slack_sdk import WebClient from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from slack_sdk.models.blocks import Block from slack_sdk.models.blocks import Block
from slack_sdk.models.metadata import Metadata from slack_sdk.models.metadata import Metadata
@ -170,8 +171,12 @@ def get_channel_from_id(client: WebClient, channel_id: str) -> dict[str, Any]:
return response["channel"] return response["channel"]
def get_channel_name_from_id(client: WebClient, channel_id: str) -> str: def get_channel_name_from_id(client: WebClient, channel_id: str) -> str | None:
return get_channel_from_id(client, channel_id)["name"] try:
return get_channel_from_id(client, channel_id).get("name")
except SlackApiError:
# Private channels such as DMs don't have a name
return None
def fetch_userids_from_emails(user_emails: list[str], client: WebClient) -> list[str]: def fetch_userids_from_emails(user_emails: list[str], client: WebClient) -> list[str]:

View File

@ -238,12 +238,16 @@ DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER = os.environ.get(
"DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER", "" "DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER", ""
).lower() not in ["false", ""] ).lower() not in ["false", ""]
DANSWER_REACT_EMOJI = os.environ.get("DANSWER_REACT_EMOJI") or "eyes" DANSWER_REACT_EMOJI = os.environ.get("DANSWER_REACT_EMOJI") or "eyes"
# Should DanswerBot send an apology message if it's not able to find an answer
# That way the user isn't confused as to why DanswerBot reacted but then said nothing
# Off by default to be less intrusive (don't want to give a notif that just says we couldnt help)
NOTIFY_SLACKBOT_NO_ANSWER = (
os.environ.get("NOTIFY_SLACKBOT_NO_ANSWER", "").lower() == "true"
)
# Default is only respond in channels that are included by a slack config set in the UI # Default is only respond in channels that are included by a slack config set in the UI
DANSWER_BOT_RESPOND_EVERY_CHANNEL = ( DANSWER_BOT_RESPOND_EVERY_CHANNEL = (
os.environ.get("DANSWER_BOT_RESPOND_EVERY_CHANNEL", "").lower() == "true" os.environ.get("DANSWER_BOT_RESPOND_EVERY_CHANNEL", "").lower() == "true"
) )
# 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 # This is the default for all DanswerBot channels unless the bot is configured individually

View File

@ -284,7 +284,6 @@ def upsert_persona(
persona.default_persona = default_persona persona.default_persona = default_persona
else: else:
persona = Persona( persona = Persona(
id=persona_id,
name=name, name=name,
retrieval_enabled=retrieval_enabled, retrieval_enabled=retrieval_enabled,
system_text=system_text, system_text=system_text,

View File

@ -81,6 +81,7 @@ services:
- DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER=${DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER:-} - DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER=${DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER:-}
- DANSWER_BOT_DISPLAY_ERROR_MSGS=${DANSWER_BOT_DISPLAY_ERROR_MSGS:-} - DANSWER_BOT_DISPLAY_ERROR_MSGS=${DANSWER_BOT_DISPLAY_ERROR_MSGS:-}
- DANSWER_BOT_RESPOND_EVERY_CHANNEL=${DANSWER_BOT_RESPOND_EVERY_CHANNEL:-} - DANSWER_BOT_RESPOND_EVERY_CHANNEL=${DANSWER_BOT_RESPOND_EVERY_CHANNEL:-}
- NOTIFY_SLACKBOT_NO_ANSWER=${NOTIFY_SLACKBOT_NO_ANSWER:-}
# Don't change the NLP model configs unless you know what you're doing # Don't change the NLP model configs unless you know what you're doing
- DOCUMENT_ENCODER_MODEL=${DOCUMENT_ENCODER_MODEL:-} - DOCUMENT_ENCODER_MODEL=${DOCUMENT_ENCODER_MODEL:-}
- NORMALIZE_EMBEDDINGS=${NORMALIZE_EMBEDDINGS:-} - NORMALIZE_EMBEDDINGS=${NORMALIZE_EMBEDDINGS:-}