mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-26 17:51:54 +01:00
Support more Slack Config Options (#494)
--------- Co-authored-by: Weves <chrisweaver101@gmail.com>
This commit is contained in:
parent
c2721c7889
commit
59bac1ca8f
@ -71,6 +71,20 @@ def build_doc_feedback_block(
|
||||
)
|
||||
|
||||
|
||||
def get_restate_blocks(
|
||||
msg: str,
|
||||
is_bot_msg: bool,
|
||||
) -> list[Block]:
|
||||
# Only the slash command needs this context because the user doesnt see their own input
|
||||
if not is_bot_msg:
|
||||
return []
|
||||
|
||||
return [
|
||||
HeaderBlock(text="Responding to the Query"),
|
||||
SectionBlock(text=f"```{msg}```"),
|
||||
]
|
||||
|
||||
|
||||
def build_documents_blocks(
|
||||
documents: list[SearchDoc],
|
||||
query_event_id: int,
|
||||
|
@ -4,6 +4,13 @@ from danswer.db.models import SlackBotConfig
|
||||
from danswer.db.slack_bot_config import fetch_slack_bot_configs
|
||||
|
||||
|
||||
VALID_SLACK_FILTERS = [
|
||||
"answerable_prefilter",
|
||||
"well_answered_postfilter",
|
||||
"questionmark_prefilter",
|
||||
]
|
||||
|
||||
|
||||
def get_slack_bot_config_for_channel(
|
||||
channel_name: str, db_session: Session
|
||||
) -> SlackBotConfig | None:
|
||||
|
@ -6,7 +6,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
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 get_restate_blocks
|
||||
from danswer.bots.slack.config import get_slack_bot_config_for_channel
|
||||
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.configs.app_configs import DANSWER_BOT_ANSWER_GENERATION_TIMEOUT
|
||||
@ -25,9 +27,12 @@ from danswer.server.models import QuestionRequest
|
||||
def handle_message(
|
||||
msg: str,
|
||||
channel: str,
|
||||
message_ts_to_respond_to: str,
|
||||
message_ts_to_respond_to: str | None,
|
||||
sender_id: str | None,
|
||||
client: WebClient,
|
||||
logger: logging.Logger,
|
||||
skip_filters: bool = False,
|
||||
is_bot_msg: bool = False,
|
||||
num_retries: int = DANSWER_BOT_NUM_RETRIES,
|
||||
answer_generation_timeout: int = DANSWER_BOT_ANSWER_GENERATION_TIMEOUT,
|
||||
should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS,
|
||||
@ -40,20 +45,60 @@ def handle_message(
|
||||
channel_name=channel_name, db_session=db_session
|
||||
)
|
||||
document_set_names: list[str] | None = None
|
||||
validity_check_enabled = ENABLE_DANSWERBOT_REFLEXION
|
||||
if slack_bot_config and slack_bot_config.persona:
|
||||
document_set_names = [
|
||||
document_set.name
|
||||
for document_set in slack_bot_config.persona.document_sets
|
||||
]
|
||||
validity_check_enabled = slack_bot_config.channel_config.get(
|
||||
"answer_validity_check_enabled", validity_check_enabled
|
||||
)
|
||||
|
||||
reflexion = ENABLE_DANSWERBOT_REFLEXION
|
||||
|
||||
# List of user id to send message to, if None, send to everyone in channel
|
||||
send_to: list[str] | None = None
|
||||
respond_sender_only = False
|
||||
respond_team_member_list = None
|
||||
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 (
|
||||
"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(
|
||||
"Found slack bot config for channel. Restricting bot to use document "
|
||||
f"sets: {document_set_names}, validity check enabled: {validity_check_enabled}"
|
||||
f"sets: {document_set_names}, "
|
||||
f"validity checks enabled: {channel_conf['answer_filters']}"
|
||||
)
|
||||
|
||||
respond_sender_only = channel_conf.get("respond_sender_only") or False
|
||||
respond_team_member_list = (
|
||||
channel_conf.get("respond_team_member_list") or None
|
||||
)
|
||||
|
||||
if sender_id and (respond_sender_only or is_bot_msg):
|
||||
send_to = [sender_id]
|
||||
elif 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(
|
||||
tries=num_retries,
|
||||
delay=0.25,
|
||||
@ -70,7 +115,7 @@ def handle_message(
|
||||
db_session=db_session,
|
||||
answer_generation_timeout=answer_generation_timeout,
|
||||
real_time_flow=False,
|
||||
enable_reflexion=validity_check_enabled,
|
||||
enable_reflexion=reflexion,
|
||||
)
|
||||
if not answer.error_msg:
|
||||
return answer
|
||||
@ -100,6 +145,7 @@ def handle_message(
|
||||
respond_in_thread(
|
||||
client=client,
|
||||
channel=channel,
|
||||
receiver_ids=None,
|
||||
text=f"Encountered exception when trying to answer: \n\n```{e}```",
|
||||
thread_ts=message_ts_to_respond_to,
|
||||
)
|
||||
@ -121,6 +167,7 @@ def handle_message(
|
||||
respond_in_thread(
|
||||
client=client,
|
||||
channel=channel,
|
||||
receiver_ids=None,
|
||||
text="Found no documents when trying to answer. Did you index any documents?",
|
||||
thread_ts=message_ts_to_respond_to,
|
||||
)
|
||||
@ -134,6 +181,10 @@ def handle_message(
|
||||
return
|
||||
|
||||
# convert raw response into "nicely" formatted Slack message
|
||||
|
||||
# If called with the DanswerBot slash command, the question is lost so we have to reshow it
|
||||
restate_question_block = get_restate_blocks(msg, is_bot_msg)
|
||||
|
||||
answer_blocks = build_qa_response_blocks(
|
||||
query_event_id=answer.query_event_id,
|
||||
answer=answer.answer,
|
||||
@ -148,12 +199,33 @@ def handle_message(
|
||||
respond_in_thread(
|
||||
client=client,
|
||||
channel=channel,
|
||||
blocks=answer_blocks + document_blocks,
|
||||
receiver_ids=send_to,
|
||||
blocks=restate_question_block + answer_blocks + document_blocks,
|
||||
thread_ts=message_ts_to_respond_to,
|
||||
# don't unfurl, since otherwise we will have 5+ previews which makes the message very long
|
||||
unfurl=False,
|
||||
)
|
||||
|
||||
# For DM (ephemeral message), we need to create a thread via a normal message so the user can see
|
||||
# the ephemeral message. This also will give the user a notification which ephemeral message does not.
|
||||
if respond_sender_only:
|
||||
respond_in_thread(
|
||||
client=client,
|
||||
channel=channel,
|
||||
text="We've just DM-ed you the answer, hope you find it useful! 💃",
|
||||
thread_ts=message_ts_to_respond_to,
|
||||
)
|
||||
elif respond_team_member_list:
|
||||
respond_in_thread(
|
||||
client=client,
|
||||
channel=channel,
|
||||
text=(
|
||||
"👋 Hi, we've just gathered and forwarded the relevant "
|
||||
+ "information to the team. They'll get back to you shortly!"
|
||||
),
|
||||
thread_ts=message_ts_to_respond_to,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
logger.exception(
|
||||
f"Unable to process message - could not respond in slack in {num_retries} attempts"
|
||||
|
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import MutableMapping
|
||||
from typing import Any
|
||||
from typing import cast
|
||||
@ -51,37 +52,39 @@ def _get_socket_client() -> SocketModeClient:
|
||||
|
||||
def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None:
|
||||
logger.info(f"Received Slack request of type: '{req.type}'")
|
||||
|
||||
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
|
||||
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
|
||||
event_type = event.get("type")
|
||||
if event_type != "message":
|
||||
channel_specific_logger.info(
|
||||
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
|
||||
# we can't send a response without it
|
||||
if not channel:
|
||||
channel_specific_logger.error("Found message without channel - skipping")
|
||||
return
|
||||
|
||||
message_subtype = event.get("subtype")
|
||||
# ignore things like channel_join, channel_leave, etc.
|
||||
# NOTE: "file_share" is just a message with a file attachment, so we
|
||||
# should not ignore it
|
||||
if message_subtype not in [None, "file_share"]:
|
||||
event = cast(dict[str, Any], req.payload.get("event", {}))
|
||||
|
||||
# Ensure that the message is a new message + of expected type
|
||||
event_type = event.get("type")
|
||||
if event_type not in ["app_mention", "message"]:
|
||||
channel_specific_logger.info(
|
||||
f"Ignoring message with subtype '{message_subtype}' since is is a special message type"
|
||||
f"Ignoring non-message event of type '{event_type}' for channel '{channel}'"
|
||||
)
|
||||
return
|
||||
|
||||
# Don't insert Danswer thoughts if there's already a long conversation
|
||||
# Or if a bunch of blocks already came through from responding to the @DanswerBot tag
|
||||
if len(event.get("blocks", [])) > 10:
|
||||
channel_specific_logger.debug(
|
||||
"Ignoring message because thread is already long or has been answered to."
|
||||
)
|
||||
return
|
||||
|
||||
@ -89,6 +92,16 @@ def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> No
|
||||
channel_specific_logger.info("Ignoring message from bot")
|
||||
return
|
||||
|
||||
# Ignore things like channel_join, channel_leave, etc.
|
||||
# NOTE: "file_share" is just a message with a file attachment, so we
|
||||
# should not ignore it
|
||||
message_subtype = event.get("subtype")
|
||||
if message_subtype not in [None, "file_share"]:
|
||||
channel_specific_logger.info(
|
||||
f"Ignoring message with subtype '{message_subtype}' since is is a special message type"
|
||||
)
|
||||
return
|
||||
|
||||
message_ts = event.get("ts")
|
||||
thread_ts = event.get("thread_ts")
|
||||
# Pick the root of the thread (if a thread exists)
|
||||
@ -99,25 +112,68 @@ def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> No
|
||||
)
|
||||
return
|
||||
|
||||
msg = cast(str | None, event.get("text"))
|
||||
msg = cast(str, event.get("text", ""))
|
||||
if not msg:
|
||||
channel_specific_logger.error("Unable to process empty message")
|
||||
return
|
||||
|
||||
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":
|
||||
# Acknowledge the request immediately
|
||||
response = SocketModeResponse(envelope_id=req.envelope_id)
|
||||
client.send_socket_mode_response(response)
|
||||
|
||||
# Verify that there's an associated channel
|
||||
channel = req.payload.get("channel_id")
|
||||
channel_specific_logger = _ChannelIdAdapter(
|
||||
logger, extra={_CHANNEL_ID: channel}
|
||||
)
|
||||
if not channel:
|
||||
channel_specific_logger.error(
|
||||
"Received DanswerBot command without channel - skipping"
|
||||
)
|
||||
return
|
||||
|
||||
msg = req.payload.get("text", "")
|
||||
sender = req.payload.get("user_id")
|
||||
if not sender:
|
||||
raise ValueError(
|
||||
"Cannot respond to DanswerBot command without sender to respond to."
|
||||
)
|
||||
|
||||
handle_message(
|
||||
msg=msg,
|
||||
channel=channel,
|
||||
message_ts_to_respond_to=None,
|
||||
sender_id=sender,
|
||||
client=client.web_client,
|
||||
skip_filters=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
|
||||
if req.type == "interactive" and req.payload.get("type") == "block_actions":
|
||||
# Acknowledge the request immediately
|
||||
|
@ -35,27 +35,47 @@ def get_web_client() -> WebClient:
|
||||
def respond_in_thread(
|
||||
client: WebClient,
|
||||
channel: str,
|
||||
thread_ts: str,
|
||||
thread_ts: str | None,
|
||||
text: str | None = None,
|
||||
blocks: list[Block] | None = None,
|
||||
receiver_ids: list[str] | None = None,
|
||||
metadata: Metadata | None = None,
|
||||
unfurl: bool = True,
|
||||
) -> None:
|
||||
if not text and not blocks:
|
||||
raise ValueError("One of `text` or `blocks` must be provided")
|
||||
|
||||
slack_call = make_slack_api_rate_limited(client.chat_postMessage)
|
||||
response = slack_call(
|
||||
channel=channel,
|
||||
text=text,
|
||||
blocks=blocks,
|
||||
thread_ts=thread_ts,
|
||||
metadata=metadata,
|
||||
unfurl_links=unfurl,
|
||||
unfurl_media=unfurl,
|
||||
)
|
||||
if not response.get("ok"):
|
||||
raise RuntimeError(f"Unable to post message: {response}")
|
||||
if not receiver_ids:
|
||||
slack_call = make_slack_api_rate_limited(client.chat_postMessage)
|
||||
else:
|
||||
slack_call = make_slack_api_rate_limited(client.chat_postEphemeral)
|
||||
|
||||
if not receiver_ids:
|
||||
response = slack_call(
|
||||
channel=channel,
|
||||
text=text,
|
||||
blocks=blocks,
|
||||
thread_ts=thread_ts,
|
||||
metadata=metadata,
|
||||
unfurl_links=unfurl,
|
||||
unfurl_media=unfurl,
|
||||
)
|
||||
if not response.get("ok"):
|
||||
raise RuntimeError(f"Failed to post message: {response}")
|
||||
else:
|
||||
for receiver in receiver_ids:
|
||||
response = slack_call(
|
||||
channel=channel,
|
||||
user=receiver,
|
||||
text=text,
|
||||
blocks=blocks,
|
||||
thread_ts=thread_ts,
|
||||
metadata=metadata,
|
||||
unfurl_links=unfurl,
|
||||
unfurl_media=unfurl,
|
||||
)
|
||||
if not response.get("ok"):
|
||||
raise RuntimeError(f"Failed to post message: {response}")
|
||||
|
||||
|
||||
def build_feedback_block_id(
|
||||
@ -136,3 +156,21 @@ def get_channel_from_id(client: WebClient, channel_id: str) -> dict[str, Any]:
|
||||
|
||||
def get_channel_name_from_id(client: WebClient, channel_id: str) -> str:
|
||||
return get_channel_from_id(client, channel_id)["name"]
|
||||
|
||||
|
||||
def fetch_userids_from_emails(user_emails: list[str], client: WebClient) -> list[str]:
|
||||
user_ids: list[str] = []
|
||||
for email in user_emails:
|
||||
try:
|
||||
user = client.users_lookupByEmail(email=email)
|
||||
user_ids.append(user.data["user"]["id"]) # type: ignore
|
||||
except Exception:
|
||||
logger.error(f"Was not able to find slack user by email: {email}")
|
||||
|
||||
if not user_ids:
|
||||
raise RuntimeError(
|
||||
"Was not able to find any Slack users to respond to. "
|
||||
"No email was parsed into a valid slack account."
|
||||
)
|
||||
|
||||
return user_ids
|
||||
|
@ -232,6 +232,7 @@ DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER = os.environ.get(
|
||||
).lower() not in ["false", ""]
|
||||
# Add a second LLM call post Answer to verify if the Answer is valid
|
||||
# Throws out answers that don't directly or fully answer the user query
|
||||
# This is the default for all DanswerBot channels unless the bot is configured individually
|
||||
ENABLE_DANSWERBOT_REFLEXION = (
|
||||
os.environ.get("ENABLE_DANSWERBOT_REFLEXION", "").lower() == "true"
|
||||
)
|
||||
|
@ -482,8 +482,9 @@ class ChannelConfig(TypedDict):
|
||||
in Postgres"""
|
||||
|
||||
channel_names: list[str]
|
||||
answer_validity_check_enabled: NotRequired[bool] # not specified => False
|
||||
team_members: NotRequired[list[str]]
|
||||
respond_sender_only: NotRequired[bool] # defaults to False
|
||||
respond_team_member_list: NotRequired[list[str]]
|
||||
answer_filters: NotRequired[list[str]]
|
||||
|
||||
|
||||
class SlackBotConfig(Base):
|
||||
|
@ -6,9 +6,11 @@ from typing import TypeVar
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import validator
|
||||
from pydantic.generics import GenericModel
|
||||
|
||||
from danswer.auth.schemas import UserRole
|
||||
from danswer.bots.slack.config import VALID_SLACK_FILTERS
|
||||
from danswer.configs.app_configs import MASK_CREDENTIAL_PREFIX
|
||||
from danswer.configs.constants import AuthType
|
||||
from danswer.configs.constants import DocumentSource
|
||||
@ -434,7 +436,18 @@ class SlackBotConfigCreationRequest(BaseModel):
|
||||
# for now for simplicity / speed of development
|
||||
document_sets: list[int]
|
||||
channel_names: list[str]
|
||||
answer_validity_check_enabled: bool
|
||||
# If not responder_sender_only and no team members, assume respond in the channel to everyone
|
||||
respond_sender_only: bool = False
|
||||
respond_team_member_list: list[str] = []
|
||||
answer_filters: list[str] = []
|
||||
|
||||
@validator("answer_filters", pre=True)
|
||||
def validate_filters(cls, value: list[str]) -> list[str]:
|
||||
if any(test not in VALID_SLACK_FILTERS for test in value):
|
||||
raise ValueError(
|
||||
f"Slack Answer filters must be one of {VALID_SLACK_FILTERS}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class SlackBotConfig(BaseModel):
|
||||
|
@ -23,13 +23,19 @@ from danswer.server.models import SlackBotTokens
|
||||
router = APIRouter(prefix="/manage")
|
||||
|
||||
|
||||
@router.post("/admin/slack-bot/config")
|
||||
def create_slack_bot_config(
|
||||
def _form_channel_config(
|
||||
slack_bot_config_creation_request: SlackBotConfigCreationRequest,
|
||||
db_session: Session = Depends(get_session),
|
||||
_: User | None = Depends(current_admin_user),
|
||||
) -> SlackBotConfig:
|
||||
if not slack_bot_config_creation_request.channel_names:
|
||||
current_slack_bot_config_id: int | None,
|
||||
db_session: Session,
|
||||
) -> ChannelConfig:
|
||||
raw_channel_names = slack_bot_config_creation_request.channel_names
|
||||
respond_sender_only = slack_bot_config_creation_request.respond_sender_only
|
||||
respond_team_member_list = (
|
||||
slack_bot_config_creation_request.respond_team_member_list
|
||||
)
|
||||
answer_filters = slack_bot_config_creation_request.answer_filters
|
||||
|
||||
if not raw_channel_names:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Must provide at least one channel name",
|
||||
@ -37,8 +43,8 @@ def create_slack_bot_config(
|
||||
|
||||
try:
|
||||
cleaned_channel_names = validate_channel_names(
|
||||
channel_names=slack_bot_config_creation_request.channel_names,
|
||||
current_slack_bot_config_id=None,
|
||||
channel_names=raw_channel_names,
|
||||
current_slack_bot_config_id=current_slack_bot_config_id,
|
||||
db_session=db_session,
|
||||
)
|
||||
except ValueError as e:
|
||||
@ -47,10 +53,35 @@ def create_slack_bot_config(
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
if respond_sender_only and respond_team_member_list:
|
||||
raise ValueError(
|
||||
"Cannot set DanswerBot to only respond to sender and "
|
||||
"also respond to a predetermined set of users. This is not logically possible..."
|
||||
)
|
||||
|
||||
channel_config: ChannelConfig = {
|
||||
"channel_names": cleaned_channel_names,
|
||||
"answer_validity_check_enabled": slack_bot_config_creation_request.answer_validity_check_enabled,
|
||||
}
|
||||
if respond_sender_only is not None:
|
||||
channel_config["respond_sender_only"] = respond_sender_only
|
||||
if respond_team_member_list:
|
||||
channel_config["respond_team_member_list"] = respond_team_member_list
|
||||
if answer_filters:
|
||||
channel_config["answer_filters"] = answer_filters
|
||||
|
||||
return channel_config
|
||||
|
||||
|
||||
@router.post("/admin/slack-bot/config")
|
||||
def create_slack_bot_config(
|
||||
slack_bot_config_creation_request: SlackBotConfigCreationRequest,
|
||||
db_session: Session = Depends(get_session),
|
||||
_: User | None = Depends(current_admin_user),
|
||||
) -> SlackBotConfig:
|
||||
channel_config = _form_channel_config(
|
||||
slack_bot_config_creation_request, None, db_session
|
||||
)
|
||||
|
||||
slack_bot_config_model = insert_slack_bot_config(
|
||||
document_sets=slack_bot_config_creation_request.document_sets,
|
||||
channel_config=channel_config,
|
||||
@ -75,31 +106,14 @@ def patch_slack_bot_config(
|
||||
db_session: Session = Depends(get_session),
|
||||
_: User | None = Depends(current_admin_user),
|
||||
) -> SlackBotConfig:
|
||||
if not slack_bot_config_creation_request.channel_names:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Must provide at least one channel name",
|
||||
)
|
||||
|
||||
try:
|
||||
cleaned_channel_names = validate_channel_names(
|
||||
channel_names=slack_bot_config_creation_request.channel_names,
|
||||
current_slack_bot_config_id=slack_bot_config_id,
|
||||
db_session=db_session,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=str(e),
|
||||
)
|
||||
channel_config = _form_channel_config(
|
||||
slack_bot_config_creation_request, slack_bot_config_id, db_session
|
||||
)
|
||||
|
||||
slack_bot_config_model = update_slack_bot_config(
|
||||
slack_bot_config_id=slack_bot_config_id,
|
||||
document_sets=slack_bot_config_creation_request.document_sets,
|
||||
channel_config={
|
||||
"channel_names": cleaned_channel_names,
|
||||
"answer_validity_check_enabled": slack_bot_config_creation_request.answer_validity_check_enabled,
|
||||
},
|
||||
channel_config=channel_config,
|
||||
db_session=db_session,
|
||||
)
|
||||
return SlackBotConfig(
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
TextArrayField,
|
||||
} from "@/components/admin/connectors/Field";
|
||||
import { createSlackBotConfig, updateSlackBotConfig } from "./lib";
|
||||
import { channel } from "diagnostics_channel";
|
||||
|
||||
interface SetCreationPopupProps {
|
||||
onClose: () => void;
|
||||
@ -39,9 +38,18 @@ export const SlackBotCreationForm = ({
|
||||
channel_names: existingSlackBotConfig
|
||||
? existingSlackBotConfig.channel_config.channel_names
|
||||
: ([] as string[]),
|
||||
answer_validity_check_enabled:
|
||||
answer_validity_check_enabled: (
|
||||
existingSlackBotConfig?.channel_config?.answer_filters || []
|
||||
).includes("well_answered_postfilter"),
|
||||
questionmark_prefilter_enabled: (
|
||||
existingSlackBotConfig?.channel_config?.answer_filters || []
|
||||
).includes("questionmark_prefilter"),
|
||||
respond_sender_only:
|
||||
existingSlackBotConfig?.channel_config?.respond_sender_only ||
|
||||
false,
|
||||
respond_team_member_list:
|
||||
existingSlackBotConfig?.channel_config
|
||||
?.answer_validity_check_enabled || false,
|
||||
?.respond_team_member_list || ([] as string[]),
|
||||
document_sets: existingSlackBotConfig
|
||||
? existingSlackBotConfig.document_sets.map(
|
||||
(documentSet) => documentSet.id
|
||||
@ -51,6 +59,9 @@ export const SlackBotCreationForm = ({
|
||||
validationSchema={Yup.object().shape({
|
||||
channel_names: Yup.array().of(Yup.string()),
|
||||
answer_validity_check_enabled: Yup.boolean().required(),
|
||||
questionmark_prefilter_enabled: Yup.boolean().required(),
|
||||
respond_sender_only: Yup.boolean().required(),
|
||||
respond_team_member_list: Yup.array().of(Yup.string()).required(),
|
||||
document_sets: Yup.array().of(Yup.number()),
|
||||
})}
|
||||
onSubmit={async (values, formikHelpers) => {
|
||||
@ -62,6 +73,10 @@ export const SlackBotCreationForm = ({
|
||||
channel_names: values.channel_names.filter(
|
||||
(channelName) => channelName !== ""
|
||||
),
|
||||
respond_team_member_list:
|
||||
values.respond_team_member_list.filter(
|
||||
(teamMemberEmail) => teamMemberEmail !== ""
|
||||
),
|
||||
};
|
||||
|
||||
let response;
|
||||
@ -124,6 +139,30 @@ export const SlackBotCreationForm = ({
|
||||
subtext="If set, will only answer questions that the model determines it can answer"
|
||||
/>
|
||||
<div className="border-t border-gray-700 py-2" />
|
||||
<BooleanFormField
|
||||
name="questionmark_prefilter_enabled"
|
||||
label="Only respond to questions"
|
||||
subtext="If set, will only respond to messages that contain a question mark"
|
||||
/>
|
||||
<div className="border-t border-gray-700 py-2" />
|
||||
<BooleanFormField
|
||||
name="respond_sender_only"
|
||||
label="Respond to Sender Only"
|
||||
subtext="If set, will respond with a message that is only visible to the sender"
|
||||
/>
|
||||
<div className="border-t border-gray-700 py-2" />
|
||||
<TextArrayField
|
||||
name="respond_team_member_list"
|
||||
label="Team Members Emails:"
|
||||
subtext={`If specified, DanswerBot responses will only be
|
||||
visible to members in this list. This is
|
||||
useful if you want DanswerBot to operate in an
|
||||
"assistant" mode, where it helps the team members find
|
||||
answers, but let's them build on top of DanswerBot's response / throw
|
||||
out the occasional incorrect answer.`}
|
||||
values={values}
|
||||
/>
|
||||
<div className="border-t border-gray-700 py-2" />
|
||||
<FieldArray
|
||||
name="document_sets"
|
||||
render={(arrayHelpers: ArrayHelpers) => (
|
||||
|
@ -4,8 +4,36 @@ interface SlackBotConfigCreationRequest {
|
||||
document_sets: number[];
|
||||
channel_names: string[];
|
||||
answer_validity_check_enabled: boolean;
|
||||
questionmark_prefilter_enabled: boolean;
|
||||
respond_sender_only: boolean;
|
||||
respond_team_member_list: string[];
|
||||
}
|
||||
|
||||
const buildFiltersFromCreationRequest = (
|
||||
creationRequest: SlackBotConfigCreationRequest
|
||||
): string[] => {
|
||||
const answerFilters = [] as string[];
|
||||
if (creationRequest.answer_validity_check_enabled) {
|
||||
answerFilters.push("well_answered_postfilter");
|
||||
}
|
||||
if (creationRequest.questionmark_prefilter_enabled) {
|
||||
answerFilters.push("questionmark_prefilter");
|
||||
}
|
||||
return answerFilters;
|
||||
};
|
||||
|
||||
const buildRequestBodyFromCreationRequest = (
|
||||
creationRequest: SlackBotConfigCreationRequest
|
||||
) => {
|
||||
return JSON.stringify({
|
||||
channel_names: creationRequest.channel_names,
|
||||
respond_sender_only: creationRequest.respond_sender_only,
|
||||
respond_team_member_list: creationRequest.respond_team_member_list,
|
||||
document_sets: creationRequest.document_sets,
|
||||
answer_filters: buildFiltersFromCreationRequest(creationRequest),
|
||||
});
|
||||
};
|
||||
|
||||
export const createSlackBotConfig = async (
|
||||
creationRequest: SlackBotConfigCreationRequest
|
||||
) => {
|
||||
@ -14,7 +42,7 @@ export const createSlackBotConfig = async (
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(creationRequest),
|
||||
body: buildRequestBodyFromCreationRequest(creationRequest),
|
||||
});
|
||||
};
|
||||
|
||||
@ -27,7 +55,7 @@ export const updateSlackBotConfig = async (
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(creationRequest),
|
||||
body: buildRequestBodyFromCreationRequest(creationRequest),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -4,12 +4,7 @@ import { Button } from "@/components/Button";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
||||
import {
|
||||
BookmarkIcon,
|
||||
CPUIcon,
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { CPUIcon, EditIcon, TrashIcon } from "@/components/icons/icons";
|
||||
import { DocumentSet, SlackBotConfig } from "@/lib/types";
|
||||
import { useState } from "react";
|
||||
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
|
||||
@ -94,10 +89,18 @@ const SlackBotConfigsTable = ({
|
||||
header: "Document Sets",
|
||||
key: "document_sets",
|
||||
},
|
||||
{
|
||||
header: "Team Members",
|
||||
key: "team_members",
|
||||
},
|
||||
{
|
||||
header: "Hide Non-Answers",
|
||||
key: "answer_validity_check_enabled",
|
||||
},
|
||||
{
|
||||
header: "Questions Only",
|
||||
key: "question_mark_only",
|
||||
},
|
||||
{
|
||||
header: "Delete",
|
||||
key: "delete",
|
||||
@ -130,8 +133,23 @@ const SlackBotConfigsTable = ({
|
||||
.join(", ")}
|
||||
</div>
|
||||
),
|
||||
answer_validity_check_enabled: slackBotConfig.channel_config
|
||||
.answer_validity_check_enabled ? (
|
||||
team_members: (
|
||||
<div>
|
||||
{(
|
||||
slackBotConfig.channel_config.respond_team_member_list || []
|
||||
).join(", ")}
|
||||
</div>
|
||||
),
|
||||
answer_validity_check_enabled: (
|
||||
slackBotConfig.channel_config.answer_filters || []
|
||||
).includes("well_answered_postfilter") ? (
|
||||
<div className="text-gray-300">Yes</div>
|
||||
) : (
|
||||
<div className="text-gray-300">No</div>
|
||||
),
|
||||
question_mark_only: (
|
||||
slackBotConfig.channel_config.answer_filters || []
|
||||
).includes("questionmark_prefilter") ? (
|
||||
<div className="text-gray-300">Yes</div>
|
||||
) : (
|
||||
<div className="text-gray-300">No</div>
|
||||
|
@ -64,9 +64,10 @@ const Main = () => {
|
||||
(connectorIndexingStatus) =>
|
||||
connectorIndexingStatus.connector.source === "hubspot"
|
||||
);
|
||||
const hubSpotCredential: Credential<HubSpotCredentialJson> = credentialsData.filter(
|
||||
(credential) => credential.credential_json?.hubspot_access_token
|
||||
)[0];
|
||||
const hubSpotCredential: Credential<HubSpotCredentialJson> =
|
||||
credentialsData.filter(
|
||||
(credential) => credential.credential_json?.hubspot_access_token
|
||||
)[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -124,7 +125,7 @@ const Main = () => {
|
||||
validationSchema={Yup.object().shape({
|
||||
hubspot_access_token: Yup.string().required(
|
||||
"Please enter your HubSpot Access Token"
|
||||
)
|
||||
),
|
||||
})}
|
||||
initialValues={{
|
||||
hubspot_access_token: "",
|
||||
@ -170,8 +171,8 @@ const Main = () => {
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
HubSpot connector is setup! We are pulling the latest tickets from HubSpot
|
||||
every <b>10</b> minutes.
|
||||
HubSpot connector is setup! We are pulling the latest tickets from
|
||||
HubSpot every <b>10</b> minutes.
|
||||
</p>
|
||||
<ConnectorsTable<HubSpotConfig, HubSpotCredentialJson>
|
||||
connectorIndexingStatuses={hubSpotConnectorIndexingStatuses}
|
||||
|
@ -238,10 +238,16 @@ export interface DocumentSet {
|
||||
}
|
||||
|
||||
// SLACK BOT CONFIGS
|
||||
|
||||
export type AnswerFilterOption =
|
||||
| "well_answered_postfilter"
|
||||
| "questionmark_prefilter";
|
||||
|
||||
export interface ChannelConfig {
|
||||
channel_names: string[];
|
||||
answer_validity_check_enabled?: boolean;
|
||||
team_members?: string[];
|
||||
respond_sender_only?: boolean;
|
||||
respond_team_member_list?: string[];
|
||||
answer_filters?: AnswerFilterOption[];
|
||||
}
|
||||
|
||||
export interface SlackBotConfig {
|
||||
|
Loading…
x
Reference in New Issue
Block a user