mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-27 12:29:41 +02:00
Support more Slack Config Options (#494)
--------- Co-authored-by: Weves <chrisweaver101@gmail.com>
This commit is contained in:
@@ -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(
|
def build_documents_blocks(
|
||||||
documents: list[SearchDoc],
|
documents: list[SearchDoc],
|
||||||
query_event_id: int,
|
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
|
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(
|
def get_slack_bot_config_for_channel(
|
||||||
channel_name: str, db_session: Session
|
channel_name: str, db_session: Session
|
||||||
) -> SlackBotConfig | None:
|
) -> 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_documents_blocks
|
||||||
from danswer.bots.slack.blocks import build_qa_response_blocks
|
from danswer.bots.slack.blocks import build_qa_response_blocks
|
||||||
|
from danswer.bots.slack.blocks import get_restate_blocks
|
||||||
from danswer.bots.slack.config import get_slack_bot_config_for_channel
|
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 get_channel_name_from_id
|
||||||
from danswer.bots.slack.utils import respond_in_thread
|
from danswer.bots.slack.utils import respond_in_thread
|
||||||
from danswer.configs.app_configs import DANSWER_BOT_ANSWER_GENERATION_TIMEOUT
|
from danswer.configs.app_configs import DANSWER_BOT_ANSWER_GENERATION_TIMEOUT
|
||||||
@@ -25,9 +27,12 @@ from danswer.server.models import QuestionRequest
|
|||||||
def handle_message(
|
def handle_message(
|
||||||
msg: str,
|
msg: str,
|
||||||
channel: str,
|
channel: str,
|
||||||
message_ts_to_respond_to: str,
|
message_ts_to_respond_to: str | None,
|
||||||
|
sender_id: str | None,
|
||||||
client: WebClient,
|
client: WebClient,
|
||||||
logger: logging.Logger,
|
logger: logging.Logger,
|
||||||
|
skip_filters: bool = False,
|
||||||
|
is_bot_msg: bool = False,
|
||||||
num_retries: int = DANSWER_BOT_NUM_RETRIES,
|
num_retries: int = DANSWER_BOT_NUM_RETRIES,
|
||||||
answer_generation_timeout: int = DANSWER_BOT_ANSWER_GENERATION_TIMEOUT,
|
answer_generation_timeout: int = DANSWER_BOT_ANSWER_GENERATION_TIMEOUT,
|
||||||
should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS,
|
should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS,
|
||||||
@@ -40,20 +45,60 @@ def handle_message(
|
|||||||
channel_name=channel_name, db_session=db_session
|
channel_name=channel_name, db_session=db_session
|
||||||
)
|
)
|
||||||
document_set_names: list[str] | None = None
|
document_set_names: list[str] | None = None
|
||||||
validity_check_enabled = ENABLE_DANSWERBOT_REFLEXION
|
|
||||||
if slack_bot_config and slack_bot_config.persona:
|
if slack_bot_config and slack_bot_config.persona:
|
||||||
document_set_names = [
|
document_set_names = [
|
||||||
document_set.name
|
document_set.name
|
||||||
for document_set in slack_bot_config.persona.document_sets
|
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(
|
logger.info(
|
||||||
"Found slack bot config for channel. Restricting bot to use document "
|
"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(
|
@retry(
|
||||||
tries=num_retries,
|
tries=num_retries,
|
||||||
delay=0.25,
|
delay=0.25,
|
||||||
@@ -70,7 +115,7 @@ def handle_message(
|
|||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
answer_generation_timeout=answer_generation_timeout,
|
answer_generation_timeout=answer_generation_timeout,
|
||||||
real_time_flow=False,
|
real_time_flow=False,
|
||||||
enable_reflexion=validity_check_enabled,
|
enable_reflexion=reflexion,
|
||||||
)
|
)
|
||||||
if not answer.error_msg:
|
if not answer.error_msg:
|
||||||
return answer
|
return answer
|
||||||
@@ -100,6 +145,7 @@ def handle_message(
|
|||||||
respond_in_thread(
|
respond_in_thread(
|
||||||
client=client,
|
client=client,
|
||||||
channel=channel,
|
channel=channel,
|
||||||
|
receiver_ids=None,
|
||||||
text=f"Encountered exception when trying to answer: \n\n```{e}```",
|
text=f"Encountered exception when trying to answer: \n\n```{e}```",
|
||||||
thread_ts=message_ts_to_respond_to,
|
thread_ts=message_ts_to_respond_to,
|
||||||
)
|
)
|
||||||
@@ -121,6 +167,7 @@ def handle_message(
|
|||||||
respond_in_thread(
|
respond_in_thread(
|
||||||
client=client,
|
client=client,
|
||||||
channel=channel,
|
channel=channel,
|
||||||
|
receiver_ids=None,
|
||||||
text="Found no documents when trying to answer. Did you index any documents?",
|
text="Found no documents when trying to answer. Did you index any documents?",
|
||||||
thread_ts=message_ts_to_respond_to,
|
thread_ts=message_ts_to_respond_to,
|
||||||
)
|
)
|
||||||
@@ -134,6 +181,10 @@ def handle_message(
|
|||||||
return
|
return
|
||||||
|
|
||||||
# convert raw response into "nicely" formatted Slack message
|
# 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(
|
answer_blocks = build_qa_response_blocks(
|
||||||
query_event_id=answer.query_event_id,
|
query_event_id=answer.query_event_id,
|
||||||
answer=answer.answer,
|
answer=answer.answer,
|
||||||
@@ -148,12 +199,33 @@ def handle_message(
|
|||||||
respond_in_thread(
|
respond_in_thread(
|
||||||
client=client,
|
client=client,
|
||||||
channel=channel,
|
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,
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
f"Unable to process message - could not respond in slack in {num_retries} attempts"
|
f"Unable to process message - could not respond in slack in {num_retries} attempts"
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from collections.abc import MutableMapping
|
from collections.abc import MutableMapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import cast
|
from typing import cast
|
||||||
@@ -51,37 +52,39 @@ def _get_socket_client() -> SocketModeClient:
|
|||||||
|
|
||||||
def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None:
|
def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> None:
|
||||||
logger.info(f"Received Slack request of type: '{req.type}'")
|
logger.info(f"Received Slack request of type: '{req.type}'")
|
||||||
|
|
||||||
if req.type == "events_api":
|
if req.type == "events_api":
|
||||||
# Acknowledge the request immediately
|
# Acknowledge the request immediately
|
||||||
response = SocketModeResponse(envelope_id=req.envelope_id)
|
response = SocketModeResponse(envelope_id=req.envelope_id)
|
||||||
client.send_socket_mode_response(response)
|
client.send_socket_mode_response(response)
|
||||||
|
|
||||||
|
# Verify channel is valid
|
||||||
event = cast(dict[str, Any], req.payload.get("event", {}))
|
event = cast(dict[str, Any], req.payload.get("event", {}))
|
||||||
channel = cast(str | None, event.get("channel"))
|
channel = cast(str | None, event.get("channel"))
|
||||||
channel_specific_logger = _ChannelIdAdapter(
|
channel_specific_logger = _ChannelIdAdapter(
|
||||||
logger, extra={_CHANNEL_ID: channel}
|
logger, extra={_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
|
# this should never happen, but we can't continue without a channel since
|
||||||
# we can't send a response without it
|
# we can't send a response without it
|
||||||
if not channel:
|
if not channel:
|
||||||
channel_specific_logger.error("Found message without channel - skipping")
|
channel_specific_logger.error("Found message without channel - skipping")
|
||||||
return
|
return
|
||||||
|
|
||||||
message_subtype = event.get("subtype")
|
event = cast(dict[str, Any], req.payload.get("event", {}))
|
||||||
# ignore things like channel_join, channel_leave, etc.
|
|
||||||
# NOTE: "file_share" is just a message with a file attachment, so we
|
# Ensure that the message is a new message + of expected type
|
||||||
# should not ignore it
|
event_type = event.get("type")
|
||||||
if message_subtype not in [None, "file_share"]:
|
if event_type not in ["app_mention", "message"]:
|
||||||
channel_specific_logger.info(
|
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
|
return
|
||||||
|
|
||||||
@@ -89,6 +92,16 @@ def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> No
|
|||||||
channel_specific_logger.info("Ignoring message from bot")
|
channel_specific_logger.info("Ignoring message from bot")
|
||||||
return
|
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")
|
message_ts = event.get("ts")
|
||||||
thread_ts = event.get("thread_ts")
|
thread_ts = event.get("thread_ts")
|
||||||
# Pick the root of the thread (if a thread exists)
|
# Pick the root of the thread (if a thread exists)
|
||||||
@@ -99,25 +112,68 @@ def _process_slack_event(client: SocketModeClient, req: SocketModeRequest) -> No
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = cast(str | None, event.get("text"))
|
msg = cast(str, event.get("text", ""))
|
||||||
if not msg:
|
if not msg:
|
||||||
channel_specific_logger.error("Unable to process empty message")
|
channel_specific_logger.error("Unable to process empty message")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
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,
|
# TODO: message should be enqueued and processed elsewhere,
|
||||||
# but doing it here for now for simplicity
|
# but doing it here for now for simplicity
|
||||||
handle_message(
|
handle_message(
|
||||||
msg=msg,
|
msg=msg,
|
||||||
channel=channel,
|
channel=channel,
|
||||||
message_ts_to_respond_to=message_ts_to_respond_to,
|
message_ts_to_respond_to=message_ts_to_respond_to,
|
||||||
|
sender_id=event.get("user") or None,
|
||||||
client=client.web_client,
|
client=client.web_client,
|
||||||
|
skip_filters=tagged,
|
||||||
logger=cast(logging.Logger, channel_specific_logger),
|
logger=cast(logging.Logger, channel_specific_logger),
|
||||||
)
|
)
|
||||||
|
|
||||||
channel_specific_logger.info(
|
channel_specific_logger.info(
|
||||||
f"Successfully processed message with ts: '{message_ts}'"
|
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
|
# Handle button clicks
|
||||||
if req.type == "interactive" and req.payload.get("type") == "block_actions":
|
if req.type == "interactive" and req.payload.get("type") == "block_actions":
|
||||||
# Acknowledge the request immediately
|
# Acknowledge the request immediately
|
||||||
|
@@ -35,27 +35,47 @@ def get_web_client() -> WebClient:
|
|||||||
def respond_in_thread(
|
def respond_in_thread(
|
||||||
client: WebClient,
|
client: WebClient,
|
||||||
channel: str,
|
channel: str,
|
||||||
thread_ts: str,
|
thread_ts: str | None,
|
||||||
text: str | None = None,
|
text: str | None = None,
|
||||||
blocks: list[Block] | None = None,
|
blocks: list[Block] | None = None,
|
||||||
|
receiver_ids: list[str] | None = None,
|
||||||
metadata: Metadata | None = None,
|
metadata: Metadata | None = None,
|
||||||
unfurl: bool = True,
|
unfurl: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not text and not blocks:
|
if not text and not blocks:
|
||||||
raise ValueError("One of `text` or `blocks` must be provided")
|
raise ValueError("One of `text` or `blocks` must be provided")
|
||||||
|
|
||||||
slack_call = make_slack_api_rate_limited(client.chat_postMessage)
|
if not receiver_ids:
|
||||||
response = slack_call(
|
slack_call = make_slack_api_rate_limited(client.chat_postMessage)
|
||||||
channel=channel,
|
else:
|
||||||
text=text,
|
slack_call = make_slack_api_rate_limited(client.chat_postEphemeral)
|
||||||
blocks=blocks,
|
|
||||||
thread_ts=thread_ts,
|
if not receiver_ids:
|
||||||
metadata=metadata,
|
response = slack_call(
|
||||||
unfurl_links=unfurl,
|
channel=channel,
|
||||||
unfurl_media=unfurl,
|
text=text,
|
||||||
)
|
blocks=blocks,
|
||||||
if not response.get("ok"):
|
thread_ts=thread_ts,
|
||||||
raise RuntimeError(f"Unable to post message: {response}")
|
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(
|
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:
|
def get_channel_name_from_id(client: WebClient, channel_id: str) -> str:
|
||||||
return get_channel_from_id(client, channel_id)["name"]
|
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", ""]
|
).lower() not in ["false", ""]
|
||||||
# Add a second LLM call post Answer to verify if the Answer is valid
|
# 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
|
# 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 = (
|
ENABLE_DANSWERBOT_REFLEXION = (
|
||||||
os.environ.get("ENABLE_DANSWERBOT_REFLEXION", "").lower() == "true"
|
os.environ.get("ENABLE_DANSWERBOT_REFLEXION", "").lower() == "true"
|
||||||
)
|
)
|
||||||
|
@@ -482,8 +482,9 @@ class ChannelConfig(TypedDict):
|
|||||||
in Postgres"""
|
in Postgres"""
|
||||||
|
|
||||||
channel_names: list[str]
|
channel_names: list[str]
|
||||||
answer_validity_check_enabled: NotRequired[bool] # not specified => False
|
respond_sender_only: NotRequired[bool] # defaults to False
|
||||||
team_members: NotRequired[list[str]]
|
respond_team_member_list: NotRequired[list[str]]
|
||||||
|
answer_filters: NotRequired[list[str]]
|
||||||
|
|
||||||
|
|
||||||
class SlackBotConfig(Base):
|
class SlackBotConfig(Base):
|
||||||
|
@@ -6,9 +6,11 @@ from typing import TypeVar
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from pydantic import validator
|
||||||
from pydantic.generics import GenericModel
|
from pydantic.generics import GenericModel
|
||||||
|
|
||||||
from danswer.auth.schemas import UserRole
|
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.app_configs import MASK_CREDENTIAL_PREFIX
|
||||||
from danswer.configs.constants import AuthType
|
from danswer.configs.constants import AuthType
|
||||||
from danswer.configs.constants import DocumentSource
|
from danswer.configs.constants import DocumentSource
|
||||||
@@ -434,7 +436,18 @@ class SlackBotConfigCreationRequest(BaseModel):
|
|||||||
# for now for simplicity / speed of development
|
# for now for simplicity / speed of development
|
||||||
document_sets: list[int]
|
document_sets: list[int]
|
||||||
channel_names: list[str]
|
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):
|
class SlackBotConfig(BaseModel):
|
||||||
|
@@ -23,13 +23,19 @@ from danswer.server.models import SlackBotTokens
|
|||||||
router = APIRouter(prefix="/manage")
|
router = APIRouter(prefix="/manage")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/admin/slack-bot/config")
|
def _form_channel_config(
|
||||||
def create_slack_bot_config(
|
|
||||||
slack_bot_config_creation_request: SlackBotConfigCreationRequest,
|
slack_bot_config_creation_request: SlackBotConfigCreationRequest,
|
||||||
db_session: Session = Depends(get_session),
|
current_slack_bot_config_id: int | None,
|
||||||
_: User | None = Depends(current_admin_user),
|
db_session: Session,
|
||||||
) -> SlackBotConfig:
|
) -> ChannelConfig:
|
||||||
if not slack_bot_config_creation_request.channel_names:
|
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(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Must provide at least one channel name",
|
detail="Must provide at least one channel name",
|
||||||
@@ -37,8 +43,8 @@ def create_slack_bot_config(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
cleaned_channel_names = validate_channel_names(
|
cleaned_channel_names = validate_channel_names(
|
||||||
channel_names=slack_bot_config_creation_request.channel_names,
|
channel_names=raw_channel_names,
|
||||||
current_slack_bot_config_id=None,
|
current_slack_bot_config_id=current_slack_bot_config_id,
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -47,10 +53,35 @@ def create_slack_bot_config(
|
|||||||
detail=str(e),
|
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_config: ChannelConfig = {
|
||||||
"channel_names": cleaned_channel_names,
|
"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(
|
slack_bot_config_model = insert_slack_bot_config(
|
||||||
document_sets=slack_bot_config_creation_request.document_sets,
|
document_sets=slack_bot_config_creation_request.document_sets,
|
||||||
channel_config=channel_config,
|
channel_config=channel_config,
|
||||||
@@ -75,31 +106,14 @@ def patch_slack_bot_config(
|
|||||||
db_session: Session = Depends(get_session),
|
db_session: Session = Depends(get_session),
|
||||||
_: User | None = Depends(current_admin_user),
|
_: User | None = Depends(current_admin_user),
|
||||||
) -> SlackBotConfig:
|
) -> SlackBotConfig:
|
||||||
if not slack_bot_config_creation_request.channel_names:
|
channel_config = _form_channel_config(
|
||||||
raise HTTPException(
|
slack_bot_config_creation_request, slack_bot_config_id, db_session
|
||||||
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),
|
|
||||||
)
|
|
||||||
|
|
||||||
slack_bot_config_model = update_slack_bot_config(
|
slack_bot_config_model = update_slack_bot_config(
|
||||||
slack_bot_config_id=slack_bot_config_id,
|
slack_bot_config_id=slack_bot_config_id,
|
||||||
document_sets=slack_bot_config_creation_request.document_sets,
|
document_sets=slack_bot_config_creation_request.document_sets,
|
||||||
channel_config={
|
channel_config=channel_config,
|
||||||
"channel_names": cleaned_channel_names,
|
|
||||||
"answer_validity_check_enabled": slack_bot_config_creation_request.answer_validity_check_enabled,
|
|
||||||
},
|
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
)
|
)
|
||||||
return SlackBotConfig(
|
return SlackBotConfig(
|
||||||
|
@@ -7,7 +7,6 @@ import {
|
|||||||
TextArrayField,
|
TextArrayField,
|
||||||
} from "@/components/admin/connectors/Field";
|
} from "@/components/admin/connectors/Field";
|
||||||
import { createSlackBotConfig, updateSlackBotConfig } from "./lib";
|
import { createSlackBotConfig, updateSlackBotConfig } from "./lib";
|
||||||
import { channel } from "diagnostics_channel";
|
|
||||||
|
|
||||||
interface SetCreationPopupProps {
|
interface SetCreationPopupProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -39,9 +38,18 @@ export const SlackBotCreationForm = ({
|
|||||||
channel_names: existingSlackBotConfig
|
channel_names: existingSlackBotConfig
|
||||||
? existingSlackBotConfig.channel_config.channel_names
|
? existingSlackBotConfig.channel_config.channel_names
|
||||||
: ([] as string[]),
|
: ([] 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
|
existingSlackBotConfig?.channel_config
|
||||||
?.answer_validity_check_enabled || false,
|
?.respond_team_member_list || ([] as string[]),
|
||||||
document_sets: existingSlackBotConfig
|
document_sets: existingSlackBotConfig
|
||||||
? existingSlackBotConfig.document_sets.map(
|
? existingSlackBotConfig.document_sets.map(
|
||||||
(documentSet) => documentSet.id
|
(documentSet) => documentSet.id
|
||||||
@@ -51,6 +59,9 @@ export const SlackBotCreationForm = ({
|
|||||||
validationSchema={Yup.object().shape({
|
validationSchema={Yup.object().shape({
|
||||||
channel_names: Yup.array().of(Yup.string()),
|
channel_names: Yup.array().of(Yup.string()),
|
||||||
answer_validity_check_enabled: Yup.boolean().required(),
|
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()),
|
document_sets: Yup.array().of(Yup.number()),
|
||||||
})}
|
})}
|
||||||
onSubmit={async (values, formikHelpers) => {
|
onSubmit={async (values, formikHelpers) => {
|
||||||
@@ -62,6 +73,10 @@ export const SlackBotCreationForm = ({
|
|||||||
channel_names: values.channel_names.filter(
|
channel_names: values.channel_names.filter(
|
||||||
(channelName) => channelName !== ""
|
(channelName) => channelName !== ""
|
||||||
),
|
),
|
||||||
|
respond_team_member_list:
|
||||||
|
values.respond_team_member_list.filter(
|
||||||
|
(teamMemberEmail) => teamMemberEmail !== ""
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
@@ -124,6 +139,30 @@ export const SlackBotCreationForm = ({
|
|||||||
subtext="If set, will only answer questions that the model determines it can answer"
|
subtext="If set, will only answer questions that the model determines it can answer"
|
||||||
/>
|
/>
|
||||||
<div className="border-t border-gray-700 py-2" />
|
<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
|
<FieldArray
|
||||||
name="document_sets"
|
name="document_sets"
|
||||||
render={(arrayHelpers: ArrayHelpers) => (
|
render={(arrayHelpers: ArrayHelpers) => (
|
||||||
|
@@ -4,8 +4,36 @@ interface SlackBotConfigCreationRequest {
|
|||||||
document_sets: number[];
|
document_sets: number[];
|
||||||
channel_names: string[];
|
channel_names: string[];
|
||||||
answer_validity_check_enabled: boolean;
|
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 (
|
export const createSlackBotConfig = async (
|
||||||
creationRequest: SlackBotConfigCreationRequest
|
creationRequest: SlackBotConfigCreationRequest
|
||||||
) => {
|
) => {
|
||||||
@@ -14,7 +42,7 @@ export const createSlackBotConfig = async (
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(creationRequest),
|
body: buildRequestBodyFromCreationRequest(creationRequest),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,7 +55,7 @@ export const updateSlackBotConfig = async (
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"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 { ThreeDotsLoader } from "@/components/Loading";
|
||||||
import { PageSelector } from "@/components/PageSelector";
|
import { PageSelector } from "@/components/PageSelector";
|
||||||
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
||||||
import {
|
import { CPUIcon, EditIcon, TrashIcon } from "@/components/icons/icons";
|
||||||
BookmarkIcon,
|
|
||||||
CPUIcon,
|
|
||||||
EditIcon,
|
|
||||||
TrashIcon,
|
|
||||||
} from "@/components/icons/icons";
|
|
||||||
import { DocumentSet, SlackBotConfig } from "@/lib/types";
|
import { DocumentSet, SlackBotConfig } from "@/lib/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
|
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
|
||||||
@@ -94,10 +89,18 @@ const SlackBotConfigsTable = ({
|
|||||||
header: "Document Sets",
|
header: "Document Sets",
|
||||||
key: "document_sets",
|
key: "document_sets",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: "Team Members",
|
||||||
|
key: "team_members",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: "Hide Non-Answers",
|
header: "Hide Non-Answers",
|
||||||
key: "answer_validity_check_enabled",
|
key: "answer_validity_check_enabled",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: "Questions Only",
|
||||||
|
key: "question_mark_only",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: "Delete",
|
header: "Delete",
|
||||||
key: "delete",
|
key: "delete",
|
||||||
@@ -130,8 +133,23 @@ const SlackBotConfigsTable = ({
|
|||||||
.join(", ")}
|
.join(", ")}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
answer_validity_check_enabled: slackBotConfig.channel_config
|
team_members: (
|
||||||
.answer_validity_check_enabled ? (
|
<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">Yes</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-gray-300">No</div>
|
<div className="text-gray-300">No</div>
|
||||||
|
@@ -64,9 +64,10 @@ const Main = () => {
|
|||||||
(connectorIndexingStatus) =>
|
(connectorIndexingStatus) =>
|
||||||
connectorIndexingStatus.connector.source === "hubspot"
|
connectorIndexingStatus.connector.source === "hubspot"
|
||||||
);
|
);
|
||||||
const hubSpotCredential: Credential<HubSpotCredentialJson> = credentialsData.filter(
|
const hubSpotCredential: Credential<HubSpotCredentialJson> =
|
||||||
(credential) => credential.credential_json?.hubspot_access_token
|
credentialsData.filter(
|
||||||
)[0];
|
(credential) => credential.credential_json?.hubspot_access_token
|
||||||
|
)[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -124,7 +125,7 @@ const Main = () => {
|
|||||||
validationSchema={Yup.object().shape({
|
validationSchema={Yup.object().shape({
|
||||||
hubspot_access_token: Yup.string().required(
|
hubspot_access_token: Yup.string().required(
|
||||||
"Please enter your HubSpot Access Token"
|
"Please enter your HubSpot Access Token"
|
||||||
)
|
),
|
||||||
})}
|
})}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
hubspot_access_token: "",
|
hubspot_access_token: "",
|
||||||
@@ -170,8 +171,8 @@ const Main = () => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm mb-2">
|
<p className="text-sm mb-2">
|
||||||
HubSpot connector is setup! We are pulling the latest tickets from HubSpot
|
HubSpot connector is setup! We are pulling the latest tickets from
|
||||||
every <b>10</b> minutes.
|
HubSpot every <b>10</b> minutes.
|
||||||
</p>
|
</p>
|
||||||
<ConnectorsTable<HubSpotConfig, HubSpotCredentialJson>
|
<ConnectorsTable<HubSpotConfig, HubSpotCredentialJson>
|
||||||
connectorIndexingStatuses={hubSpotConnectorIndexingStatuses}
|
connectorIndexingStatuses={hubSpotConnectorIndexingStatuses}
|
||||||
|
@@ -238,10 +238,16 @@ export interface DocumentSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SLACK BOT CONFIGS
|
// SLACK BOT CONFIGS
|
||||||
|
|
||||||
|
export type AnswerFilterOption =
|
||||||
|
| "well_answered_postfilter"
|
||||||
|
| "questionmark_prefilter";
|
||||||
|
|
||||||
export interface ChannelConfig {
|
export interface ChannelConfig {
|
||||||
channel_names: string[];
|
channel_names: string[];
|
||||||
answer_validity_check_enabled?: boolean;
|
respond_sender_only?: boolean;
|
||||||
team_members?: string[];
|
respond_team_member_list?: string[];
|
||||||
|
answer_filters?: AnswerFilterOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SlackBotConfig {
|
export interface SlackBotConfig {
|
||||||
|
Reference in New Issue
Block a user