Fixed slack groups (#1814)

* Simplified slackbot response groups and fixed need more help bug

* mypy fixes

* added exceptions for the couldnt find passthrough arrays
This commit is contained in:
hagen-danswer 2024-07-13 15:34:35 -07:00 committed by GitHub
parent c7af6a4601
commit 36da2e4b27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 137 additions and 96 deletions

View File

@ -0,0 +1,45 @@
"""combined slack id fields
Revision ID: d716b0791ddd
Revises: 7aea705850d5
Create Date: 2024-07-10 17:57:45.630550
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "d716b0791ddd"
down_revision = "7aea705850d5"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"""
UPDATE slack_bot_config
SET channel_config = jsonb_set(
channel_config,
'{respond_member_group_list}',
coalesce(channel_config->'respond_team_member_list', '[]'::jsonb) ||
coalesce(channel_config->'respond_slack_group_list', '[]'::jsonb)
) - 'respond_team_member_list' - 'respond_slack_group_list'
"""
)
def downgrade() -> None:
op.execute(
"""
UPDATE slack_bot_config
SET channel_config = jsonb_set(
jsonb_set(
channel_config - 'respond_member_group_list',
'{respond_team_member_list}',
'[]'::jsonb
),
'{respond_slack_group_list}',
'[]'::jsonb
)
"""
)

View File

@ -474,7 +474,7 @@ def build_follow_up_resolved_blocks(
if tag_str: if tag_str:
tag_str += " " tag_str += " "
group_str = " ".join([f"<!subteam^{group}>" for group in group_ids]) group_str = " ".join([f"<!subteam^{group_id}|>" for group_id in group_ids])
if group_str: if group_str:
group_str += " " group_str += " "

View File

@ -29,8 +29,8 @@ from danswer.danswerbot.slack.handlers.handle_regular_answer import (
from danswer.danswerbot.slack.models import SlackMessageInfo from danswer.danswerbot.slack.models import SlackMessageInfo
from danswer.danswerbot.slack.utils import build_feedback_id from danswer.danswerbot.slack.utils import build_feedback_id
from danswer.danswerbot.slack.utils import decompose_action_id from danswer.danswerbot.slack.utils import decompose_action_id
from danswer.danswerbot.slack.utils import fetch_groupids_from_names from danswer.danswerbot.slack.utils import fetch_group_ids_from_names
from danswer.danswerbot.slack.utils import fetch_userids_from_emails from danswer.danswerbot.slack.utils import fetch_user_ids_from_emails
from danswer.danswerbot.slack.utils import get_channel_name_from_id from danswer.danswerbot.slack.utils import get_channel_name_from_id
from danswer.danswerbot.slack.utils import get_feedback_visibility from danswer.danswerbot.slack.utils import get_feedback_visibility
from danswer.danswerbot.slack.utils import read_slack_thread from danswer.danswerbot.slack.utils import read_slack_thread
@ -43,7 +43,7 @@ from danswer.document_index.document_index_utils import get_both_index_names
from danswer.document_index.factory import get_default_document_index from danswer.document_index.factory import get_default_document_index
from danswer.utils.logger import setup_logger from danswer.utils.logger import setup_logger
logger_base = setup_logger() logger = setup_logger()
def handle_doc_feedback_button( def handle_doc_feedback_button(
@ -51,7 +51,7 @@ def handle_doc_feedback_button(
client: SocketModeClient, client: SocketModeClient,
) -> None: ) -> None:
if not (actions := req.payload.get("actions")): if not (actions := req.payload.get("actions")):
logger_base.error("Missing actions. Unable to build the source feedback view") logger.error("Missing actions. Unable to build the source feedback view")
return return
# Extracts the feedback_id coming from the 'source feedback' button # Extracts the feedback_id coming from the 'source feedback' button
@ -134,7 +134,7 @@ def handle_generate_answer_button(
receiver_ids=None, receiver_ids=None,
client=client.web_client, client=client.web_client,
channel=channel_id, channel=channel_id,
logger=cast(logging.Logger, logger_base), logger=cast(logging.Logger, logger),
feedback_reminder_id=None, feedback_reminder_id=None,
) )
@ -196,7 +196,7 @@ def handle_slack_feedback(
feedback=feedback, feedback=feedback,
) )
else: else:
logger_base.error(f"Feedback type '{feedback_type}' not supported") logger.error(f"Feedback type '{feedback_type}' not supported")
if get_feedback_visibility() == FeedbackVisibility.PRIVATE or feedback_type not in [ if get_feedback_visibility() == FeedbackVisibility.PRIVATE or feedback_type not in [
LIKE_BLOCK_ACTION_ID, LIKE_BLOCK_ACTION_ID,
@ -260,11 +260,11 @@ def handle_followup_button(
tag_names = slack_bot_config.channel_config.get("follow_up_tags") tag_names = slack_bot_config.channel_config.get("follow_up_tags")
remaining = None remaining = None
if tag_names: if tag_names:
tag_ids, remaining = fetch_userids_from_emails( tag_ids, remaining = fetch_user_ids_from_emails(
tag_names, client.web_client tag_names, client.web_client
) )
if remaining: if remaining:
group_ids, _ = fetch_groupids_from_names(remaining, client.web_client) group_ids, _ = fetch_group_ids_from_names(remaining, client.web_client)
blocks = build_follow_up_resolved_blocks(tag_ids=tag_ids, group_ids=group_ids) blocks = build_follow_up_resolved_blocks(tag_ids=tag_ids, group_ids=group_ids)
@ -339,7 +339,7 @@ def handle_followup_resolved_button(
) )
if not response.get("ok"): if not response.get("ok"):
logger_base.error("Unable to delete message for resolved") logger.error("Unable to delete message for resolved")
if immediate: if immediate:
msg_text = f"{clicker_name} has marked this question as resolved!" msg_text = f"{clicker_name} has marked this question as resolved!"

View File

@ -18,8 +18,8 @@ from danswer.danswerbot.slack.handlers.handle_standard_answers import (
) )
from danswer.danswerbot.slack.models import SlackMessageInfo from danswer.danswerbot.slack.models import SlackMessageInfo
from danswer.danswerbot.slack.utils import ChannelIdAdapter from danswer.danswerbot.slack.utils import ChannelIdAdapter
from danswer.danswerbot.slack.utils import fetch_userids_from_emails from danswer.danswerbot.slack.utils import fetch_user_ids_from_emails
from danswer.danswerbot.slack.utils import fetch_userids_from_groups from danswer.danswerbot.slack.utils import fetch_user_ids_from_groups
from danswer.danswerbot.slack.utils import respond_in_thread from danswer.danswerbot.slack.utils import respond_in_thread
from danswer.danswerbot.slack.utils import slack_usage_report from danswer.danswerbot.slack.utils import slack_usage_report
from danswer.danswerbot.slack.utils import update_emote_react from danswer.danswerbot.slack.utils import update_emote_react
@ -158,11 +158,8 @@ def handle_message(
] ]
prompt = persona.prompts[0] if persona.prompts else None prompt = persona.prompts[0] if persona.prompts else None
# List of user id to send message to, if None, send to everyone in channel
send_to: list[str] | None = None
respond_tag_only = False respond_tag_only = False
respond_team_member_list = None respond_member_group_list = None
respond_slack_group_list = None
channel_conf = None channel_conf = None
if slack_bot_config and slack_bot_config.channel_config: if slack_bot_config and slack_bot_config.channel_config:
@ -184,8 +181,7 @@ def handle_message(
) )
respond_tag_only = channel_conf.get("respond_tag_only") or False respond_tag_only = channel_conf.get("respond_tag_only") or False
respond_team_member_list = channel_conf.get("respond_team_member_list") or None respond_member_group_list = channel_conf.get("respond_member_group_list", None)
respond_slack_group_list = channel_conf.get("respond_slack_group_list") or None
if respond_tag_only and not bypass_filters: if respond_tag_only and not bypass_filters:
logger.info( logger.info(
@ -194,17 +190,23 @@ def handle_message(
) )
return False return False
if respond_team_member_list: # List of user id to send message to, if None, send to everyone in channel
send_to, _ = fetch_userids_from_emails(respond_team_member_list, client) send_to: list[str] | None = None
if respond_slack_group_list: missing_users: list[str] | None = None
user_ids, _ = fetch_userids_from_groups(respond_slack_group_list, client) if respond_member_group_list:
send_to = (send_to + user_ids) if send_to else user_ids send_to, missing_ids = fetch_user_ids_from_emails(
if send_to: respond_member_group_list, client
send_to = list(set(send_to)) # remove duplicates )
user_ids, missing_users = fetch_user_ids_from_groups(missing_ids, client)
send_to = list(set(send_to + user_ids)) if send_to else user_ids
if missing_users:
logger.warning(f"Failed to find these users/groups: {missing_users}")
# If configured to respond to team members only, then cannot be used with a /DanswerBot command # If configured to respond to team members only, then cannot be used with a /DanswerBot command
# which would just respond to the sender # which would just respond to the sender
if (respond_team_member_list or respond_slack_group_list) and is_bot_msg: if send_to and is_bot_msg:
if sender_id: if sender_id:
respond_in_thread( respond_in_thread(
client=client, client=client,

View File

@ -302,7 +302,7 @@ def get_channel_name_from_id(
raise e raise e
def fetch_userids_from_emails( def fetch_user_ids_from_emails(
user_emails: list[str], client: WebClient user_emails: list[str], client: WebClient
) -> tuple[list[str], list[str]]: ) -> tuple[list[str], list[str]]:
user_ids: list[str] = [] user_ids: list[str] = []
@ -318,57 +318,72 @@ def fetch_userids_from_emails(
return user_ids, failed_to_find return user_ids, failed_to_find
def fetch_userids_from_groups( def fetch_user_ids_from_groups(
group_names: list[str], client: WebClient given_names: list[str], client: WebClient
) -> tuple[list[str], list[str]]: ) -> tuple[list[str], list[str]]:
user_ids: list[str] = [] user_ids: list[str] = []
failed_to_find: list[str] = [] failed_to_find: list[str] = []
for group_name in group_names: try:
try: response = client.usergroups_list()
# First, find the group ID from the group name if not isinstance(response.data, dict):
response = client.usergroups_list() logger.error("Error fetching user groups")
groups = {group["name"]: group["id"] for group in response["usergroups"]} return user_ids, given_names
group_id = groups.get(group_name)
if group_id: all_group_data = response.data.get("usergroups", [])
# Fetch user IDs for the group name_id_map = {d["name"]: d["id"] for d in all_group_data}
handle_id_map = {d["handle"]: d["id"] for d in all_group_data}
for given_name in given_names:
group_id = name_id_map.get(given_name) or handle_id_map.get(
given_name.lstrip("@")
)
if not group_id:
failed_to_find.append(given_name)
continue
try:
response = client.usergroups_users_list(usergroup=group_id) response = client.usergroups_users_list(usergroup=group_id)
user_ids.extend(response["users"]) if isinstance(response.data, dict):
else: user_ids.extend(response.data.get("users", []))
failed_to_find.append(group_name) else:
except Exception as e: failed_to_find.append(given_name)
logger.error(f"Error fetching user IDs for group {group_name}: {str(e)}") except Exception as e:
failed_to_find.append(group_name) logger.error(f"Error fetching user group ids: {str(e)}")
failed_to_find.append(given_name)
except Exception as e:
logger.error(f"Error fetching user groups: {str(e)}")
failed_to_find = given_names
return user_ids, failed_to_find return user_ids, failed_to_find
def fetch_groupids_from_names( def fetch_group_ids_from_names(
names: list[str], client: WebClient given_names: list[str], client: WebClient
) -> tuple[list[str], list[str]]: ) -> tuple[list[str], list[str]]:
group_ids: set[str] = set() group_data: list[str] = []
failed_to_find: list[str] = [] failed_to_find: list[str] = []
try: try:
response = client.usergroups_list() response = client.usergroups_list()
if response.get("ok") and "usergroups" in response.data: if not isinstance(response.data, dict):
all_groups_dicts = response.data["usergroups"] # type: ignore
name_id_map = {d["name"]: d["id"] for d in all_groups_dicts}
handle_id_map = {d["handle"]: d["id"] for d in all_groups_dicts}
for group in names:
if group in name_id_map:
group_ids.add(name_id_map[group])
elif group in handle_id_map:
group_ids.add(handle_id_map[group])
else:
failed_to_find.append(group)
else:
# Most likely a Slack App scope issue
logger.error("Error fetching user groups") logger.error("Error fetching user groups")
return group_data, given_names
all_group_data = response.data.get("usergroups", [])
name_id_map = {d["name"]: d["id"] for d in all_group_data}
handle_id_map = {d["handle"]: d["id"] for d in all_group_data}
for given_name in given_names:
id = handle_id_map.get(given_name.lstrip("@"))
id = id or name_id_map.get(given_name)
if id:
group_data.append(id)
else:
failed_to_find.append(given_name)
except Exception as e: except Exception as e:
failed_to_find = given_names
logger.error(f"Error fetching user groups: {str(e)}") logger.error(f"Error fetching user groups: {str(e)}")
return list(group_ids), failed_to_find return group_data, failed_to_find
def fetch_user_semantic_id_from_id( def fetch_user_semantic_id_from_id(

View File

@ -1121,8 +1121,7 @@ class ChannelConfig(TypedDict):
channel_names: list[str] channel_names: list[str]
respond_tag_only: NotRequired[bool] # defaults to False respond_tag_only: NotRequired[bool] # defaults to False
respond_to_bots: NotRequired[bool] # defaults to False respond_to_bots: NotRequired[bool] # defaults to False
respond_team_member_list: NotRequired[list[str]] respond_member_group_list: NotRequired[list[str]]
respond_slack_group_list: NotRequired[list[str]]
answer_filters: NotRequired[list[AllowedAnswerFilters]] answer_filters: NotRequired[list[AllowedAnswerFilters]]
# If None then no follow up # If None then no follow up
# If empty list, follow up with no tags # If empty list, follow up with no tags

View File

@ -157,8 +157,7 @@ class SlackBotConfigCreationRequest(BaseModel):
respond_to_bots: bool = False respond_to_bots: bool = False
enable_auto_filters: bool = False enable_auto_filters: bool = False
# If no team members, assume respond in the channel to everyone # If no team members, assume respond in the channel to everyone
respond_team_member_list: list[str] = [] respond_member_group_list: list[str] = []
respond_slack_group_list: list[str] = []
answer_filters: list[AllowedAnswerFilters] = [] answer_filters: list[AllowedAnswerFilters] = []
# list of user emails # list of user emails
follow_up_tags: list[str] | None = None follow_up_tags: list[str] | None = None

View File

@ -34,11 +34,8 @@ def _form_channel_config(
) -> ChannelConfig: ) -> ChannelConfig:
raw_channel_names = slack_bot_config_creation_request.channel_names raw_channel_names = slack_bot_config_creation_request.channel_names
respond_tag_only = slack_bot_config_creation_request.respond_tag_only respond_tag_only = slack_bot_config_creation_request.respond_tag_only
respond_team_member_list = ( respond_member_group_list = (
slack_bot_config_creation_request.respond_team_member_list slack_bot_config_creation_request.respond_member_group_list
)
respond_slack_group_list = (
slack_bot_config_creation_request.respond_slack_group_list
) )
answer_filters = slack_bot_config_creation_request.answer_filters answer_filters = slack_bot_config_creation_request.answer_filters
follow_up_tags = slack_bot_config_creation_request.follow_up_tags follow_up_tags = slack_bot_config_creation_request.follow_up_tags
@ -61,7 +58,7 @@ def _form_channel_config(
detail=str(e), detail=str(e),
) )
if respond_tag_only and (respond_team_member_list or respond_slack_group_list): if respond_tag_only and respond_member_group_list:
raise ValueError( raise ValueError(
"Cannot set DanswerBot to only respond to tags only and " "Cannot set DanswerBot to only respond to tags only and "
"also respond to a predetermined set of users." "also respond to a predetermined set of users."
@ -72,10 +69,8 @@ def _form_channel_config(
} }
if respond_tag_only is not None: if respond_tag_only is not None:
channel_config["respond_tag_only"] = respond_tag_only channel_config["respond_tag_only"] = respond_tag_only
if respond_team_member_list: if respond_member_group_list:
channel_config["respond_team_member_list"] = respond_team_member_list channel_config["respond_member_group_list"] = respond_member_group_list
if respond_slack_group_list:
channel_config["respond_slack_group_list"] = respond_slack_group_list
if answer_filters: if answer_filters:
channel_config["answer_filters"] = answer_filters channel_config["answer_filters"] = answer_filters
if follow_up_tags is not None: if follow_up_tags is not None:

View File

@ -79,13 +79,9 @@ export const SlackBotCreationForm = ({
existingSlackBotConfig?.channel_config?.respond_to_bots || false, existingSlackBotConfig?.channel_config?.respond_to_bots || false,
enable_auto_filters: enable_auto_filters:
existingSlackBotConfig?.enable_auto_filters || false, existingSlackBotConfig?.enable_auto_filters || false,
respond_member_group_list: ( respond_member_group_list:
existingSlackBotConfig?.channel_config existingSlackBotConfig?.channel_config
?.respond_team_member_list ?? [] ?.respond_member_group_list ?? [],
).concat(
existingSlackBotConfig?.channel_config
?.respond_slack_group_list ?? []
),
still_need_help_enabled: still_need_help_enabled:
existingSlackBotConfig?.channel_config?.follow_up_tags !== existingSlackBotConfig?.channel_config?.follow_up_tags !==
undefined, undefined,
@ -133,14 +129,7 @@ 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_member_group_list.filter( respond_member_group_list: values.respond_member_group_list,
(teamMemberEmail) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(teamMemberEmail)
),
respond_slack_group_list: values.respond_member_group_list.filter(
(slackGroupName) =>
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(slackGroupName)
),
usePersona: usingPersonas, usePersona: usingPersonas,
standard_answer_categories: values.standard_answer_categories.map( standard_answer_categories: values.standard_answer_categories.map(
(category) => category.id (category) => category.id
@ -257,13 +246,13 @@ export const SlackBotCreationForm = ({
/> />
<TextArrayField <TextArrayField
name="respond_member_group_list" name="respond_member_group_list"
label="Team Member Emails Or Slack Group Names" label="Team Member Emails Or Slack Group Names/Handles"
subtext={`If specified, DanswerBot responses will only be subtext={`If specified, DanswerBot responses will only be
visible to the members or groups in this list. This is visible to the members or groups in this list. This is
useful if you want DanswerBot to operate in an useful if you want DanswerBot to operate in an
"assistant" mode, where it helps the team members find "assistant" mode, where it helps the team members find
answers, but let's them build on top of DanswerBot's response / throw answers, but let's them build on top of DanswerBot's response / throw
out the occasional incorrect answer. Group names are case sensitive.`} out the occasional incorrect answer. Group names and handles are case sensitive.`}
values={values} values={values}
/> />
<Divider /> <Divider />

View File

@ -14,8 +14,7 @@ interface SlackBotConfigCreationRequest {
questionmark_prefilter_enabled: boolean; questionmark_prefilter_enabled: boolean;
respond_tag_only: boolean; respond_tag_only: boolean;
respond_to_bots: boolean; respond_to_bots: boolean;
respond_team_member_list: string[]; respond_member_group_list: string[];
respond_slack_group_list: string[];
follow_up_tags?: string[]; follow_up_tags?: string[];
usePersona: boolean; usePersona: boolean;
response_type: SlackBotResponseType; response_type: SlackBotResponseType;
@ -43,8 +42,7 @@ const buildRequestBodyFromCreationRequest = (
respond_tag_only: creationRequest.respond_tag_only, respond_tag_only: creationRequest.respond_tag_only,
respond_to_bots: creationRequest.respond_to_bots, respond_to_bots: creationRequest.respond_to_bots,
enable_auto_filters: creationRequest.enable_auto_filters, enable_auto_filters: creationRequest.enable_auto_filters,
respond_team_member_list: creationRequest.respond_team_member_list, respond_member_group_list: creationRequest.respond_member_group_list,
respond_slack_group_list: creationRequest.respond_slack_group_list,
answer_filters: buildFiltersFromCreationRequest(creationRequest), answer_filters: buildFiltersFromCreationRequest(creationRequest),
follow_up_tags: creationRequest.follow_up_tags?.filter((tag) => tag !== ""), follow_up_tags: creationRequest.follow_up_tags?.filter((tag) => tag !== ""),
...(creationRequest.usePersona ...(creationRequest.usePersona

View File

@ -539,8 +539,7 @@ export interface ChannelConfig {
channel_names: string[]; channel_names: string[];
respond_tag_only?: boolean; respond_tag_only?: boolean;
respond_to_bots?: boolean; respond_to_bots?: boolean;
respond_team_member_list?: string[]; respond_member_group_list?: string[];
respond_slack_group_list?: string[];
answer_filters?: AnswerFilterOption[]; answer_filters?: AnswerFilterOption[];
follow_up_tags?: string[]; follow_up_tags?: string[];
} }