Support more Slack Config Options (#494)

---------

Co-authored-by: Weves <chrisweaver101@gmail.com>
This commit is contained in:
Yuhong Sun 2023-10-03 14:55:29 -07:00 committed by GitHub
parent c2721c7889
commit 59bac1ca8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 399 additions and 91 deletions

View File

@ -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,

View File

@ -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:

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"
)

View File

@ -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):

View File

@ -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):

View File

@ -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(

View File

@ -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) => (

View File

@ -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),
});
};

View File

@ -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>

View File

@ -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}

View File

@ -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 {