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:
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:
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.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!"

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = ({
/>
<TextArrayField
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
visible to the members or groups 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. Group names are case sensitive.`}
out the occasional incorrect answer. Group names and handles are case sensitive.`}
values={values}
/>
<Divider />

View File

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

View File

@ -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[];
}