Slack Bot to respond very quickly to acknowledge seeing the question (#544)

This commit is contained in:
Yuhong Sun
2023-10-09 09:24:28 -07:00
committed by GitHub
parent dff7a4ba1e
commit 30cdc5c9de
6 changed files with 322 additions and 192 deletions

View File

@@ -1,2 +1,3 @@
LIKE_BLOCK_ACTION_ID = "feedback-like" LIKE_BLOCK_ACTION_ID = "feedback-like"
DISLIKE_BLOCK_ACTION_ID = "feedback-dislike" DISLIKE_BLOCK_ACTION_ID = "feedback-dislike"
SLACK_CHANNEL_ID = "channel_id"

View File

@@ -1,4 +1,5 @@
import logging import logging
from typing import cast
from retry import retry from retry import retry
from slack_sdk import WebClient from slack_sdk import WebClient
@@ -7,111 +8,112 @@ 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.blocks import get_restate_blocks
from danswer.bots.slack.config import get_slack_bot_config_for_channel from danswer.bots.slack.constants import SLACK_CHANNEL_ID
from danswer.bots.slack.models import SlackMessageInfo
from danswer.bots.slack.utils import ChannelIdAdapter
from danswer.bots.slack.utils import fetch_userids_from_emails 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.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
from danswer.configs.app_configs import DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER from danswer.configs.app_configs import DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER
from danswer.configs.app_configs import DANSWER_BOT_DISPLAY_ERROR_MSGS from danswer.configs.app_configs import DANSWER_BOT_DISPLAY_ERROR_MSGS
from danswer.configs.app_configs import DANSWER_BOT_NUM_RETRIES from danswer.configs.app_configs import DANSWER_BOT_NUM_RETRIES
from danswer.configs.app_configs import DANSWER_BOT_RESPOND_EVERY_CHANNEL
from danswer.configs.app_configs import DOCUMENT_INDEX_NAME from danswer.configs.app_configs import DOCUMENT_INDEX_NAME
from danswer.configs.app_configs import ENABLE_DANSWERBOT_REFLEXION from danswer.configs.app_configs import ENABLE_DANSWERBOT_REFLEXION
from danswer.configs.constants import DOCUMENT_SETS from danswer.configs.constants import DOCUMENT_SETS
from danswer.db.engine import get_sqlalchemy_engine from danswer.db.engine import get_sqlalchemy_engine
from danswer.db.models import SlackBotConfig
from danswer.direct_qa.answer_question import answer_qa_query from danswer.direct_qa.answer_question import answer_qa_query
from danswer.server.models import QAResponse from danswer.server.models import QAResponse
from danswer.server.models import QuestionRequest from danswer.server.models import QuestionRequest
from danswer.utils.logger import setup_logger
logger_base = setup_logger()
def handle_message( def handle_message(
msg: str, message_info: SlackMessageInfo,
channel: str, channel_config: SlackBotConfig | None,
message_ts_to_respond_to: str | None,
sender_id: str | None,
client: WebClient, client: WebClient,
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,
disable_docs_only_answer: bool = DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER, disable_docs_only_answer: bool = DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER,
respond_every_channel: bool = DANSWER_BOT_RESPOND_EVERY_CHANNEL, ) -> bool:
) -> None: """Potentially respond to the user message depending on filters and if an answer was generated
engine = get_sqlalchemy_engine()
with Session(engine) as db_session:
channel_name = get_channel_name_from_id(client=client, channel_id=channel)
slack_bot_config = get_slack_bot_config_for_channel(
channel_name=channel_name, db_session=db_session
)
if slack_bot_config is None and not respond_every_channel:
logger.info(
"Skipping message since the channel is not configured to use DanswerBot"
)
return
document_set_names: list[str] | None = None Returns True if need to respond with an additional message to the user(s) after this
if slack_bot_config and slack_bot_config.persona: function is finished. True indicates an unexpected failure that needs to be communicated
document_set_names = [ Query thrown out by filters due to config does not count as a failure that should be notified
document_set.name Danswer failing to answer/retrieve docs does count and should be notified
for document_set in slack_bot_config.persona.document_sets """
] msg = message_info.msg_content
channel = message_info.channel_to_respond
message_ts_to_respond_to = message_info.msg_to_respond
sender_id = message_info.sender
bipass_filters = message_info.bipass_filters
is_bot_msg = message_info.is_bot_msg
reflexion = ENABLE_DANSWERBOT_REFLEXION logger = cast(
logging.Logger,
ChannelIdAdapter(logger_base, extra={SLACK_CHANNEL_ID: channel}),
)
# List of user id to send message to, if None, send to everyone in channel document_set_names: list[str] | None = None
send_to: list[str] | None = None if channel_config and channel_config.persona:
respond_tag_only = False document_set_names = [
respond_team_member_list = None document_set.name for document_set in channel_config.persona.document_sets
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 ( reflexion = ENABLE_DANSWERBOT_REFLEXION
"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( # List of user id to send message to, if None, send to everyone in channel
"Found slack bot config for channel. Restricting bot to use document " send_to: list[str] | None = None
f"sets: {document_set_names}, " respond_tag_only = False
f"validity checks enabled: {channel_conf.get('answer_filters', 'NA')}" respond_team_member_list = None
) if channel_config and channel_config.channel_config:
channel_conf = channel_config.channel_config
if not bipass_filters and "answer_filters" in channel_conf:
reflexion = "well_answered_postfilter" in channel_conf["answer_filters"]
respond_tag_only = channel_conf.get("respond_tag_only") or False if (
respond_team_member_list = ( "questionmark_prefilter" in channel_conf["answer_filters"]
channel_conf.get("respond_team_member_list") or None and "?" not in msg
) ):
logger.info(
# `skip_filters=True` -> this is a tag, so we *should* respond "Skipping message since it does not contain a question mark"
if respond_tag_only and not skip_filters:
logger.info(
"Skipping message since the channel is configured such that "
"DanswerBot only responds to tags"
)
return
if 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,
) )
return False
logger.info(
"Found slack bot config for channel. Restricting bot to use document "
f"sets: {document_set_names}, "
f"validity checks enabled: {channel_conf.get('answer_filters', 'NA')}"
)
respond_tag_only = channel_conf.get("respond_tag_only") or False
respond_team_member_list = channel_conf.get("respond_team_member_list") or None
if respond_tag_only and not bipass_filters:
logger.info(
"Skipping message since the channel is configured such that "
"DanswerBot only responds to tags"
)
return False
if 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,
@@ -163,7 +165,7 @@ def handle_message(
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,
) )
return return True
if answer.eval_res_valid is False: if answer.eval_res_valid is False:
logger.info( logger.info(
@@ -171,7 +173,7 @@ def handle_message(
) )
if answer.answer: if answer.answer:
logger.debug(answer.answer) logger.debug(answer.answer)
return return True
if not answer.top_ranked_docs: if not answer.top_ranked_docs:
logger.error(f"Unable to answer question: '{msg}' - no documents found") logger.error(f"Unable to answer question: '{msg}' - no documents found")
@@ -185,14 +187,14 @@ def handle_message(
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,
) )
return return True
if not answer.answer and disable_docs_only_answer: if not answer.answer and disable_docs_only_answer:
logger.info( logger.info(
"Unable to find answer - not responding since the " "Unable to find answer - not responding since the "
"`DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER` env variable is set" "`DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER` env variable is set"
) )
return return True
# convert raw response into "nicely" formatted Slack message # convert raw response into "nicely" formatted Slack message
@@ -233,8 +235,10 @@ def handle_message(
thread_ts=message_ts_to_respond_to, thread_ts=message_ts_to_respond_to,
) )
return False
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"
) )
return return True

