mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-20 21:33:56 +02:00
Slack Followup Option (#948)
This commit is contained in:
26
backend/alembic/versions/7f726bad5367_slack_followup.py
Normal file
26
backend/alembic/versions/7f726bad5367_slack_followup.py
Normal 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")
|
@@ -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()
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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]
|
||||||
|
@@ -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"
|
||||||
|
@@ -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,
|
||||||
|
)
|
@@ -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,
|
||||||
|
@@ -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:
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
|
@@ -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):
|
||||||
|
@@ -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]:
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user