diff --git a/backend/alembic/versions/d716b0791ddd_combined_slack_id_fields.py b/backend/alembic/versions/d716b0791ddd_combined_slack_id_fields.py new file mode 100644 index 0000000000..3f13d7c556 --- /dev/null +++ b/backend/alembic/versions/d716b0791ddd_combined_slack_id_fields.py @@ -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 + ) + """ + ) diff --git a/backend/danswer/danswerbot/slack/blocks.py b/backend/danswer/danswerbot/slack/blocks.py index aaf2631885..4c7931a020 100644 --- a/backend/danswer/danswerbot/slack/blocks.py +++ b/backend/danswer/danswerbot/slack/blocks.py @@ -474,7 +474,7 @@ def build_follow_up_resolved_blocks( if tag_str: tag_str += " " - group_str = " ".join([f"" for group in group_ids]) + group_str = " ".join([f"" for group_id in group_ids]) if group_str: group_str += " " diff --git a/backend/danswer/danswerbot/slack/handlers/handle_buttons.py b/backend/danswer/danswerbot/slack/handlers/handle_buttons.py index 30c7015b96..b7b7bea9f4 100644 --- a/backend/danswer/danswerbot/slack/handlers/handle_buttons.py +++ b/backend/danswer/danswerbot/slack/handlers/handle_buttons.py @@ -29,8 +29,8 @@ from danswer.danswerbot.slack.handlers.handle_regular_answer import ( from danswer.danswerbot.slack.models import SlackMessageInfo from danswer.danswerbot.slack.utils import build_feedback_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_userids_from_emails +from danswer.danswerbot.slack.utils import fetch_group_ids_from_names +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_feedback_visibility 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.utils.logger import setup_logger -logger_base = setup_logger() +logger = setup_logger() def handle_doc_feedback_button( @@ -51,7 +51,7 @@ def handle_doc_feedback_button( client: SocketModeClient, ) -> None: 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 # Extracts the feedback_id coming from the 'source feedback' button @@ -134,7 +134,7 @@ def handle_generate_answer_button( receiver_ids=None, client=client.web_client, channel=channel_id, - logger=cast(logging.Logger, logger_base), + logger=cast(logging.Logger, logger), feedback_reminder_id=None, ) @@ -196,7 +196,7 @@ def handle_slack_feedback( feedback=feedback, ) 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 [ LIKE_BLOCK_ACTION_ID, @@ -260,11 +260,11 @@ def handle_followup_button( tag_names = slack_bot_config.channel_config.get("follow_up_tags") remaining = None if tag_names: - tag_ids, remaining = fetch_userids_from_emails( + tag_ids, remaining = fetch_user_ids_from_emails( tag_names, client.web_client ) 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) @@ -339,7 +339,7 @@ def handle_followup_resolved_button( ) if not response.get("ok"): - logger_base.error("Unable to delete message for resolved") + logger.error("Unable to delete message for resolved") if immediate: msg_text = f"{clicker_name} has marked this question as resolved!" diff --git a/backend/danswer/danswerbot/slack/handlers/handle_message.py b/backend/danswer/danswerbot/slack/handlers/handle_message.py index a05006dec1..cd97af9684 100644 --- a/backend/danswer/danswerbot/slack/handlers/handle_message.py +++ b/backend/danswer/danswerbot/slack/handlers/handle_message.py @@ -18,8 +18,8 @@ from danswer.danswerbot.slack.handlers.handle_standard_answers import ( ) from danswer.danswerbot.slack.models import SlackMessageInfo from danswer.danswerbot.slack.utils import ChannelIdAdapter -from danswer.danswerbot.slack.utils import fetch_userids_from_emails -from danswer.danswerbot.slack.utils import fetch_userids_from_groups +from danswer.danswerbot.slack.utils import fetch_user_ids_from_emails +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 slack_usage_report 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 - # 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_team_member_list = None - respond_slack_group_list = None + respond_member_group_list = None channel_conf = None 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_team_member_list = channel_conf.get("respond_team_member_list") or None - respond_slack_group_list = channel_conf.get("respond_slack_group_list") or None + respond_member_group_list = channel_conf.get("respond_member_group_list", None) if respond_tag_only and not bypass_filters: logger.info( @@ -194,17 +190,23 @@ def handle_message( ) return False - if respond_team_member_list: - send_to, _ = fetch_userids_from_emails(respond_team_member_list, client) - if respond_slack_group_list: - user_ids, _ = fetch_userids_from_groups(respond_slack_group_list, client) - send_to = (send_to + user_ids) if send_to else user_ids - if send_to: - send_to = list(set(send_to)) # remove duplicates + # List of user id to send message to, if None, send to everyone in channel + send_to: list[str] | None = None + missing_users: list[str] | None = None + if respond_member_group_list: + send_to, missing_ids = fetch_user_ids_from_emails( + respond_member_group_list, client + ) + + 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 # 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: respond_in_thread( client=client, diff --git a/backend/danswer/danswerbot/slack/utils.py b/backend/danswer/danswerbot/slack/utils.py index 1e5ffcc52f..b89b9f65f2 100644 --- a/backend/danswer/danswerbot/slack/utils.py +++ b/backend/danswer/danswerbot/slack/utils.py @@ -302,7 +302,7 @@ def get_channel_name_from_id( raise e -def fetch_userids_from_emails( +def fetch_user_ids_from_emails( user_emails: list[str], client: WebClient ) -> tuple[list[str], list[str]]: user_ids: list[str] = [] @@ -318,57 +318,72 @@ def fetch_userids_from_emails( return user_ids, failed_to_find -def fetch_userids_from_groups( - group_names: list[str], client: WebClient +def fetch_user_ids_from_groups( + given_names: list[str], client: WebClient ) -> tuple[list[str], list[str]]: user_ids: list[str] = [] failed_to_find: list[str] = [] - for group_name in group_names: - try: - # First, find the group ID from the group name - response = client.usergroups_list() - groups = {group["name"]: group["id"] for group in response["usergroups"]} - group_id = groups.get(group_name) + try: + response = client.usergroups_list() + if not isinstance(response.data, dict): + logger.error("Error fetching user groups") + return user_ids, given_names - if group_id: - # Fetch user IDs for the group + 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: + 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) - user_ids.extend(response["users"]) - else: - failed_to_find.append(group_name) - except Exception as e: - logger.error(f"Error fetching user IDs for group {group_name}: {str(e)}") - failed_to_find.append(group_name) + if isinstance(response.data, dict): + user_ids.extend(response.data.get("users", [])) + else: + failed_to_find.append(given_name) + except Exception as e: + 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 -def fetch_groupids_from_names( - names: list[str], client: WebClient +def fetch_group_ids_from_names( + given_names: list[str], client: WebClient ) -> tuple[list[str], list[str]]: - group_ids: set[str] = set() + group_data: list[str] = [] failed_to_find: list[str] = [] try: response = client.usergroups_list() - if response.get("ok") and "usergroups" in response.data: - 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 + if not isinstance(response.data, dict): 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: + failed_to_find = given_names 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( diff --git a/backend/danswer/db/models.py b/backend/danswer/db/models.py index f7d16743c6..3d33596a19 100644 --- a/backend/danswer/db/models.py +++ b/backend/danswer/db/models.py @@ -1121,8 +1121,7 @@ class ChannelConfig(TypedDict): channel_names: list[str] respond_tag_only: NotRequired[bool] # defaults to False respond_to_bots: NotRequired[bool] # defaults to False - respond_team_member_list: NotRequired[list[str]] - respond_slack_group_list: NotRequired[list[str]] + respond_member_group_list: NotRequired[list[str]] answer_filters: NotRequired[list[AllowedAnswerFilters]] # If None then no follow up # If empty list, follow up with no tags diff --git a/backend/danswer/server/manage/models.py b/backend/danswer/server/manage/models.py index f544df0f24..80a2728c6f 100644 --- a/backend/danswer/server/manage/models.py +++ b/backend/danswer/server/manage/models.py @@ -157,8 +157,7 @@ class SlackBotConfigCreationRequest(BaseModel): respond_to_bots: bool = False enable_auto_filters: bool = False # If no team members, assume respond in the channel to everyone - respond_team_member_list: list[str] = [] - respond_slack_group_list: list[str] = [] + respond_member_group_list: list[str] = [] answer_filters: list[AllowedAnswerFilters] = [] # list of user emails follow_up_tags: list[str] | None = None diff --git a/backend/danswer/server/manage/slack_bot.py b/backend/danswer/server/manage/slack_bot.py index d5f08e2694..b587badef0 100644 --- a/backend/danswer/server/manage/slack_bot.py +++ b/backend/danswer/server/manage/slack_bot.py @@ -34,11 +34,8 @@ def _form_channel_config( ) -> ChannelConfig: raw_channel_names = slack_bot_config_creation_request.channel_names respond_tag_only = slack_bot_config_creation_request.respond_tag_only - respond_team_member_list = ( - slack_bot_config_creation_request.respond_team_member_list - ) - respond_slack_group_list = ( - slack_bot_config_creation_request.respond_slack_group_list + respond_member_group_list = ( + slack_bot_config_creation_request.respond_member_group_list ) answer_filters = slack_bot_config_creation_request.answer_filters follow_up_tags = slack_bot_config_creation_request.follow_up_tags @@ -61,7 +58,7 @@ def _form_channel_config( 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( "Cannot set DanswerBot to only respond to tags only and " "also respond to a predetermined set of users." @@ -72,10 +69,8 @@ def _form_channel_config( } if respond_tag_only is not None: channel_config["respond_tag_only"] = respond_tag_only - if respond_team_member_list: - channel_config["respond_team_member_list"] = respond_team_member_list - if respond_slack_group_list: - channel_config["respond_slack_group_list"] = respond_slack_group_list + if respond_member_group_list: + channel_config["respond_member_group_list"] = respond_member_group_list if answer_filters: channel_config["answer_filters"] = answer_filters if follow_up_tags is not None: diff --git a/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx b/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx index 23968d585f..881076eb41 100644 --- a/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx +++ b/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx @@ -79,13 +79,9 @@ export const SlackBotCreationForm = ({ existingSlackBotConfig?.channel_config?.respond_to_bots || false, enable_auto_filters: existingSlackBotConfig?.enable_auto_filters || false, - respond_member_group_list: ( + respond_member_group_list: existingSlackBotConfig?.channel_config - ?.respond_team_member_list ?? [] - ).concat( - existingSlackBotConfig?.channel_config - ?.respond_slack_group_list ?? [] - ), + ?.respond_member_group_list ?? [], still_need_help_enabled: existingSlackBotConfig?.channel_config?.follow_up_tags !== undefined, @@ -133,14 +129,7 @@ export const SlackBotCreationForm = ({ channel_names: values.channel_names.filter( (channelName) => channelName !== "" ), - respond_team_member_list: values.respond_member_group_list.filter( - (teamMemberEmail) => - /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(teamMemberEmail) - ), - respond_slack_group_list: values.respond_member_group_list.filter( - (slackGroupName) => - !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(slackGroupName) - ), + respond_member_group_list: values.respond_member_group_list, usePersona: usingPersonas, standard_answer_categories: values.standard_answer_categories.map( (category) => category.id @@ -257,13 +246,13 @@ export const SlackBotCreationForm = ({ /> diff --git a/web/src/app/admin/bot/lib.ts b/web/src/app/admin/bot/lib.ts index b3d6ec678b..c2d2b29150 100644 --- a/web/src/app/admin/bot/lib.ts +++ b/web/src/app/admin/bot/lib.ts @@ -14,8 +14,7 @@ interface SlackBotConfigCreationRequest { questionmark_prefilter_enabled: boolean; respond_tag_only: boolean; respond_to_bots: boolean; - respond_team_member_list: string[]; - respond_slack_group_list: string[]; + respond_member_group_list: string[]; follow_up_tags?: string[]; usePersona: boolean; response_type: SlackBotResponseType; @@ -43,8 +42,7 @@ const buildRequestBodyFromCreationRequest = ( respond_tag_only: creationRequest.respond_tag_only, respond_to_bots: creationRequest.respond_to_bots, enable_auto_filters: creationRequest.enable_auto_filters, - respond_team_member_list: creationRequest.respond_team_member_list, - respond_slack_group_list: creationRequest.respond_slack_group_list, + respond_member_group_list: creationRequest.respond_member_group_list, answer_filters: buildFiltersFromCreationRequest(creationRequest), follow_up_tags: creationRequest.follow_up_tags?.filter((tag) => tag !== ""), ...(creationRequest.usePersona diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index a507e1fa5b..60cb664b07 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -539,8 +539,7 @@ export interface ChannelConfig { channel_names: string[]; respond_tag_only?: boolean; respond_to_bots?: boolean; - respond_team_member_list?: string[]; - respond_slack_group_list?: string[]; + respond_member_group_list?: string[]; answer_filters?: AnswerFilterOption[]; follow_up_tags?: string[]; }