Slack Followup Option (#948)

This commit is contained in:
Yuhong Sun
2024-01-15 14:26:20 -08:00
committed by GitHub
parent 22fb7c3352
commit 4cd9122ba5
14 changed files with 235 additions and 55 deletions

View File

@@ -0,0 +1,26 @@
"""Slack Followup
Revision ID: 7f726bad5367
Revises: 79acd316403a
Create Date: 2024-01-15 00:19:55.991224
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "7f726bad5367"
down_revision = "79acd316403a"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"chat_feedback",
sa.Column("required_followup", sa.Boolean(), nullable=True),
)
def downgrade() -> None:
op.drop_column("chat_feedback", "required_followup")

View File

@@ -16,7 +16,6 @@ from danswer.background.indexing.run_indexing import run_indexing_entrypoint
from danswer.configs.app_configs import CLEANUP_INDEXING_JOBS_TIMEOUT from danswer.configs.app_configs import CLEANUP_INDEXING_JOBS_TIMEOUT
from danswer.configs.app_configs import DASK_JOB_CLIENT_ENABLED from danswer.configs.app_configs import DASK_JOB_CLIENT_ENABLED
from danswer.configs.app_configs import LOG_LEVEL from danswer.configs.app_configs import LOG_LEVEL
from danswer.configs.app_configs import MODEL_SERVER_HOST
from danswer.configs.app_configs import NUM_INDEXING_WORKERS from danswer.configs.app_configs import NUM_INDEXING_WORKERS
from danswer.configs.model_configs import MIN_THREADS_ML_MODELS from danswer.configs.model_configs import MIN_THREADS_ML_MODELS
from danswer.db.connector import fetch_connectors from danswer.db.connector import fetch_connectors
@@ -33,7 +32,6 @@ from danswer.db.index_attempt import mark_attempt_failed
from danswer.db.models import Connector from danswer.db.models import Connector
from danswer.db.models import IndexAttempt from danswer.db.models import IndexAttempt
from danswer.db.models import IndexingStatus from danswer.db.models import IndexingStatus
from danswer.search.search_nlp_models import warm_up_models
from danswer.utils.logger import setup_logger from danswer.utils.logger import setup_logger
logger = setup_logger() logger = setup_logger()
@@ -343,9 +341,6 @@ def update__main() -> None:
if not DASK_JOB_CLIENT_ENABLED: if not DASK_JOB_CLIENT_ENABLED:
torch.multiprocessing.set_start_method("spawn") torch.multiprocessing.set_start_method("spawn")
if not MODEL_SERVER_HOST:
logger.info("Warming up Embedding Model(s)")
warm_up_models(indexer_only=True, skip_cross_encoders=True)
logger.info("Starting Indexing Loop") logger.info("Starting Indexing Loop")
update_loop() update_loop()

View File

@@ -17,6 +17,8 @@ DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER = os.environ.get(
).lower() not in ["false", ""] ).lower() not in ["false", ""]
# When Danswer is considering a message, what emoji does it react with # When Danswer is considering a message, what emoji does it react with
DANSWER_REACT_EMOJI = os.environ.get("DANSWER_REACT_EMOJI") or "eyes" DANSWER_REACT_EMOJI = os.environ.get("DANSWER_REACT_EMOJI") or "eyes"
# When User needs more help, what should the emoji be
DANSWER_FOLLOWUP_EMOJI = os.environ.get("DANSWER_FOLLOWUP_EMOJI") or "sos"
# Should DanswerBot send an apology message if it's not able to find an answer # 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 # 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) # Off by default to be less intrusive (don't want to give a notif that just says we couldnt help)

View File

@@ -91,6 +91,9 @@ def make_slack_api_rate_limited(
f"Slack call rate limited, retrying after {retry_after} seconds. Exception: {e}" f"Slack call rate limited, retrying after {retry_after} seconds. Exception: {e}"
) )
time.sleep(retry_after) time.sleep(retry_after)
if e.response["error"] in ["already_reacted", "no_reaction"]:
# The response isn't used for reactions, this is basically just a pass
return e.response
else: else:
# Raise the error for non-transient errors # Raise the error for non-transient errors
raise raise

View File

@@ -18,6 +18,8 @@ from danswer.configs.constants import SearchFeedbackType
from danswer.configs.danswerbot_configs import DANSWER_BOT_NUM_DOCS_TO_DISPLAY from danswer.configs.danswerbot_configs import DANSWER_BOT_NUM_DOCS_TO_DISPLAY
from danswer.danswerbot.slack.constants import DISLIKE_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import DISLIKE_BLOCK_ACTION_ID
from danswer.danswerbot.slack.constants import FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID
from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_ACTION_ID
from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_RESOLVED_ACTION_ID
from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID
from danswer.danswerbot.slack.utils import build_feedback_id from danswer.danswerbot.slack.utils import build_feedback_id
from danswer.danswerbot.slack.utils import remove_slack_text_interactions from danswer.danswerbot.slack.utils import remove_slack_text_interactions
@@ -211,6 +213,7 @@ def build_qa_response_blocks(
time_cutoff: datetime | None, time_cutoff: datetime | None,
favor_recent: bool, favor_recent: bool,
skip_quotes: bool = False, skip_quotes: bool = False,
skip_ai_feedback: bool = False,
) -> list[Block]: ) -> list[Block]:
if DISABLE_GENERATIVE_AI: if DISABLE_GENERATIVE_AI:
return [] return []
@@ -255,10 +258,6 @@ def build_qa_response_blocks(
) )
] ]
feedback_block = None
if message_id is not None:
feedback_block = build_qa_feedback_block(message_id=message_id)
response_blocks: list[Block] = [ai_answer_header] response_blocks: list[Block] = [ai_answer_header]
if filter_block is not None: if filter_block is not None:
@@ -266,11 +265,45 @@ def build_qa_response_blocks(
response_blocks.append(answer_block) response_blocks.append(answer_block)
if feedback_block is not None: if message_id is not None and not skip_ai_feedback:
response_blocks.append(feedback_block) response_blocks.append(build_qa_feedback_block(message_id=message_id))
if not skip_quotes: if not skip_quotes:
response_blocks.extend(quotes_blocks) response_blocks.extend(quotes_blocks)
response_blocks.append(DividerBlock()) response_blocks.append(DividerBlock())
return response_blocks return response_blocks
def build_follow_up_block(message_id: int | None) -> ActionsBlock:
return ActionsBlock(
block_id=build_feedback_id(message_id) if message_id is not None else None,
elements=[
ButtonElement(
action_id=FOLLOWUP_BUTTON_ACTION_ID,
style="danger",
text="I need more help from a human!",
)
],
)
def build_follow_up_resolved_blocks(tag_ids: list[str]) -> list[Block]:
tag_str = " ".join([f"<@{tag}>" for tag in tag_ids])
if tag_str:
tag_str += " "
text = (
tag_str
+ "Someone has requested more help.\n:point_down:Please mark this resolved after answering!"
)
text_block = SectionBlock(text=text)
button_block = ActionsBlock(
elements=[
ButtonElement(
action_id=FOLLOWUP_BUTTON_RESOLVED_ACTION_ID,
style="primary",
text="Mark Resolved",
)
]
)
return [text_block, button_block]

View File

@@ -1,5 +1,7 @@
LIKE_BLOCK_ACTION_ID = "feedback-like" LIKE_BLOCK_ACTION_ID = "feedback-like"
DISLIKE_BLOCK_ACTION_ID = "feedback-dislike" DISLIKE_BLOCK_ACTION_ID = "feedback-dislike"
FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID = "feedback-doc-button" FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID = "feedback-doc-button"
FOLLOWUP_BUTTON_ACTION_ID = "followup-button"
FOLLOWUP_BUTTON_RESOLVED_ACTION_ID = "followup-resolved-button"
SLACK_CHANNEL_ID = "channel_id" SLACK_CHANNEL_ID = "channel_id"
VIEW_DOC_FEEDBACK_ID = "view-doc-feedback" VIEW_DOC_FEEDBACK_ID = "view-doc-feedback"

View File

@@ -1,3 +1,6 @@
from typing import Any
from typing import cast
from slack_sdk import WebClient from slack_sdk import WebClient
from slack_sdk.models.views import View from slack_sdk.models.views import View
from slack_sdk.socket_mode import SocketModeClient from slack_sdk.socket_mode import SocketModeClient
@@ -5,12 +8,19 @@ from slack_sdk.socket_mode.request import SocketModeRequest
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from danswer.configs.constants import SearchFeedbackType from danswer.configs.constants import SearchFeedbackType
from danswer.configs.danswerbot_configs import DANSWER_FOLLOWUP_EMOJI
from danswer.danswerbot.slack.blocks import build_follow_up_resolved_blocks
from danswer.danswerbot.slack.blocks import get_document_feedback_blocks from danswer.danswerbot.slack.blocks import get_document_feedback_blocks
from danswer.danswerbot.slack.config import get_slack_bot_config_for_channel
from danswer.danswerbot.slack.constants import DISLIKE_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import DISLIKE_BLOCK_ACTION_ID
from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID
from danswer.danswerbot.slack.constants import VIEW_DOC_FEEDBACK_ID from danswer.danswerbot.slack.constants import VIEW_DOC_FEEDBACK_ID
from danswer.danswerbot.slack.utils import build_feedback_id from danswer.danswerbot.slack.utils import build_feedback_id
from danswer.danswerbot.slack.utils import decompose_feedback_id from danswer.danswerbot.slack.utils import decompose_action_id
from danswer.danswerbot.slack.utils import fetch_userids_from_emails
from danswer.danswerbot.slack.utils import get_channel_name_from_id
from danswer.danswerbot.slack.utils import respond_in_thread
from danswer.danswerbot.slack.utils import update_emote_react
from danswer.db.engine import get_sqlalchemy_engine from danswer.db.engine import get_sqlalchemy_engine
from danswer.db.feedback import create_chat_message_feedback from danswer.db.feedback import create_chat_message_feedback
from danswer.db.feedback import create_doc_retrieval_feedback from danswer.db.feedback import create_doc_retrieval_feedback
@@ -30,7 +40,7 @@ def handle_doc_feedback_button(
# Extracts the feedback_id coming from the 'source feedback' button # Extracts the feedback_id coming from the 'source feedback' button
# and generates a new one for the View, to keep track of the doc info # and generates a new one for the View, to keep track of the doc info
query_event_id, doc_id, doc_rank = decompose_feedback_id(actions[0].get("value")) query_event_id, doc_id, doc_rank = decompose_action_id(actions[0].get("value"))
external_id = build_feedback_id(query_event_id, doc_id, doc_rank) external_id = build_feedback_id(query_event_id, doc_id, doc_rank)
channel_id = req.payload["container"]["channel_id"] channel_id = req.payload["container"]["channel_id"]
@@ -63,7 +73,7 @@ def handle_slack_feedback(
) -> None: ) -> None:
engine = get_sqlalchemy_engine() engine = get_sqlalchemy_engine()
message_id, doc_id, doc_rank = decompose_feedback_id(feedback_id) message_id, doc_id, doc_rank = decompose_action_id(feedback_id)
with Session(engine) as db_session: with Session(engine) as db_session:
if feedback_type in [LIKE_BLOCK_ACTION_ID, DISLIKE_BLOCK_ACTION_ID]: if feedback_type in [LIKE_BLOCK_ACTION_ID, DISLIKE_BLOCK_ACTION_ID]:
@@ -108,3 +118,76 @@ def handle_slack_feedback(
thread_ts=thread_ts_to_post_confirmation, thread_ts=thread_ts_to_post_confirmation,
text="Thanks for your feedback!", text="Thanks for your feedback!",
) )
def handle_followup_button(
req: SocketModeRequest,
client: SocketModeClient,
) -> None:
action_id = None
if actions := req.payload.get("actions"):
action = cast(dict[str, Any], actions[0])
action_id = cast(str, action.get("block_id"))
channel_id = req.payload["container"]["channel_id"]
thread_ts = req.payload["container"]["thread_ts"]
update_emote_react(
emoji=DANSWER_FOLLOWUP_EMOJI,
channel=channel_id,
message_ts=thread_ts,
remove=False,
client=client.web_client,
)
tag_ids = []
with Session(get_sqlalchemy_engine()) as db_session:
channel_name, is_dm = get_channel_name_from_id(
client=client.web_client, channel_id=channel_id
)
slack_bot_config = get_slack_bot_config_for_channel(
channel_name=channel_name, db_session=db_session
)
if slack_bot_config:
emails = slack_bot_config.channel_config.get("follow_up_tags")
if emails:
tag_ids = fetch_userids_from_emails(emails, client.web_client)
blocks = build_follow_up_resolved_blocks(tag_ids=tag_ids)
respond_in_thread(
client=client.web_client,
channel=channel_id,
text="Received your request for more help",
blocks=blocks,
thread_ts=thread_ts,
unfurl=False,
)
if action_id is not None:
message_id, _, _ = decompose_action_id(action_id)
create_chat_message_feedback(
is_positive=None,
feedback_text="",
chat_message_id=message_id,
user_id=None, # no "user" for Slack bot for now
db_session=db_session,
required_followup=True,
)
def handle_followup_resolved_button(
req: SocketModeRequest,
client: SocketModeClient,
) -> None:
channel_id = req.payload["container"]["channel_id"]
thread_ts = req.payload["container"]["thread_ts"]
update_emote_react(
emoji=DANSWER_FOLLOWUP_EMOJI,
channel=channel_id,
message_ts=thread_ts,
remove=True,
client=client.web_client,
)

View File

@@ -14,8 +14,8 @@ from danswer.configs.danswerbot_configs import DANSWER_BOT_NUM_RETRIES
from danswer.configs.danswerbot_configs import DANSWER_REACT_EMOJI from danswer.configs.danswerbot_configs import DANSWER_REACT_EMOJI
from danswer.configs.danswerbot_configs import DISABLE_DANSWER_BOT_FILTER_DETECT from danswer.configs.danswerbot_configs import DISABLE_DANSWER_BOT_FILTER_DETECT
from danswer.configs.danswerbot_configs import ENABLE_DANSWERBOT_REFLEXION from danswer.configs.danswerbot_configs import ENABLE_DANSWERBOT_REFLEXION
from danswer.connectors.slack.utils import make_slack_api_rate_limited
from danswer.danswerbot.slack.blocks import build_documents_blocks from danswer.danswerbot.slack.blocks import build_documents_blocks
from danswer.danswerbot.slack.blocks import build_follow_up_block
from danswer.danswerbot.slack.blocks import build_qa_response_blocks from danswer.danswerbot.slack.blocks import build_qa_response_blocks
from danswer.danswerbot.slack.blocks import get_restate_blocks from danswer.danswerbot.slack.blocks import get_restate_blocks
from danswer.danswerbot.slack.constants import SLACK_CHANNEL_ID from danswer.danswerbot.slack.constants import SLACK_CHANNEL_ID
@@ -23,6 +23,7 @@ from danswer.danswerbot.slack.models import SlackMessageInfo
from danswer.danswerbot.slack.utils import ChannelIdAdapter from danswer.danswerbot.slack.utils import ChannelIdAdapter
from danswer.danswerbot.slack.utils import fetch_userids_from_emails from danswer.danswerbot.slack.utils import fetch_userids_from_emails
from danswer.danswerbot.slack.utils import respond_in_thread from danswer.danswerbot.slack.utils import respond_in_thread
from danswer.danswerbot.slack.utils import update_emote_react
from danswer.db.engine import get_sqlalchemy_engine from danswer.db.engine import get_sqlalchemy_engine
from danswer.db.models import SlackBotConfig from danswer.db.models import SlackBotConfig
from danswer.one_shot_answer.answer_question import get_search_answer from danswer.one_shot_answer.answer_question import get_search_answer
@@ -49,23 +50,12 @@ def send_msg_ack_to_user(details: SlackMessageInfo, client: WebClient) -> None:
) )
return return
slack_call = make_slack_api_rate_limited(client.reactions_add) update_emote_react(
slack_call( emoji=DANSWER_REACT_EMOJI,
name=DANSWER_REACT_EMOJI,
channel=details.channel_to_respond, channel=details.channel_to_respond,
timestamp=details.msg_to_respond, message_ts=details.msg_to_respond,
) remove=False,
client=client,
def remove_react(details: SlackMessageInfo, client: WebClient) -> None:
if details.is_bot_msg:
return
slack_call = make_slack_api_rate_limited(client.reactions_remove)
slack_call(
name=DANSWER_REACT_EMOJI,
channel=details.channel_to_respond,
timestamp=details.msg_to_respond,
) )
@@ -130,6 +120,7 @@ def handle_message(
# with non-public document sets # with non-public document sets
bypass_acl = True bypass_acl = True
channel_conf = None
if channel_config and channel_config.channel_config: if channel_config and channel_config.channel_config:
channel_conf = channel_config.channel_config channel_conf = channel_config.channel_config
if not bypass_filters and "answer_filters" in channel_conf: if not bypass_filters and "answer_filters" in channel_conf:
@@ -265,7 +256,13 @@ def handle_message(
# In case of failures, don't keep the reaction there permanently # In case of failures, don't keep the reaction there permanently
try: try:
remove_react(message_info, client) update_emote_react(
emoji=DANSWER_REACT_EMOJI,
channel=message_info.channel_to_respond,
message_ts=message_info.msg_to_respond,
remove=True,
client=client,
)
except SlackApiError as e: except SlackApiError as e:
logger.error(f"Failed to remove Reaction due to: {e}") logger.error(f"Failed to remove Reaction due to: {e}")
@@ -273,7 +270,13 @@ def handle_message(
# Got an answer at this point, can remove reaction and give results # Got an answer at this point, can remove reaction and give results
try: try:
remove_react(message_info, client) update_emote_react(
emoji=DANSWER_REACT_EMOJI,
channel=message_info.channel_to_respond,
message_ts=message_info.msg_to_respond,
remove=True,
client=client,
)
except SlackApiError as e: except SlackApiError as e:
logger.error(f"Failed to remove Reaction due to: {e}") logger.error(f"Failed to remove Reaction due to: {e}")
@@ -343,13 +346,18 @@ def handle_message(
else [] else []
) )
all_blocks = restate_question_block + answer_blocks + document_blocks
if channel_conf and channel_conf.get("follow_up_tags") is not None:
all_blocks.append(build_follow_up_block(message_id=answer.chat_message_id))
try: try:
respond_in_thread( respond_in_thread(
client=client, client=client,
channel=channel, channel=channel,
receiver_ids=send_to, receiver_ids=send_to,
text="Hello! Danswer has some results for you!", text="Hello! Danswer has some results for you!",
blocks=restate_question_block + answer_blocks + document_blocks, blocks=all_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,

View File

@@ -16,19 +16,24 @@ from danswer.configs.model_configs import ENABLE_RERANKING_ASYNC_FLOW
from danswer.danswerbot.slack.config import get_slack_bot_config_for_channel from danswer.danswerbot.slack.config import get_slack_bot_config_for_channel
from danswer.danswerbot.slack.constants import DISLIKE_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import DISLIKE_BLOCK_ACTION_ID
from danswer.danswerbot.slack.constants import FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID
from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_ACTION_ID
from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_RESOLVED_ACTION_ID
from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID
from danswer.danswerbot.slack.constants import SLACK_CHANNEL_ID from danswer.danswerbot.slack.constants import SLACK_CHANNEL_ID
from danswer.danswerbot.slack.constants import VIEW_DOC_FEEDBACK_ID from danswer.danswerbot.slack.constants import VIEW_DOC_FEEDBACK_ID
from danswer.danswerbot.slack.handlers.handle_feedback import handle_doc_feedback_button from danswer.danswerbot.slack.handlers.handle_buttons import handle_doc_feedback_button
from danswer.danswerbot.slack.handlers.handle_feedback import handle_slack_feedback from danswer.danswerbot.slack.handlers.handle_buttons import handle_followup_button
from danswer.danswerbot.slack.handlers.handle_buttons import (
handle_followup_resolved_button,
)
from danswer.danswerbot.slack.handlers.handle_buttons import handle_slack_feedback
from danswer.danswerbot.slack.handlers.handle_message import handle_message from danswer.danswerbot.slack.handlers.handle_message import handle_message
from danswer.danswerbot.slack.models import SlackMessageInfo from danswer.danswerbot.slack.models import SlackMessageInfo
from danswer.danswerbot.slack.tokens import fetch_tokens from danswer.danswerbot.slack.tokens import fetch_tokens
from danswer.danswerbot.slack.utils import ChannelIdAdapter from danswer.danswerbot.slack.utils import ChannelIdAdapter
from danswer.danswerbot.slack.utils import decompose_feedback_id from danswer.danswerbot.slack.utils import decompose_action_id
from danswer.danswerbot.slack.utils import get_channel_name_from_id from danswer.danswerbot.slack.utils import get_channel_name_from_id
from danswer.danswerbot.slack.utils import get_danswer_bot_app_id from danswer.danswerbot.slack.utils import get_danswer_bot_app_id
from danswer.danswerbot.slack.utils import get_view_values
from danswer.danswerbot.slack.utils import read_slack_thread from danswer.danswerbot.slack.utils import read_slack_thread
from danswer.danswerbot.slack.utils import remove_danswer_bot_tag from danswer.danswerbot.slack.utils import remove_danswer_bot_tag
from danswer.danswerbot.slack.utils import respond_in_thread from danswer.danswerbot.slack.utils import respond_in_thread
@@ -136,27 +141,14 @@ def prefilter_requests(req: SocketModeRequest, client: SocketModeClient) -> bool
def process_feedback(req: SocketModeRequest, client: SocketModeClient) -> None: def process_feedback(req: SocketModeRequest, client: SocketModeClient) -> None:
# Answer feedback
if actions := req.payload.get("actions"): if actions := req.payload.get("actions"):
action = cast(dict[str, Any], actions[0]) action = cast(dict[str, Any], actions[0])
feedback_type = cast(str, action.get("action_id")) feedback_type = cast(str, action.get("action_id"))
feedback_id = cast(str, action.get("block_id")) feedback_id = cast(str, action.get("block_id"))
channel_id = cast(str, req.payload["container"]["channel_id"]) channel_id = cast(str, req.payload["container"]["channel_id"])
thread_ts = cast(str, req.payload["container"]["thread_ts"]) thread_ts = cast(str, req.payload["container"]["thread_ts"])
# Doc feedback
elif view := req.payload.get("view"):
view_values = get_view_values(view["state"]["values"])
private_metadata = view.get("private_metadata").split("_")
if not view_values:
logger.error("Unable to process feedback. Missing view values")
return
feedback_type = [x for x in view_values.values()][0]
feedback_id = cast(str, view.get("external_id"))
channel_id = private_metadata[0]
thread_ts = private_metadata[1]
else: else:
logger.error("Unable to process feedback. Actions or View not found") logger.error("Unable to process feedback. Action not found")
return return
user_id = cast(str, req.payload["user"]["id"]) user_id = cast(str, req.payload["user"]["id"])
@@ -170,7 +162,7 @@ def process_feedback(req: SocketModeRequest, client: SocketModeClient) -> None:
thread_ts_to_post_confirmation=thread_ts, thread_ts_to_post_confirmation=thread_ts,
) )
query_event_id, _, _ = decompose_feedback_id(feedback_id) query_event_id, _, _ = decompose_action_id(feedback_id)
logger.info(f"Successfully handled QA feedback for event: {query_event_id}") logger.info(f"Successfully handled QA feedback for event: {query_event_id}")
@@ -304,6 +296,10 @@ def action_routing(req: SocketModeRequest, client: SocketModeClient) -> None:
elif action["action_id"] == FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID: elif action["action_id"] == FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID:
# Activation of the "source feedback" button # Activation of the "source feedback" button
return handle_doc_feedback_button(req, client) return handle_doc_feedback_button(req, client)
elif action["action_id"] == FOLLOWUP_BUTTON_ACTION_ID:
return handle_followup_button(req, client)
elif action["action_id"] == FOLLOWUP_BUTTON_RESOLVED_ACTION_ID:
return handle_followup_resolved_button(req, client)
def view_routing(req: SocketModeRequest, client: SocketModeClient) -> None: def view_routing(req: SocketModeRequest, client: SocketModeClient) -> None:

View File

@@ -29,6 +29,26 @@ logger = setup_logger()
DANSWER_BOT_APP_ID: str | None = None DANSWER_BOT_APP_ID: str | None = None
def update_emote_react(
emoji: str,
channel: str,
message_ts: str | None,
remove: bool,
client: WebClient,
) -> None:
if not message_ts:
logger.error(f"Tried to remove a react in {channel} but no message specified")
return
func = client.reactions_remove if remove else client.reactions_add
slack_call = make_slack_api_rate_limited(func) # type: ignore
slack_call(
name=emoji,
channel=channel,
timestamp=message_ts,
)
def get_danswer_bot_app_id(web_client: WebClient) -> Any: def get_danswer_bot_app_id(web_client: WebClient) -> Any:
global DANSWER_BOT_APP_ID global DANSWER_BOT_APP_ID
if DANSWER_BOT_APP_ID is None: if DANSWER_BOT_APP_ID is None:
@@ -134,7 +154,7 @@ def build_feedback_id(
return unique_prefix + ID_SEPARATOR + feedback_id return unique_prefix + ID_SEPARATOR + feedback_id
def decompose_feedback_id(feedback_id: str) -> tuple[int, str | None, int | None]: def decompose_action_id(feedback_id: str) -> tuple[int, str | None, int | None]:
"""Decompose into query_id, document_id, document_rank, see above function""" """Decompose into query_id, document_id, document_rank, see above function"""
try: try:
components = feedback_id.split(ID_SEPARATOR) components = feedback_id.split(ID_SEPARATOR)

View File

@@ -146,8 +146,10 @@ def create_chat_message_feedback(
chat_message_id: int, chat_message_id: int,
user_id: UUID | None, user_id: UUID | None,
db_session: Session, db_session: Session,
# Slack user requested help from human
required_followup: bool | None = None,
) -> None: ) -> None:
if is_positive is None and feedback_text is None: if is_positive is None and feedback_text is None and required_followup is None:
raise ValueError("No feedback provided") raise ValueError("No feedback provided")
chat_message = get_chat_message( chat_message = get_chat_message(
@@ -161,6 +163,7 @@ def create_chat_message_feedback(
chat_message_id=chat_message_id, chat_message_id=chat_message_id,
is_positive=is_positive, is_positive=is_positive,
feedback_text=feedback_text, feedback_text=feedback_text,
required_followup=required_followup,
) )
db_session.add(message_feedback) db_session.add(message_feedback)

View File

@@ -602,6 +602,7 @@ class ChatMessageFeedback(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
chat_message_id: Mapped[int] = mapped_column(ForeignKey("chat_message.id")) chat_message_id: Mapped[int] = mapped_column(ForeignKey("chat_message.id"))
is_positive: Mapped[bool | None] = mapped_column(Boolean, nullable=True) is_positive: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
required_followup: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
feedback_text: Mapped[str | None] = mapped_column(Text, nullable=True) feedback_text: Mapped[str | None] = mapped_column(Text, nullable=True)
chat_message: Mapped[ChatMessage] = relationship( chat_message: Mapped[ChatMessage] = relationship(
@@ -740,6 +741,9 @@ class ChannelConfig(TypedDict):
respond_tag_only: NotRequired[bool] # defaults to False respond_tag_only: NotRequired[bool] # defaults to False
respond_team_member_list: NotRequired[list[str]] respond_team_member_list: NotRequired[list[str]]
answer_filters: NotRequired[list[AllowedAnswerFilters]] answer_filters: NotRequired[list[AllowedAnswerFilters]]
# If None then no follow up
# If empty list, follow up with no tags
follow_up_tags: NotRequired[list[str]]
class SlackBotConfig(Base): class SlackBotConfig(Base):

View File

@@ -78,6 +78,8 @@ class SlackBotConfigCreationRequest(BaseModel):
# If no team members, assume respond in the channel to everyone # If no team members, assume respond in the channel to everyone
respond_team_member_list: list[str] = [] respond_team_member_list: list[str] = []
answer_filters: list[AllowedAnswerFilters] = [] answer_filters: list[AllowedAnswerFilters] = []
# list of user emails
follow_up_tags: list[str] | None = None
@validator("answer_filters", pre=True) @validator("answer_filters", pre=True)
def validate_filters(cls, value: list[str]) -> list[str]: def validate_filters(cls, value: list[str]) -> list[str]:

View File

@@ -39,6 +39,7 @@ def _form_channel_config(
slack_bot_config_creation_request.respond_team_member_list slack_bot_config_creation_request.respond_team_member_list
) )
answer_filters = slack_bot_config_creation_request.answer_filters answer_filters = slack_bot_config_creation_request.answer_filters
follow_up_tags = slack_bot_config_creation_request.follow_up_tags
if not raw_channel_names: if not raw_channel_names:
raise HTTPException( raise HTTPException(
@@ -73,6 +74,8 @@ def _form_channel_config(
channel_config["respond_team_member_list"] = respond_team_member_list channel_config["respond_team_member_list"] = respond_team_member_list
if answer_filters: if answer_filters:
channel_config["answer_filters"] = answer_filters channel_config["answer_filters"] = answer_filters
if follow_up_tags is not None:
channel_config["follow_up_tags"] = follow_up_tags
return channel_config return channel_config