Add ability to respond with error message in slack thread

This commit is contained in:
Weves 2023-08-26 13:57:34 -07:00 committed by Chris Weaver
parent a2ec1e2cda
commit 20b6369eea
2 changed files with 88 additions and 54 deletions

View File

@ -190,3 +190,9 @@ DANSWER_BOT_NUM_RETRIES = int(os.environ.get("DANSWER_BOT_NUM_RETRIES", "5"))
DANSWER_BOT_ANSWER_GENERATION_TIMEOUT = int( DANSWER_BOT_ANSWER_GENERATION_TIMEOUT = int(
os.environ.get("DANSWER_BOT_ANSWER_GENERATION_TIMEOUT", "60") os.environ.get("DANSWER_BOT_ANSWER_GENERATION_TIMEOUT", "60")
) )
DANSWER_BOT_DISPLAY_ERROR_MSGS = os.environ.get(
"DANSWER_BOT_DISPLAY_ERROR_MSGS", ""
).lower() not in [
"false",
"",
]

View File

@ -1,7 +1,9 @@
import logging import logging
import os import os
from collections.abc import Callable from collections.abc import Callable
from collections.abc import MutableMapping
from functools import wraps from functools import wraps
from typing import Any
from typing import cast from typing import cast
from retry import retry from retry import retry
@ -11,6 +13,7 @@ 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 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_DISPLAY_ERROR_MSGS
from danswer.configs.app_configs import DANSWER_BOT_NUM_DOCS_TO_DISPLAY from danswer.configs.app_configs import DANSWER_BOT_NUM_DOCS_TO_DISPLAY
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 DOCUMENT_INDEX_NAME from danswer.configs.app_configs import DOCUMENT_INDEX_NAME
@ -27,14 +30,21 @@ from danswer.utils.logger import setup_logger
logger = setup_logger() logger = setup_logger()
def _wrap_logger_fn_to_include_channel( _CHANNEL_ID = "channel_id"
log_fn: Callable[[str], None], channel: str
) -> Callable[[str], None]:
@wraps(log_fn)
def wrapped_fn(msg: str) -> None:
log_fn(f"[{channel}] {msg}")
return wrapped_fn
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:
@ -133,54 +143,81 @@ def _process_documents(
return "\n".join(top_document_lines) return "\n".join(top_document_lines)
@retry(
tries=DANSWER_BOT_NUM_RETRIES,
delay=0.25,
backoff=2,
logger=cast(logging.Logger, logger),
)
def _respond_in_thread(
client: SocketModeClient,
channel: str,
text: str,
thread_ts: str,
) -> None:
logger.info(f"Trying to send message: {text}")
slack_call = make_slack_api_rate_limited(client.web_client.chat_postMessage)
response = slack_call(
channel=channel,
text=text,
thread_ts=thread_ts,
)
if not response.get("ok"):
raise RuntimeError(f"Unable to post message: {response}")
def process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None: def process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None:
if req.type == "events_api": if req.type == "events_api":
# Acknowledge the request anyway # Acknowledge the request anyway
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)
channel = cast(str | None, req.payload.get("event", {}).get("channel")) 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 # Ensure that the message is a new message + of expected type
event_type = req.payload.get("event", {}).get("type") event_type = event.get("type")
if event_type != "message": if event_type != "message":
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}'"
) )
# 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:
logger.error(f"Found message without channel - skipping") channel_specific_logger.error(f"Found message without channel - skipping")
return return
# utils which will preprend the channel to the log message message_subtype = event.get("subtype")
log_info = _wrap_logger_fn_to_include_channel(logger.info, channel)
log_error = _wrap_logger_fn_to_include_channel(logger.error, channel)
log_exception = _wrap_logger_fn_to_include_channel(logger.exception, channel)
message_subtype = req.payload.get("event", {}).get("subtype")
# 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
# should not ignore it # should not ignore it
if message_subtype not in [None, "file_share"]: if message_subtype not in [None, "file_share"]:
log_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
if req.payload.get("event", {}).get("bot_profile"): if event.get("bot_profile"):
log_info("Ignoring message from bot") channel_specific_logger.info("Ignoring message from bot")
return return
message_ts = req.payload.get("event", {}).get("ts") message_ts = event.get("ts")
thread_ts = req.payload.get("event", {}).get("thread_ts") thread_ts = event.get("thread_ts")
# 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:
log_info("Skipping message since it is not the root of a thread") channel_specific_logger.info(
"Skipping message since it is not the root of a thread"
)
return return
msg = req.payload.get("event", {}).get("text") msg = cast(str | None, event.get("text"))
if not msg: if not msg:
log_error("Unable to process empty message") channel_specific_logger.error("Unable to process empty message")
return return
# TODO: message should be enqueued and processed elsewhere, # TODO: message should be enqueued and processed elsewhere,
@ -207,17 +244,27 @@ def process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> Non
try: try:
answer = _get_answer( answer = _get_answer(
QuestionRequest( QuestionRequest(
query=req.payload.get("event", {}).get("text"), query=msg,
collection=DOCUMENT_INDEX_NAME, collection=DOCUMENT_INDEX_NAME,
use_keyword=None, use_keyword=None,
filters=None, filters=None,
offset=None, offset=None,
) )
) )
except Exception: except Exception as e:
log_exception( channel_specific_logger.exception(
f"Unable to process message - did not successfully answer in {DANSWER_BOT_NUM_RETRIES} attempts" f"Unable to process message - did not successfully answer "
f"in {DANSWER_BOT_NUM_RETRIES} attempts"
) )
# Optionally, respond in thread with the error message, Used primarily
# for debugging purposes
if DANSWER_BOT_DISPLAY_ERROR_MSGS:
_respond_in_thread(
client=client,
channel=channel,
text=f"Encountered exception when trying to answer: \n\n```{e}```",
thread_ts=message_ts_to_respond_to,
)
return return
# convert raw response into "nicely" formatted Slack message # convert raw response into "nicely" formatted Slack message
@ -235,41 +282,22 @@ def process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> Non
else: else:
text = f"{answer.answer}\n\n*Warning*: no sources were quoted for this answer, so it may be unreliable 😔\n\n{top_documents_str_with_header}" text = f"{answer.answer}\n\n*Warning*: no sources were quoted for this answer, so it may be unreliable 😔\n\n{top_documents_str_with_header}"
@retry(
tries=DANSWER_BOT_NUM_RETRIES,
delay=0.25,
backoff=2,
logger=cast(logging.Logger, logger),
)
def _respond_in_thread(
channel: str,
text: str,
thread_ts: str,
) -> None:
logger.info(f"Trying to send message: {text}")
slack_call = make_slack_api_rate_limited(client.web_client.chat_postMessage)
response = slack_call(
channel=channel,
text=text,
thread_ts=thread_ts,
)
if not response.get("ok"):
raise RuntimeError(f"Unable to post message: {response}")
try: try:
_respond_in_thread( _respond_in_thread(
client=client,
channel=channel, channel=channel,
text=text, text=text,
thread_ts=thread_ts thread_ts=message_ts_to_respond_to,
or message_ts, # pick the root of the thread (if a thread exists)
) )
except Exception: except Exception:
log_exception( channel_specific_logger.exception(
f"Unable to process message - could not respond in slack in {DANSWER_BOT_NUM_RETRIES} attempts" f"Unable to process message - could not respond in slack in {DANSWER_BOT_NUM_RETRIES} attempts"
) )
return return
log_info(f"Successfully processed message with ts: '{message_ts}'") channel_specific_logger.info(
f"Successfully processed message with ts: '{message_ts}'"
)
# Follow the guide (https://docs.danswer.dev/slack_bot_setup) to set up # Follow the guide (https://docs.danswer.dev/slack_bot_setup) to set up