View File

@@ -1,46 +1,40 @@
import logging
import re import re
import time import time
from collections.abc import MutableMapping
from typing import Any from typing import Any
from typing import cast from typing import cast
from slack_sdk import WebClient from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from slack_sdk.socket_mode import SocketModeClient from slack_sdk.socket_mode import SocketModeClient
from slack_sdk.socket_mode.request import SocketModeRequest from slack_sdk.socket_mode.request import SocketModeRequest
from slack_sdk.socket_mode.response import SocketModeResponse from slack_sdk.socket_mode.response import SocketModeResponse
from sqlalchemy.orm import Session
from danswer.bots.slack.config import get_slack_bot_config_for_channel
from danswer.bots.slack.constants import SLACK_CHANNEL_ID
from danswer.bots.slack.handlers.handle_feedback import handle_slack_feedback from danswer.bots.slack.handlers.handle_feedback import handle_slack_feedback
from danswer.bots.slack.handlers.handle_message import handle_message from danswer.bots.slack.handlers.handle_message import handle_message
from danswer.bots.slack.models import SlackMessageInfo
from danswer.bots.slack.tokens import fetch_tokens from danswer.bots.slack.tokens import fetch_tokens
from danswer.bots.slack.utils import ChannelIdAdapter
from danswer.bots.slack.utils import decompose_block_id from danswer.bots.slack.utils import decompose_block_id
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_RESPOND_EVERY_CHANNEL
from danswer.configs.app_configs import DANSWER_REACT_EMOJI
from danswer.connectors.slack.utils import make_slack_api_rate_limited
from danswer.db.engine import get_sqlalchemy_engine
from danswer.dynamic_configs.interface import ConfigNotFoundError from danswer.dynamic_configs.interface import ConfigNotFoundError
from danswer.utils.logger import setup_logger from danswer.utils.logger import setup_logger
logger = setup_logger() logger = setup_logger()
_CHANNEL_ID = "channel_id"
class MissingTokensException(Exception): class MissingTokensException(Exception):
pass pass
class _ChannelIdAdapter(logging.LoggerAdapter):
"""This is used to add the channel ID to all log messages
emitted in this file"""
def process(
self, msg: str, kwargs: MutableMapping[str, Any]
) -> tuple[str, MutableMapping[str, Any]]:
channel_id = self.extra.get(_CHANNEL_ID) if self.extra else None
if channel_id:
return f"[Channel ID: {channel_id}] {msg}", kwargs
else:
return msg, kwargs
def _get_socket_client() -> SocketModeClient: def _get_socket_client() -> SocketModeClient:
# For more info on how to set this up, checkout the docs: # For more info on how to set this up, checkout the docs:
# https://docs.danswer.dev/slack_bot_setup # https://docs.danswer.dev/slack_bot_setup
@@ -55,47 +49,44 @@ def _get_socket_client() -> SocketModeClient:
) )
def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None: def prefilter_requests(req: SocketModeRequest, client: SocketModeClient) -> bool:
logger.info(f"Received Slack request of type: '{req.type}'") """True to keep going, False to ignore this Slack request"""
if req.type == "events_api": 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 # Verify channel is valid
event = cast(dict[str, Any], req.payload.get("event", {})) event = cast(dict[str, Any], req.payload.get("event", {}))
msg = cast(str | None, event.get("text"))
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={SLACK_CHANNEL_ID: 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 False
event = cast(dict[str, Any], req.payload.get("event", {})) if not msg:
channel_specific_logger.error("Cannot respond to empty message - skipping")
return False
# Ensure that the message is a new message + of expected type # Ensure that the message is a new message of expected type
event_type = event.get("type") event_type = event.get("type")
if event_type not in ["app_mention", "message"]: if event_type not in ["app_mention", "message"]:
channel_specific_logger.info( channel_specific_logger.info(
f"Ignoring non-message event of type '{event_type}' for channel '{channel}'" f"Ignoring non-message event of type '{event_type}' for channel '{channel}'"
) )
return return False
# Don't insert Danswer thoughts if there's already a long conversation if event_type == "message":
# Or if a bunch of blocks already came through from responding to the @DanswerBot tag bot_tag_id = client.web_client.auth_test().get("user_id")
if len(event.get("blocks", [])) > 10: if bot_tag_id and bot_tag_id in msg:
channel_specific_logger.debug( # Let the tag flow handle this case, don't reply twice
"Ignoring message because thread is already long or has been answered to." return False
)
return
if event.get("bot_profile"): if event.get("bot_profile"):
channel_specific_logger.info("Ignoring message from bot") channel_specific_logger.info("Ignoring message from bot")
return return False
# Ignore things like channel_join, channel_leave, etc. # Ignore things like channel_join, channel_leave, etc.
# NOTE: "file_share" is just a message with a file attachment, so we # NOTE: "file_share" is just a message with a file attachment, so we
@@ -105,114 +96,221 @@ def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> No
channel_specific_logger.info( channel_specific_logger.info(
f"Ignoring message with subtype '{message_subtype}' since is is a special message type" f"Ignoring message with subtype '{message_subtype}' since is is a special message type"
) )
return return False
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)
message_ts_to_respond_to = cast(str, thread_ts or message_ts)
if thread_ts and message_ts != thread_ts: if thread_ts and message_ts != thread_ts:
channel_specific_logger.info( channel_specific_logger.info(
"Skipping message since it is not the root of a thread" "Skipping message since it is not the root of a thread"
) )
return return False
msg = cast(str, 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 False
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": 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 # Verify that there's an associated channel
channel = req.payload.get("channel_id") channel = req.payload.get("channel_id")
channel_specific_logger = _ChannelIdAdapter( channel_specific_logger = ChannelIdAdapter(
logger, extra={_CHANNEL_ID: channel} logger, extra={SLACK_CHANNEL_ID: channel}
) )
if not channel: if not channel:
channel_specific_logger.error( channel_specific_logger.error(
"Received DanswerBot command without channel - skipping" "Received DanswerBot command without channel - skipping"
) )
return return False
msg = req.payload.get("text", "")
sender = req.payload.get("user_id") sender = req.payload.get("user_id")
if not sender: if not sender:
raise ValueError( channel_specific_logger.error(
"Cannot respond to DanswerBot command without sender to respond to." "Cannot respond to DanswerBot command without sender to respond to."
) )
return False
handle_message( return True
msg=msg,
channel=channel,
message_ts_to_respond_to=None, def process_feedback(req: SocketModeRequest, client: SocketModeClient) -> None:
sender_id=sender, actions = req.payload.get("actions")
client=client.web_client, if not actions:
skip_filters=True, logger.error("Unable to process block actions - no actions found")
return
action = cast(dict[str, Any], actions[0])
action_id = cast(str, action.get("action_id"))
block_id = cast(str, action.get("block_id"))
user_id = cast(str, req.payload["user"]["id"])
channel_id = cast(str, req.payload["container"]["channel_id"])
thread_ts = cast(str, req.payload["container"]["thread_ts"])
handle_slack_feedback(
block_id=block_id,
feedback_type=action_id,
client=client.web_client,
user_id_to_post_confirmation=user_id,
channel_id_to_post_confirmation=channel_id,
thread_ts_to_post_confirmation=thread_ts,
)
query_event_id, _, _ = decompose_block_id(block_id)
logger.info(f"Successfully handled QA feedback for event: {query_event_id}")
def build_request_details(
req: SocketModeRequest, client: SocketModeClient
) -> SlackMessageInfo:
if req.type == "events_api":
event = cast(dict[str, Any], req.payload["event"])
msg = cast(str, event["text"])
channel = cast(str, event["channel"])
tagged = event.get("type") == "app_mention"
message_ts = event.get("ts")
thread_ts = event.get("thread_ts")
if tagged:
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(
msg_content=msg,
channel_to_respond=channel,
msg_to_respond=cast(str, thread_ts or message_ts),
sender=event.get("user") or None,
bipass_filters=tagged,
is_bot_msg=False,
)
elif req.type == "slash_commands":
channel = req.payload["channel_id"]
msg = req.payload["text"]
sender = req.payload["user_id"]
return SlackMessageInfo(
msg_content=msg,
channel_to_respond=channel,
msg_to_respond=None,
sender=sender,
bipass_filters=True,
is_bot_msg=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 raise RuntimeError("Programming fault, this should never happen.")
if req.type == "interactive" and req.payload.get("type") == "block_actions":
# Acknowledge the request immediately
response = SocketModeResponse(envelope_id=req.envelope_id)
client.send_socket_mode_response(response)
actions = req.payload.get("actions")
if not actions:
logger.error("Unable to process block actions - no actions found")
return
action = cast(dict[str, Any], actions[0]) def send_msg_ack_to_user(details: SlackMessageInfo, client: SocketModeClient) -> None:
action_id = cast(str, action.get("action_id")) if details.is_bot_msg and details.sender:
block_id = cast(str, action.get("block_id")) respond_in_thread(
user_id = cast(str, req.payload["user"]["id"])
channel_id = cast(str, req.payload["container"]["channel_id"])
thread_ts = cast(str, req.payload["container"]["thread_ts"])
handle_slack_feedback(
block_id=block_id,
feedback_type=action_id,
client=client.web_client, client=client.web_client,
user_id_to_post_confirmation=user_id, channel=details.channel_to_respond,
channel_id_to_post_confirmation=channel_id, thread_ts=details.msg_to_respond,
thread_ts_to_post_confirmation=thread_ts, receiver_ids=[details.sender],
text="Hi, we're evaluating your query :face_with_monocle:",
)
return
slack_call = make_slack_api_rate_limited(client.web_client.reactions_add)
slack_call(
name=DANSWER_REACT_EMOJI,
channel=details.channel_to_respond,
timestamp=details.msg_to_respond,
)
def remove_react(details: SlackMessageInfo, client: SocketModeClient) -> None:
if details.is_bot_msg:
return
slack_call = make_slack_api_rate_limited(client.web_client.reactions_remove)
slack_call(
name=DANSWER_REACT_EMOJI,
channel=details.channel_to_respond,
timestamp=details.msg_to_respond,
)
def apologize_for_fail(
details: SlackMessageInfo,
client: SocketModeClient,
) -> None:
respond_in_thread(
client=client.web_client,
channel=details.channel_to_respond,
thread_ts=details.msg_to_respond,
text="Sorry, we weren't able to find anything relevant :cold_sweat:",
)
def process_message(
req: SocketModeRequest,
client: SocketModeClient,
respond_every_channel: bool = DANSWER_BOT_RESPOND_EVERY_CHANNEL,
) -> None:
logger.info(f"Received Slack request of type: '{req.type}'")
# Throw out requests that can't or shouldn't be handled
if not prefilter_requests(req, client):
return
details = build_request_details(req, client)
channel = details.channel_to_respond
channel_name = get_channel_name_from_id(
client=client.web_client, channel_id=channel
)
engine = get_sqlalchemy_engine()
with Session(engine) as db_session:
slack_bot_config = get_slack_bot_config_for_channel(
channel_name=channel_name, db_session=db_session
) )
query_event_id, _, _ = decompose_block_id(block_id) # Be careful about this default, don't want to accidentally spam every channel
logger.info(f"Successfully handled QA feedback for event: {query_event_id}") if slack_bot_config is None and not respond_every_channel:
logger.info(
"Skipping message since the channel is not configured to use DanswerBot"
)
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,
)
# 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:
response = SocketModeResponse(envelope_id=req.envelope_id)
client.send_socket_mode_response(response)
def process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None: def process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None:
# Always respond right away, if Slack doesn't receive these frequently enough
# it will assume the Bot is DEAD!!! :(
acknowledge_message(req, client)
try: try:
_process_slack_event(client=client, req=req) if req.type == "interactive" and req.payload.get("type") == "block_actions":
return process_feedback(req, client)
elif req.type == "events_api" or req.type == "slash_commands":
return process_message(req, client)
except Exception: except Exception:
logger.exception("Failed to process slack event") logger.exception("Failed to process slack event")

View File

@@ -0,0 +1,10 @@
from pydantic import BaseModel
class SlackMessageInfo(BaseModel):
msg_content: str
channel_to_respond: str
msg_to_respond: str | None
sender: str | None
bipass_filters: bool
is_bot_msg: bool

View File

@@ -2,6 +2,7 @@ import logging
import random import random
import re import re
import string import string
from collections.abc import MutableMapping
from typing import Any from typing import Any
from typing import cast from typing import cast
@@ -10,6 +11,7 @@ from slack_sdk import WebClient
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
from danswer.bots.slack.constants import SLACK_CHANNEL_ID
from danswer.bots.slack.tokens import fetch_tokens from danswer.bots.slack.tokens import fetch_tokens
from danswer.configs.app_configs import DANSWER_BOT_NUM_RETRIES from danswer.configs.app_configs import DANSWER_BOT_NUM_RETRIES
from danswer.configs.constants import ID_SEPARATOR from danswer.configs.constants import ID_SEPARATOR
@@ -21,6 +23,20 @@ from danswer.utils.text_processing import replace_whitespaces_w_space
logger = setup_logger() logger = setup_logger()
class ChannelIdAdapter(logging.LoggerAdapter):
"""This is used to add the channel ID to all log messages
emitted in this file"""
def process(
self, msg: str, kwargs: MutableMapping[str, Any]
) -> tuple[str, MutableMapping[str, Any]]:
channel_id = self.extra.get(SLACK_CHANNEL_ID) if self.extra else None
if channel_id:
return f"[Channel ID: {channel_id}] {msg}", kwargs
else:
return msg, kwargs
def get_web_client() -> WebClient: def get_web_client() -> WebClient:
slack_tokens = fetch_tokens() slack_tokens = fetch_tokens()
return WebClient(token=slack_tokens.bot_token) return WebClient(token=slack_tokens.bot_token)

View File

@@ -230,6 +230,7 @@ DANSWER_BOT_DISPLAY_ERROR_MSGS = os.environ.get(
DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER = os.environ.get( 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"
# 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 = (