Tool to retrieve Slack messages from a DM conversation with a given username (#241)
Currently, retrieving DMs with a given username requires several actions: first get the current user's ID; list all users and find the ID of the username; then scan all DM conversations and find the one with the current user's ID and the username's ID, to finally retrieve the messages using that conversation ID. This tool abstracts all that in a single call. PS: we'll implement a similar tool for multi-person DM conversations in a subsequent PR.
This commit is contained in:
parent
df969d9d73
commit
a00bd4734e
8 changed files with 1128 additions and 1173 deletions
|
|
@ -12,3 +12,18 @@ class PaginationTimeoutError(SlackToolkitError):
|
|||
|
||||
class ItemNotFoundError(SlackToolkitError):
|
||||
"""Raised when an item is not found."""
|
||||
|
||||
|
||||
class UsernameNotFoundError(SlackToolkitError):
|
||||
"""Raised when a user is not found by the username searched"""
|
||||
|
||||
def __init__(self, usernames_found: list[str]) -> None:
|
||||
self.usernames_found = usernames_found
|
||||
|
||||
|
||||
class ConversationNotFoundError(SlackToolkitError):
|
||||
"""Raised when a conversation is not found"""
|
||||
|
||||
|
||||
class DirectMessageConversationNotFoundError(ConversationNotFoundError):
|
||||
"""Raised when a direct message conversation searched is not found"""
|
||||
|
|
|
|||
|
|
@ -9,9 +9,15 @@ from slack_sdk.errors import SlackApiError
|
|||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from arcade_slack.constants import MAX_PAGINATION_TIMEOUT_SECONDS
|
||||
from arcade_slack.exceptions import ItemNotFoundError
|
||||
from arcade_slack.models import ConversationType, SlackUserList
|
||||
from arcade_slack.tools.users import get_user_info_by_id
|
||||
from arcade_slack.exceptions import (
|
||||
ItemNotFoundError,
|
||||
UsernameNotFoundError,
|
||||
)
|
||||
from arcade_slack.models import (
|
||||
ConversationType,
|
||||
SlackUserList,
|
||||
)
|
||||
from arcade_slack.tools.users import get_user_info_by_id, list_users
|
||||
from arcade_slack.utils import (
|
||||
async_paginate,
|
||||
convert_conversation_type_to_slack_name,
|
||||
|
|
@ -21,6 +27,8 @@ from arcade_slack.utils import (
|
|||
extract_conversation_metadata,
|
||||
format_conversations_as_csv,
|
||||
format_users,
|
||||
get_user_by_username,
|
||||
retrieve_conversations_by_user_ids,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -218,20 +226,18 @@ async def get_members_in_conversation_by_id(
|
|||
],
|
||||
)
|
||||
)
|
||||
async def get_members_in_conversation_by_name(
|
||||
async def get_members_in_channel_by_name(
|
||||
context: ToolContext,
|
||||
conversation_name: Annotated[str, "The name of the conversation to get members for"],
|
||||
channel_name: Annotated[str, "The name of the channel to get members for"],
|
||||
limit: Annotated[Optional[int], "The maximum number of members to return."] = None,
|
||||
next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None,
|
||||
) -> Annotated[dict, "The conversation members' IDs and Names"]:
|
||||
) -> Annotated[dict, "The channel members' IDs and Names"]:
|
||||
"""Get the members of a conversation in Slack by the conversation's name."""
|
||||
conversation_metadata = await get_conversation_metadata_by_name(
|
||||
context=context, conversation_name=conversation_name, next_cursor=next_cursor
|
||||
)
|
||||
channel = await get_channel_metadata_by_name(context=context, channel_name=channel_name)
|
||||
|
||||
return await get_members_in_conversation_by_id( # type: ignore[no-any-return]
|
||||
context=context,
|
||||
conversation_id=conversation_metadata["id"],
|
||||
conversation_id=channel["id"],
|
||||
limit=limit,
|
||||
next_cursor=next_cursor,
|
||||
)
|
||||
|
|
@ -433,12 +439,84 @@ async def get_messages_in_channel_by_name(
|
|||
'latest_relative'.
|
||||
|
||||
Leave all arguments with the default None to get messages without date/time filtering"""
|
||||
conversation_metadata = await get_conversation_metadata_by_name(
|
||||
context=context, conversation_name=channel_name
|
||||
)
|
||||
channel = await get_channel_metadata_by_name(context=context, channel_name=channel_name)
|
||||
|
||||
return await get_messages_in_conversation_by_id( # type: ignore[no-any-return]
|
||||
context=context,
|
||||
conversation_id=conversation_metadata["id"],
|
||||
conversation_id=channel["id"],
|
||||
oldest_relative=oldest_relative,
|
||||
latest_relative=latest_relative,
|
||||
oldest_datetime=oldest_datetime,
|
||||
latest_datetime=latest_datetime,
|
||||
limit=limit,
|
||||
next_cursor=next_cursor,
|
||||
)
|
||||
|
||||
|
||||
@tool(requires_auth=Slack(scopes=["im:history", "im:read"]))
|
||||
async def get_messages_in_direct_message_conversation_by_username(
|
||||
context: ToolContext,
|
||||
username: Annotated[str, "The username of the user to get messages from"],
|
||||
oldest_relative: Annotated[
|
||||
Optional[str],
|
||||
(
|
||||
"The oldest message to include in the results, specified as a time offset from the "
|
||||
"current time in the format 'DD:HH:MM'"
|
||||
),
|
||||
] = None,
|
||||
latest_relative: Annotated[
|
||||
Optional[str],
|
||||
(
|
||||
"The latest message to include in the results, specified as a time offset from the "
|
||||
"current time in the format 'DD:HH:MM'"
|
||||
),
|
||||
] = None,
|
||||
oldest_datetime: Annotated[
|
||||
Optional[str],
|
||||
(
|
||||
"The oldest message to include in the results, specified as a datetime object in the "
|
||||
"format 'YYYY-MM-DD HH:MM:SS'"
|
||||
),
|
||||
] = None,
|
||||
latest_datetime: Annotated[
|
||||
Optional[str],
|
||||
(
|
||||
"The latest message to include in the results, specified as a datetime object in the "
|
||||
"format 'YYYY-MM-DD HH:MM:SS'"
|
||||
),
|
||||
] = None,
|
||||
limit: Annotated[Optional[int], "The maximum number of messages to return."] = None,
|
||||
next_cursor: Annotated[Optional[str], "The cursor to use for pagination."] = None,
|
||||
) -> Annotated[
|
||||
dict,
|
||||
(
|
||||
"The messages in a direct message conversation and next cursor for paginating results "
|
||||
"(when there are additional messages to retrieve)."
|
||||
),
|
||||
]:
|
||||
"""Get the messages in a direct conversation by the user's name.
|
||||
|
||||
To filter messages by an absolute datetime, use 'oldest_datetime' and/or 'latest_datetime'. If
|
||||
only 'oldest_datetime' is provided, it will return messages from the oldest_datetime to the
|
||||
current time. If only 'latest_datetime' is provided, it will return messages since the
|
||||
beginning of the conversation to the latest_datetime.
|
||||
|
||||
To filter messages by a relative datetime (e.g. 3 days ago, 1 hour ago, etc.), use
|
||||
'oldest_relative' and/or 'latest_relative'. If only 'oldest_relative' is provided, it will
|
||||
return messages from the oldest_relative to the current time. If only 'latest_relative' is
|
||||
provided, it will return messages from the current time to the latest_relative.
|
||||
|
||||
Do not provide both 'oldest_datetime' and 'oldest_relative' or both 'latest_datetime' and
|
||||
'latest_relative'.
|
||||
|
||||
Leave all arguments with the default None to get messages without date/time filtering"""
|
||||
direct_conversation = await get_direct_message_conversation_metadata_by_username(
|
||||
context=context, username=username
|
||||
)
|
||||
|
||||
return await get_messages_in_conversation_by_id( # type: ignore[no-any-return]
|
||||
context=context,
|
||||
conversation_id=direct_conversation["id"],
|
||||
oldest_relative=oldest_relative,
|
||||
latest_relative=latest_relative,
|
||||
oldest_datetime=oldest_datetime,
|
||||
|
|
@ -490,39 +568,33 @@ async def get_conversation_metadata_by_id(
|
|||
return dict(**extract_conversation_metadata(response["channel"]))
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Slack(
|
||||
scopes=["channels:read", "groups:read", "im:read", "mpim:read"],
|
||||
)
|
||||
)
|
||||
async def get_conversation_metadata_by_name(
|
||||
@tool(requires_auth=Slack(scopes=["channels:read"]))
|
||||
async def get_channel_metadata_by_name(
|
||||
context: ToolContext,
|
||||
conversation_name: Annotated[str, "The name of the conversation to get metadata for"],
|
||||
channel_name: Annotated[str, "The name of the channel to get metadata for"],
|
||||
next_cursor: Annotated[
|
||||
Optional[str],
|
||||
"The cursor to use for pagination, if continuing from a previous search.",
|
||||
] = None,
|
||||
) -> Annotated[dict, "The conversation metadata"]:
|
||||
"""Get the metadata of a conversation in Slack searching by its name."""
|
||||
conversation_names: list[str] = []
|
||||
) -> Annotated[dict, "The channel metadata"]:
|
||||
"""Get the metadata of a channel in Slack searching by its name."""
|
||||
channel_names: list[str] = []
|
||||
|
||||
async def find_conversation() -> dict:
|
||||
nonlocal conversation_names, conversation_name, next_cursor
|
||||
async def find_channel() -> dict:
|
||||
nonlocal channel_names, channel_name, next_cursor
|
||||
should_continue = True
|
||||
|
||||
while should_continue:
|
||||
response = await list_conversations_metadata(context, next_cursor=next_cursor)
|
||||
next_cursor = response["next_cursor"]
|
||||
next_cursor = response.get("next_cursor")
|
||||
|
||||
for conversation in response["conversations"]:
|
||||
response_conversation_name = (
|
||||
""
|
||||
if not isinstance(conversation.get("name"), str)
|
||||
else conversation["name"].lower()
|
||||
for channel in response["conversations"]:
|
||||
response_channel_name = (
|
||||
"" if not isinstance(channel.get("name"), str) else channel["name"].lower()
|
||||
)
|
||||
if response_conversation_name == conversation_name.lower():
|
||||
return conversation # type: ignore[no-any-return]
|
||||
conversation_names.append(conversation["name"])
|
||||
if response_channel_name == channel_name.lower():
|
||||
return channel # type: ignore[no-any-return]
|
||||
channel_names.append(channel["name"])
|
||||
|
||||
if not next_cursor:
|
||||
should_continue = False
|
||||
|
|
@ -530,31 +602,80 @@ async def get_conversation_metadata_by_name(
|
|||
raise ItemNotFoundError()
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(find_conversation(), timeout=MAX_PAGINATION_TIMEOUT_SECONDS)
|
||||
return await asyncio.wait_for(find_channel(), timeout=MAX_PAGINATION_TIMEOUT_SECONDS)
|
||||
except ItemNotFoundError:
|
||||
raise RetryableToolError(
|
||||
"Conversation not found",
|
||||
developer_message=f"Conversation with name '{conversation_name}' not found.",
|
||||
additional_prompt_content=f"Available conversation names: {conversation_names}",
|
||||
"Channel not found",
|
||||
developer_message=f"Channel with name '{channel_name}' not found.",
|
||||
additional_prompt_content=f"Available channel names: {channel_names}",
|
||||
retry_after_ms=500,
|
||||
)
|
||||
except TimeoutError:
|
||||
raise RetryableToolError(
|
||||
"Conversation not found, search timed out.",
|
||||
"Channel not found, search timed out.",
|
||||
developer_message=(
|
||||
f"Conversation with name '{conversation_name}' not found. "
|
||||
f"Channel with name '{channel_name}' not found. "
|
||||
f"Search timed out after {MAX_PAGINATION_TIMEOUT_SECONDS} seconds."
|
||||
),
|
||||
additional_prompt_content=(
|
||||
f"Other conversation names found are: {conversation_names}. "
|
||||
f"Other channel names found are: {channel_names}. "
|
||||
"The list is potentially non-exhaustive, since the search process timed out. "
|
||||
f"Use the '{list_conversations_metadata.__name__}' tool to get a comprehensive "
|
||||
"list of conversations."
|
||||
f"Use the '{list_conversations_metadata.__tool_name__}' tool to get"
|
||||
"a comprehensive list of channels."
|
||||
),
|
||||
retry_after_ms=500,
|
||||
)
|
||||
|
||||
|
||||
@tool(requires_auth=Slack(scopes=["im:read"]))
|
||||
async def get_direct_message_conversation_metadata_by_username(
|
||||
context: ToolContext,
|
||||
username: Annotated[str, "The username of the user/person to get messages with"],
|
||||
next_cursor: Annotated[
|
||||
Optional[str],
|
||||
"The cursor to use for pagination, if continuing from a previous search.",
|
||||
] = None,
|
||||
) -> Annotated[
|
||||
Optional[dict],
|
||||
"The direct message conversation metadata.",
|
||||
]:
|
||||
"""Get the metadata of a direct message conversation in Slack by the username."""
|
||||
try:
|
||||
token = (
|
||||
context.authorization.token
|
||||
if context.authorization and context.authorization.token
|
||||
else ""
|
||||
)
|
||||
slack_client = AsyncWebClient(token=token)
|
||||
|
||||
current_user, list_users_response = await asyncio.gather(
|
||||
slack_client.auth_test(), list_users(context)
|
||||
)
|
||||
|
||||
other_user = get_user_by_username(username, list_users_response["users"])
|
||||
|
||||
conversations_found = await retrieve_conversations_by_user_ids(
|
||||
list_conversations_func=list_conversations_metadata,
|
||||
get_members_in_conversation_func=get_members_in_conversation_by_id,
|
||||
context=context,
|
||||
conversation_types=[ConversationType.DIRECT_MESSAGE],
|
||||
user_ids=[current_user["user_id"], other_user["id"]],
|
||||
exact_match=True,
|
||||
limit=1,
|
||||
next_cursor=next_cursor,
|
||||
)
|
||||
|
||||
return None if not conversations_found else conversations_found[0]
|
||||
|
||||
except UsernameNotFoundError as e:
|
||||
raise RetryableToolError(
|
||||
f"Username '{username}' not found",
|
||||
developer_message=f"User with username '{username}' not found.",
|
||||
additional_prompt_content=f"Available users: {e.usernames_found}",
|
||||
retry_after_ms=500,
|
||||
)
|
||||
|
||||
|
||||
@tool(
|
||||
requires_auth=Slack(
|
||||
scopes=["channels:read", "groups:read", "im:read", "mpim:read"],
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@ import asyncio
|
|||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from arcade.sdk import ToolContext
|
||||
from arcade.sdk.errors import RetryableToolError
|
||||
|
||||
from arcade_slack.constants import MAX_PAGINATION_SIZE_LIMIT, MAX_PAGINATION_TIMEOUT_SECONDS
|
||||
from arcade_slack.custom_types import SlackPaginationNextCursor
|
||||
from arcade_slack.exceptions import PaginationTimeoutError
|
||||
from arcade_slack.exceptions import (
|
||||
PaginationTimeoutError,
|
||||
UsernameNotFoundError,
|
||||
)
|
||||
from arcade_slack.models import (
|
||||
BasicUserInfo,
|
||||
ConversationMetadata,
|
||||
|
|
@ -90,6 +94,18 @@ def get_slack_conversation_type_as_str(channel: SlackConversation) -> str:
|
|||
raise ValueError(f"Invalid conversation type in channel {channel.get('name')}")
|
||||
|
||||
|
||||
def get_user_by_username(username: str, users_list: list[dict]) -> SlackUser:
|
||||
usernames_found = []
|
||||
for user in users_list:
|
||||
if isinstance(user.get("name"), str):
|
||||
usernames_found.append(user["name"])
|
||||
username_found = user.get("name") or ""
|
||||
if username.lower() == username_found.lower():
|
||||
return SlackUser(**user)
|
||||
|
||||
raise UsernameNotFoundError(usernames_found)
|
||||
|
||||
|
||||
def convert_conversation_type_to_slack_name(
|
||||
conversation_type: ConversationType,
|
||||
) -> ConversationTypeSlackName:
|
||||
|
|
@ -174,6 +190,93 @@ def extract_basic_user_info(user_info: SlackUser) -> BasicUserInfo:
|
|||
)
|
||||
|
||||
|
||||
async def associate_members_of_multiple_conversations(
|
||||
get_members_in_conversation_func: Callable,
|
||||
conversations: list[dict],
|
||||
context: ToolContext,
|
||||
) -> list[dict]:
|
||||
"""Associate members to each conversation, returning the updated list."""
|
||||
return await asyncio.gather(*[
|
||||
associate_members_of_conversation(get_members_in_conversation_func, context, conv)
|
||||
for conv in conversations
|
||||
])
|
||||
|
||||
|
||||
async def associate_members_of_conversation(
|
||||
get_members_in_conversation_func: Callable,
|
||||
context: ToolContext,
|
||||
conversation: dict,
|
||||
) -> dict:
|
||||
response = await get_members_in_conversation_func(context, conversation["id"])
|
||||
conversation["members"] = response["members"]
|
||||
return conversation
|
||||
|
||||
|
||||
async def retrieve_conversations_by_user_ids(
|
||||
list_conversations_func: Callable,
|
||||
get_members_in_conversation_func: Callable,
|
||||
context: ToolContext,
|
||||
conversation_types: list[ConversationType],
|
||||
user_ids: list[str],
|
||||
exact_match: bool = False,
|
||||
limit: Optional[int] = None,
|
||||
next_cursor: Optional[str] = None,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Retrieve conversations filtered by the given user IDs. Includes pagination support
|
||||
and optionally limits the number of returned conversations.
|
||||
"""
|
||||
conversations_found: list[dict] = []
|
||||
|
||||
response = await list_conversations_func(
|
||||
context=context,
|
||||
conversation_types=conversation_types,
|
||||
next_cursor=next_cursor,
|
||||
)
|
||||
|
||||
# Associate members to each conversation
|
||||
conversations_with_members = await associate_members_of_multiple_conversations(
|
||||
get_members_in_conversation_func, response["conversations"], context
|
||||
)
|
||||
|
||||
conversations_found.extend(
|
||||
filter_conversations_by_user_ids(conversations_with_members, user_ids, exact_match)
|
||||
)
|
||||
|
||||
return conversations_found[:limit]
|
||||
|
||||
|
||||
def filter_conversations_by_user_ids(
|
||||
conversations: list[dict],
|
||||
user_ids: list[str],
|
||||
exact_match: bool = False,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Filter conversations by the members' user IDs.
|
||||
|
||||
Args:
|
||||
conversations: The list of conversations to filter.
|
||||
user_ids: The user IDs to filter conversations for.
|
||||
exact_match: Whether to match the exact number of members in the conversations.
|
||||
|
||||
Returns:
|
||||
The list of conversations found.
|
||||
"""
|
||||
matches = []
|
||||
for conversation in conversations:
|
||||
member_ids = [member["id"] for member in conversation["members"]]
|
||||
if exact_match:
|
||||
same_length = len(user_ids) == len(member_ids)
|
||||
has_all_members = all(user_id in member_ids for user_id in user_ids)
|
||||
if same_length and has_all_members:
|
||||
matches.append(conversation)
|
||||
else:
|
||||
if all(user_id in member_ids for user_id in user_ids):
|
||||
matches.append(conversation)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def is_user_a_bot(user: SlackUser) -> bool:
|
||||
"""Check if a Slack user represents a bot.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,3 +6,15 @@ from arcade.sdk import ToolAuthorizationContext, ToolContext
|
|||
def mock_context():
|
||||
mock_auth = ToolAuthorizationContext(token="fake-token") # noqa: S106
|
||||
return ToolContext(authorization=mock_auth)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_chat_slack_client(mocker):
|
||||
mock_client = mocker.patch("arcade_slack.tools.chat.AsyncWebClient", autospec=True)
|
||||
return mock_client.return_value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_users_slack_client(mocker):
|
||||
mock_client = mocker.patch("arcade_slack.tools.users.AsyncWebClient", autospec=True)
|
||||
return mock_client.return_value
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@ from arcade.sdk.eval import (
|
|||
import arcade_slack
|
||||
from arcade_slack.critics import RelativeTimeBinaryCritic
|
||||
from arcade_slack.tools.chat import (
|
||||
get_channel_metadata_by_name,
|
||||
get_conversation_metadata_by_id,
|
||||
get_conversation_metadata_by_name,
|
||||
get_direct_message_conversation_metadata_by_username,
|
||||
get_members_in_channel_by_name,
|
||||
get_members_in_conversation_by_id,
|
||||
get_members_in_conversation_by_name,
|
||||
get_messages_in_channel_by_name,
|
||||
get_messages_in_conversation_by_id,
|
||||
get_messages_in_direct_message_conversation_by_username,
|
||||
list_conversations_metadata,
|
||||
list_direct_message_conversations_metadata,
|
||||
list_group_direct_message_conversations_metadata,
|
||||
|
|
@ -433,19 +435,19 @@ def get_conversations_metadata_eval_suite() -> EvalSuite:
|
|||
"""Create an evaluation suite for tools getting conversations metadata."""
|
||||
suite = EvalSuite(
|
||||
name="Slack Tools Evaluation",
|
||||
system_message="You are an AI assistant that can interact with Slack to send messages and get information from conversations, users, etc.",
|
||||
system_message="You are an AI assistant that can interact with Slack to get information from conversations, users, etc.",
|
||||
catalog=catalog,
|
||||
rubric=rubric,
|
||||
)
|
||||
|
||||
suite.add_case(
|
||||
name="Get conversation metadata by name",
|
||||
name="Get channel metadata by name",
|
||||
user_message="Get the metadata of the #general channel",
|
||||
expected_tool_calls=[
|
||||
ExpectedToolCall(
|
||||
func=get_conversation_metadata_by_name,
|
||||
func=get_channel_metadata_by_name,
|
||||
args={
|
||||
"conversation_name": "general",
|
||||
"channel_name": "general",
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
@ -470,6 +472,26 @@ def get_conversations_metadata_eval_suite() -> EvalSuite:
|
|||
],
|
||||
)
|
||||
|
||||
get_metadata_by_username_user_messages = [
|
||||
"get the metadata of the direct message conversation with the user 'jane.doe'"
|
||||
"get data about my private conversation with the user 'jane.doe'",
|
||||
"get data about my IM conversation with the 'jane.doe'",
|
||||
]
|
||||
|
||||
for i, user_message in enumerate(get_metadata_by_username_user_messages):
|
||||
suite.add_case(
|
||||
name=f"Get direct message conversation metadata by username {i}",
|
||||
user_message=user_message,
|
||||
expected_tool_calls=[
|
||||
ExpectedToolCall(
|
||||
func=get_direct_message_conversation_metadata_by_username,
|
||||
args={
|
||||
"username": "jane.doe",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
return suite
|
||||
|
||||
|
||||
|
|
@ -496,13 +518,13 @@ def get_conversations_members_eval_suite() -> EvalSuite:
|
|||
|
||||
for user_message in user_messages:
|
||||
suite.add_case(
|
||||
name=f"Get conversation members by name: {user_message}",
|
||||
name=f"Get channel members by name: {user_message}",
|
||||
user_message=user_message,
|
||||
expected_tool_calls=[
|
||||
ExpectedToolCall(
|
||||
func=get_members_in_conversation_by_name,
|
||||
func=get_members_in_channel_by_name,
|
||||
args={
|
||||
"conversation_name": "general",
|
||||
"channel_name": "general",
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
@ -531,8 +553,8 @@ def get_conversations_members_eval_suite() -> EvalSuite:
|
|||
|
||||
|
||||
@tool_eval()
|
||||
def get_conversation_history_eval_suite() -> EvalSuite:
|
||||
"""Create an evaluation suite for tools getting conversations history."""
|
||||
def get_messages_in_channel_eval_suite() -> EvalSuite:
|
||||
"""Create an evaluation suite for tools getting messages in channels."""
|
||||
suite = EvalSuite(
|
||||
name="Slack Chat Tools Evaluation",
|
||||
system_message="You are an AI assistant that can interact with Slack to send messages and get information from conversations, users, etc.",
|
||||
|
|
@ -540,16 +562,16 @@ def get_conversation_history_eval_suite() -> EvalSuite:
|
|||
rubric=rubric,
|
||||
)
|
||||
|
||||
no_arguments_user_messages_by_conversation_name = [
|
||||
"Get the history of the #general channel",
|
||||
"Get the history of the general channel",
|
||||
no_arguments_user_messages_by_channel_name = [
|
||||
"what are the latest messages in the #general channel",
|
||||
"show me the messages in the general channel",
|
||||
"list the messages in the #general channel",
|
||||
"list the messages in the general channel",
|
||||
]
|
||||
|
||||
for user_message in no_arguments_user_messages_by_conversation_name:
|
||||
for i, user_message in enumerate(no_arguments_user_messages_by_channel_name):
|
||||
suite.add_case(
|
||||
name=f"Get conversation history by name: '{user_message}'",
|
||||
name=f"Get messages in conversation by name {i}: '{user_message}'",
|
||||
user_message=user_message,
|
||||
expected_tool_calls=[
|
||||
ExpectedToolCall(
|
||||
|
|
@ -985,3 +1007,82 @@ def get_conversation_history_eval_suite() -> EvalSuite:
|
|||
)
|
||||
|
||||
return suite
|
||||
|
||||
|
||||
@tool_eval()
|
||||
def get_messages_in_direct_message_eval_suite() -> EvalSuite:
|
||||
"""Create an evaluation suite for tools getting messages in direct messages."""
|
||||
suite = EvalSuite(
|
||||
name="Slack Chat Tools Evaluation",
|
||||
system_message="You are an AI assistant that can interact with Slack to send messages and get information from conversations, users, etc.",
|
||||
catalog=catalog,
|
||||
rubric=rubric,
|
||||
)
|
||||
|
||||
no_arguments_user_messages_by_username = [
|
||||
"what are the latest messages I exchanged with jane.doe",
|
||||
"show my messages with jane.doe on Slack",
|
||||
"list the messages I exchanged with jane.doe",
|
||||
"list the message history with jane.doe",
|
||||
]
|
||||
|
||||
for i, user_message in enumerate(no_arguments_user_messages_by_username):
|
||||
suite.add_case(
|
||||
name=f"{user_message} [{i}]",
|
||||
user_message=user_message,
|
||||
expected_tool_calls=[
|
||||
ExpectedToolCall(
|
||||
func=get_messages_in_direct_message_conversation_by_username,
|
||||
args={
|
||||
"username": "jane.doe",
|
||||
},
|
||||
),
|
||||
],
|
||||
critics=[
|
||||
BinaryCritic(critic_field="username", weight=1.0),
|
||||
],
|
||||
)
|
||||
|
||||
suite.add_case(
|
||||
name="get messages in direct conversation by username (on a specific date)",
|
||||
user_message="get the messages I exchanged with jane.doe on 2025-01-31",
|
||||
expected_tool_calls=[
|
||||
ExpectedToolCall(
|
||||
func=get_messages_in_direct_message_conversation_by_username,
|
||||
args={
|
||||
"username": "jane.doe",
|
||||
"oldest_datetime": "2025-01-31 00:00:00",
|
||||
"latest_datetime": "2025-01-31 23:59:59",
|
||||
},
|
||||
),
|
||||
],
|
||||
critics=[
|
||||
BinaryCritic(critic_field="username", weight=1 / 3),
|
||||
DatetimeCritic(
|
||||
critic_field="oldest_datetime", weight=1 / 3, max_difference=timedelta(minutes=2)
|
||||
),
|
||||
DatetimeCritic(
|
||||
critic_field="latest_datetime", weight=1 / 3, max_difference=timedelta(minutes=2)
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
suite.add_case(
|
||||
name="Get conversation history oldest relative by username (2 days ago)",
|
||||
user_message="Get the messages I exchanged with jane.doe starting 2 days ago",
|
||||
expected_tool_calls=[
|
||||
ExpectedToolCall(
|
||||
func=get_messages_in_direct_message_conversation_by_username,
|
||||
args={
|
||||
"username": "jane.doe",
|
||||
"oldest_relative": "02:00:00",
|
||||
},
|
||||
),
|
||||
],
|
||||
critics=[
|
||||
BinaryCritic(critic_field="username", weight=0.5),
|
||||
RelativeTimeBinaryCritic(critic_field="oldest_relative", weight=0.5),
|
||||
],
|
||||
)
|
||||
|
||||
return suite
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -10,12 +10,14 @@ from slack_sdk.web.async_slack_response import AsyncSlackResponse
|
|||
from arcade_slack.constants import MAX_PAGINATION_SIZE_LIMIT
|
||||
from arcade_slack.models import ConversationType, ConversationTypeSlackName
|
||||
from arcade_slack.tools.chat import (
|
||||
get_channel_metadata_by_name,
|
||||
get_conversation_metadata_by_id,
|
||||
get_conversation_metadata_by_name,
|
||||
get_direct_message_conversation_metadata_by_username,
|
||||
get_members_in_channel_by_name,
|
||||
get_members_in_conversation_by_id,
|
||||
get_members_in_conversation_by_name,
|
||||
get_messages_in_channel_by_name,
|
||||
get_messages_in_conversation_by_id,
|
||||
get_messages_in_direct_message_conversation_by_username,
|
||||
list_conversations_metadata,
|
||||
list_direct_message_conversations_metadata,
|
||||
list_group_direct_message_conversations_metadata,
|
||||
|
|
@ -37,37 +39,31 @@ def mock_channel_info() -> dict:
|
|||
return {"name": "general", "id": "C12345", "is_member": True, "is_channel": True}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_slack_client(mocker):
|
||||
mock_client = mocker.patch("arcade_slack.tools.chat.AsyncWebClient", autospec=True)
|
||||
return mock_client.return_value
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_dm_to_user(mock_context, mock_slack_client):
|
||||
mock_slack_client.users_list.return_value = {
|
||||
async def test_send_dm_to_user(mock_context, mock_chat_slack_client):
|
||||
mock_chat_slack_client.users_list.return_value = {
|
||||
"ok": True,
|
||||
"members": [{"name": "testuser", "id": "U12345"}],
|
||||
}
|
||||
mock_slack_client.conversations_open.return_value = {
|
||||
mock_chat_slack_client.conversations_open.return_value = {
|
||||
"ok": True,
|
||||
"channel": {"id": "D12345"},
|
||||
}
|
||||
mock_slack_response = Mock(spec=AsyncSlackResponse)
|
||||
mock_slack_response.data = {"ok": True}
|
||||
mock_slack_client.chat_postMessage.return_value = mock_slack_response
|
||||
mock_chat_slack_client.chat_postMessage.return_value = mock_slack_response
|
||||
|
||||
response = await send_dm_to_user(mock_context, "testuser", "Hello!")
|
||||
|
||||
assert response["response"]["ok"] is True
|
||||
mock_slack_client.users_list.assert_called_once()
|
||||
mock_slack_client.conversations_open.assert_called_once_with(users=["U12345"])
|
||||
mock_slack_client.chat_postMessage.assert_called_once_with(channel="D12345", text="Hello!")
|
||||
mock_chat_slack_client.users_list.assert_called_once()
|
||||
mock_chat_slack_client.conversations_open.assert_called_once_with(users=["U12345"])
|
||||
mock_chat_slack_client.chat_postMessage.assert_called_once_with(channel="D12345", text="Hello!")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_dm_to_inexistent_user(mock_context, mock_slack_client):
|
||||
mock_slack_client.users_list.return_value = {
|
||||
async def test_send_dm_to_inexistent_user(mock_context, mock_chat_slack_client):
|
||||
mock_chat_slack_client.users_list.return_value = {
|
||||
"ok": True,
|
||||
"members": [{"name": "testuser", "id": "U12345"}],
|
||||
}
|
||||
|
|
@ -75,33 +71,33 @@ async def test_send_dm_to_inexistent_user(mock_context, mock_slack_client):
|
|||
with pytest.raises(RetryableToolError):
|
||||
await send_dm_to_user(mock_context, "inexistent_user", "Hello!")
|
||||
|
||||
mock_slack_client.users_list.assert_called_once()
|
||||
mock_slack_client.conversations_open.assert_not_called()
|
||||
mock_slack_client.chat_postMessage.assert_not_called()
|
||||
mock_chat_slack_client.users_list.assert_called_once()
|
||||
mock_chat_slack_client.conversations_open.assert_not_called()
|
||||
mock_chat_slack_client.chat_postMessage.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_message_to_channel(mock_context, mock_slack_client):
|
||||
mock_slack_client.conversations_list.return_value = {
|
||||
async def test_send_message_to_channel(mock_context, mock_chat_slack_client):
|
||||
mock_chat_slack_client.conversations_list.return_value = {
|
||||
"ok": True,
|
||||
"channels": [{"id": "C12345", "name": "general"}],
|
||||
}
|
||||
mock_slack_response = Mock(spec=AsyncSlackResponse)
|
||||
mock_slack_response.data = {"ok": True}
|
||||
mock_slack_client.chat_postMessage.return_value = mock_slack_response
|
||||
mock_chat_slack_client.chat_postMessage.return_value = mock_slack_response
|
||||
|
||||
response = await send_message_to_channel(mock_context, "general", "Hello, channel!")
|
||||
|
||||
assert response["response"]["ok"] is True
|
||||
mock_slack_client.conversations_list.assert_called_once()
|
||||
mock_slack_client.chat_postMessage.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_list.assert_called_once()
|
||||
mock_chat_slack_client.chat_postMessage.assert_called_once_with(
|
||||
channel="C12345", text="Hello, channel!"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_message_to_inexistent_channel(mock_context, mock_slack_client):
|
||||
mock_slack_client.conversations_list.return_value = {
|
||||
async def test_send_message_to_inexistent_channel(mock_context, mock_chat_slack_client):
|
||||
mock_chat_slack_client.conversations_list.return_value = {
|
||||
"ok": True,
|
||||
"channels": [],
|
||||
}
|
||||
|
|
@ -109,15 +105,15 @@ async def test_send_message_to_inexistent_channel(mock_context, mock_slack_clien
|
|||
with pytest.raises(RetryableToolError):
|
||||
await send_message_to_channel(mock_context, "inexistent_channel", "Hello!")
|
||||
|
||||
mock_slack_client.conversations_list.assert_called_once()
|
||||
mock_slack_client.chat_postMessage.assert_not_called()
|
||||
mock_chat_slack_client.conversations_list.assert_called_once()
|
||||
mock_chat_slack_client.chat_postMessage.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_conversations_metadata_with_default_args(
|
||||
mock_context, mock_slack_client, mock_channel_info
|
||||
mock_context, mock_chat_slack_client, mock_channel_info
|
||||
):
|
||||
mock_slack_client.conversations_list.return_value = {
|
||||
mock_chat_slack_client.conversations_list.return_value = {
|
||||
"ok": True,
|
||||
"channels": [mock_channel_info],
|
||||
}
|
||||
|
|
@ -127,7 +123,7 @@ async def test_list_conversations_metadata_with_default_args(
|
|||
assert response["conversations"] == [extract_conversation_metadata(mock_channel_info)]
|
||||
assert response["next_cursor"] is None
|
||||
|
||||
mock_slack_client.conversations_list.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_list.assert_called_once_with(
|
||||
types=",".join([conv_type.value for conv_type in ConversationTypeSlackName]),
|
||||
exclude_archived=True,
|
||||
limit=MAX_PAGINATION_SIZE_LIMIT,
|
||||
|
|
@ -137,9 +133,9 @@ async def test_list_conversations_metadata_with_default_args(
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_conversations_metadata_filtering_single_conversation_type(
|
||||
mock_context, mock_slack_client, mock_channel_info
|
||||
mock_context, mock_chat_slack_client, mock_channel_info
|
||||
):
|
||||
mock_slack_client.conversations_list.return_value = {
|
||||
mock_chat_slack_client.conversations_list.return_value = {
|
||||
"ok": True,
|
||||
"channels": [mock_channel_info],
|
||||
}
|
||||
|
|
@ -151,7 +147,7 @@ async def test_list_conversations_metadata_filtering_single_conversation_type(
|
|||
assert response["conversations"] == [extract_conversation_metadata(mock_channel_info)]
|
||||
assert response["next_cursor"] is None
|
||||
|
||||
mock_slack_client.conversations_list.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_list.assert_called_once_with(
|
||||
types=ConversationTypeSlackName.PUBLIC_CHANNEL.value,
|
||||
exclude_archived=True,
|
||||
limit=MAX_PAGINATION_SIZE_LIMIT,
|
||||
|
|
@ -161,9 +157,9 @@ async def test_list_conversations_metadata_filtering_single_conversation_type(
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_conversations_metadata_filtering_multiple_conversation_types(
|
||||
mock_context, mock_slack_client, mock_channel_info
|
||||
mock_context, mock_chat_slack_client, mock_channel_info
|
||||
):
|
||||
mock_slack_client.conversations_list.return_value = {
|
||||
mock_chat_slack_client.conversations_list.return_value = {
|
||||
"ok": True,
|
||||
"channels": [mock_channel_info],
|
||||
}
|
||||
|
|
@ -179,7 +175,7 @@ async def test_list_conversations_metadata_filtering_multiple_conversation_types
|
|||
assert response["conversations"] == [extract_conversation_metadata(mock_channel_info)]
|
||||
assert response["next_cursor"] is None
|
||||
|
||||
mock_slack_client.conversations_list.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_list.assert_called_once_with(
|
||||
types=f"{ConversationTypeSlackName.PUBLIC_CHANNEL.value},{ConversationTypeSlackName.PRIVATE_CHANNEL.value}",
|
||||
exclude_archived=True,
|
||||
limit=MAX_PAGINATION_SIZE_LIMIT,
|
||||
|
|
@ -189,9 +185,9 @@ async def test_list_conversations_metadata_filtering_multiple_conversation_types
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_conversations_metadata_with_custom_pagination_args(
|
||||
mock_context, mock_slack_client, mock_channel_info
|
||||
mock_context, mock_chat_slack_client, mock_channel_info
|
||||
):
|
||||
mock_slack_client.conversations_list.return_value = {
|
||||
mock_chat_slack_client.conversations_list.return_value = {
|
||||
"ok": True,
|
||||
"channels": [mock_channel_info] * 3,
|
||||
"response_metadata": {"next_cursor": "456"},
|
||||
|
|
@ -204,7 +200,7 @@ async def test_list_conversations_metadata_with_custom_pagination_args(
|
|||
]
|
||||
assert response["next_cursor"] == "456"
|
||||
|
||||
mock_slack_client.conversations_list.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_list.assert_called_once_with(
|
||||
types=",".join([conv_type.value for conv_type in ConversationTypeSlackName]),
|
||||
exclude_archived=True,
|
||||
limit=3,
|
||||
|
|
@ -221,9 +217,9 @@ async def test_list_conversations_metadata_with_custom_pagination_args(
|
|||
],
|
||||
)
|
||||
async def test_tools_with_slack_error(
|
||||
mock_context, mock_slack_client, faulty_slack_function_name, tool_function, tool_args
|
||||
mock_context, mock_chat_slack_client, faulty_slack_function_name, tool_function, tool_args
|
||||
):
|
||||
getattr(mock_slack_client, faulty_slack_function_name).side_effect = SlackApiError(
|
||||
getattr(mock_chat_slack_client, faulty_slack_function_name).side_effect = SlackApiError(
|
||||
message="test_slack_error",
|
||||
response={"ok": False, "error": "test_slack_error"},
|
||||
)
|
||||
|
|
@ -262,8 +258,10 @@ async def test_list_channels_metadata(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conversation_metadata_by_id(mock_context, mock_slack_client, mock_channel_info):
|
||||
mock_slack_client.conversations_info.return_value = {
|
||||
async def test_get_conversation_metadata_by_id(
|
||||
mock_context, mock_chat_slack_client, mock_channel_info
|
||||
):
|
||||
mock_chat_slack_client.conversations_info.return_value = {
|
||||
"ok": True,
|
||||
"channel": mock_channel_info,
|
||||
}
|
||||
|
|
@ -271,7 +269,7 @@ async def test_get_conversation_metadata_by_id(mock_context, mock_slack_client,
|
|||
response = await get_conversation_metadata_by_id(mock_context, "C12345")
|
||||
|
||||
assert response == extract_conversation_metadata(mock_channel_info)
|
||||
mock_slack_client.conversations_info.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_info.assert_called_once_with(
|
||||
channel="C12345",
|
||||
include_locale=True,
|
||||
include_num_members=True,
|
||||
|
|
@ -281,14 +279,14 @@ async def test_get_conversation_metadata_by_id(mock_context, mock_slack_client,
|
|||
@pytest.mark.asyncio
|
||||
@patch("arcade_slack.tools.chat.list_conversations_metadata")
|
||||
async def test_get_conversation_metadata_by_id_slack_api_error(
|
||||
mock_list_conversations_metadata, mock_context, mock_slack_client, mock_channel_info
|
||||
mock_list_conversations_metadata, mock_context, mock_chat_slack_client, mock_channel_info
|
||||
):
|
||||
mock_channel_info["name"] = "whatever_conversation_should_be_present_in_additional_prompt"
|
||||
mock_list_conversations_metadata.return_value = {
|
||||
"conversations": [extract_conversation_metadata(mock_channel_info)],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
}
|
||||
mock_slack_client.conversations_info.side_effect = SlackApiError(
|
||||
mock_chat_slack_client.conversations_info.side_effect = SlackApiError(
|
||||
message="channel_not_found",
|
||||
response={"ok": False, "error": "channel_not_found"},
|
||||
)
|
||||
|
|
@ -301,7 +299,7 @@ async def test_get_conversation_metadata_by_id_slack_api_error(
|
|||
in e.additional_prompt_content
|
||||
)
|
||||
|
||||
mock_slack_client.conversations_info.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_info.assert_called_once_with(
|
||||
channel="C12345",
|
||||
include_locale=True,
|
||||
include_num_members=True,
|
||||
|
|
@ -319,34 +317,34 @@ async def test_get_conversation_metadata_by_name(
|
|||
"next_cursor": None,
|
||||
}
|
||||
|
||||
response = await get_conversation_metadata_by_name(mock_context, sample_conversation["name"])
|
||||
response = await get_channel_metadata_by_name(mock_context, sample_conversation["name"])
|
||||
|
||||
assert response == sample_conversation
|
||||
mock_list_conversations_metadata.assert_called_once_with(mock_context, next_cursor=None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conversation_metadata_by_name_triggering_pagination(
|
||||
async def test_get_channel_metadata_by_name_triggering_pagination(
|
||||
mock_context, mock_list_conversations_metadata, mock_channel_info
|
||||
):
|
||||
target_conversation = extract_conversation_metadata(mock_channel_info)
|
||||
another_conversation = extract_conversation_metadata(mock_channel_info)
|
||||
another_conversation["name"] = "another_conversation"
|
||||
target_channel = extract_conversation_metadata(mock_channel_info)
|
||||
another_channel = extract_conversation_metadata(mock_channel_info)
|
||||
another_channel["name"] = "another_channel"
|
||||
|
||||
mock_list_conversations_metadata.side_effect = [
|
||||
{
|
||||
"conversations": [another_conversation],
|
||||
"conversations": [another_channel],
|
||||
"next_cursor": "123",
|
||||
},
|
||||
{
|
||||
"conversations": [target_conversation],
|
||||
"conversations": [target_channel],
|
||||
"next_cursor": None,
|
||||
},
|
||||
]
|
||||
|
||||
response = await get_conversation_metadata_by_name(mock_context, target_conversation["name"])
|
||||
response = await get_channel_metadata_by_name(mock_context, target_channel["name"])
|
||||
|
||||
assert response == target_conversation
|
||||
assert response == target_channel
|
||||
assert mock_list_conversations_metadata.call_count == 2
|
||||
mock_list_conversations_metadata.assert_has_calls([
|
||||
call(mock_context, next_cursor=None),
|
||||
|
|
@ -355,26 +353,26 @@ async def test_get_conversation_metadata_by_name_triggering_pagination(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conversation_metadata_by_name_not_found(
|
||||
async def test_get_channel_metadata_by_name_not_found(
|
||||
mock_context, mock_list_conversations_metadata, mock_channel_info
|
||||
):
|
||||
first_conversation = extract_conversation_metadata(mock_channel_info)
|
||||
second_conversation = extract_conversation_metadata(mock_channel_info)
|
||||
second_conversation["name"] = "second_conversation"
|
||||
first_channel = extract_conversation_metadata(mock_channel_info)
|
||||
second_channel = extract_conversation_metadata(mock_channel_info)
|
||||
second_channel["name"] = "second_channel"
|
||||
|
||||
mock_list_conversations_metadata.side_effect = [
|
||||
{
|
||||
"conversations": [second_conversation],
|
||||
"conversations": [second_channel],
|
||||
"next_cursor": "123",
|
||||
},
|
||||
{
|
||||
"conversations": [first_conversation],
|
||||
"conversations": [first_channel],
|
||||
"next_cursor": None,
|
||||
},
|
||||
]
|
||||
|
||||
with pytest.raises(RetryableToolError):
|
||||
await get_conversation_metadata_by_name(mock_context, "inexistent_conversation")
|
||||
await get_channel_metadata_by_name(mock_context, "inexistent_channel")
|
||||
|
||||
assert mock_list_conversations_metadata.call_count == 2
|
||||
mock_list_conversations_metadata.assert_has_calls([
|
||||
|
|
@ -387,7 +385,7 @@ async def test_get_conversation_metadata_by_name_not_found(
|
|||
@patch("arcade_slack.tools.chat.async_paginate")
|
||||
@patch("arcade_slack.tools.chat.get_user_info_by_id")
|
||||
async def test_get_members_from_conversation_id(
|
||||
mock_get_user_info_by_id, mock_async_paginate, mock_context, mock_slack_client
|
||||
mock_get_user_info_by_id, mock_async_paginate, mock_context, mock_chat_slack_client
|
||||
):
|
||||
member1 = {"id": "U123", "name": "testuser123"}
|
||||
member1_info = extract_basic_user_info(member1)
|
||||
|
|
@ -406,7 +404,7 @@ async def test_get_members_from_conversation_id(
|
|||
"next_cursor": "token123",
|
||||
}
|
||||
mock_async_paginate.assert_called_once_with(
|
||||
mock_slack_client.conversations_members,
|
||||
mock_chat_slack_client.conversations_members,
|
||||
"members",
|
||||
limit=2,
|
||||
next_cursor=None,
|
||||
|
|
@ -427,7 +425,7 @@ async def test_get_members_from_conversation_id_channel_not_found(
|
|||
mock_get_user_info_by_id,
|
||||
mock_async_paginate,
|
||||
mock_context,
|
||||
mock_slack_client,
|
||||
mock_chat_slack_client,
|
||||
mock_channel_info,
|
||||
):
|
||||
conversations = [extract_conversation_metadata(mock_channel_info)] * 2
|
||||
|
|
@ -451,7 +449,7 @@ async def test_get_members_from_conversation_id_channel_not_found(
|
|||
await get_members_in_conversation_by_id(mock_context, conversation_id="C12345", limit=2)
|
||||
|
||||
mock_async_paginate.assert_called_once_with(
|
||||
mock_slack_client.conversations_members,
|
||||
mock_chat_slack_client.conversations_members,
|
||||
"members",
|
||||
limit=2,
|
||||
next_cursor=None,
|
||||
|
|
@ -463,7 +461,7 @@ async def test_get_members_from_conversation_id_channel_not_found(
|
|||
@pytest.mark.asyncio
|
||||
@patch("arcade_slack.tools.chat.list_conversations_metadata")
|
||||
@patch("arcade_slack.tools.chat.get_members_in_conversation_by_id")
|
||||
async def test_get_members_in_conversation_by_name(
|
||||
async def test_get_members_in_channel_by_name(
|
||||
mock_get_members_in_conversation_by_id,
|
||||
mock_list_conversations_metadata,
|
||||
mock_context,
|
||||
|
|
@ -474,7 +472,7 @@ async def test_get_members_in_conversation_by_name(
|
|||
"next_cursor": None,
|
||||
}
|
||||
|
||||
response = await get_members_in_conversation_by_name(
|
||||
response = await get_members_in_channel_by_name(
|
||||
mock_context, mock_channel_info["name"], limit=2
|
||||
)
|
||||
|
||||
|
|
@ -491,7 +489,7 @@ async def test_get_members_in_conversation_by_name(
|
|||
@pytest.mark.asyncio
|
||||
@patch("arcade_slack.tools.chat.list_conversations_metadata")
|
||||
@patch("arcade_slack.tools.chat.get_members_in_conversation_by_id")
|
||||
async def test_get_members_in_conversation_by_name_triggering_pagination(
|
||||
async def test_get_members_in_channel_by_name_triggering_pagination(
|
||||
mock_get_members_in_conversation_by_id,
|
||||
mock_list_conversations_metadata,
|
||||
mock_context,
|
||||
|
|
@ -513,9 +511,7 @@ async def test_get_members_in_conversation_by_name_triggering_pagination(
|
|||
},
|
||||
]
|
||||
|
||||
response = await get_members_in_conversation_by_name(
|
||||
mock_context, conversation2["name"], limit=2
|
||||
)
|
||||
response = await get_members_in_channel_by_name(mock_context, conversation2["name"], limit=2)
|
||||
|
||||
assert response == mock_get_members_in_conversation_by_id.return_value
|
||||
mock_list_conversations_metadata.assert_has_calls([
|
||||
|
|
@ -531,8 +527,8 @@ async def test_get_members_in_conversation_by_name_triggering_pagination(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conversation_history_by_id(mock_context, mock_slack_client):
|
||||
mock_slack_client.conversations_history.return_value = {
|
||||
async def test_get_conversation_history_by_id(mock_context, mock_chat_slack_client):
|
||||
mock_chat_slack_client.conversations_history.return_value = {
|
||||
"ok": True,
|
||||
"messages": [{"text": "Hello, world!"}],
|
||||
}
|
||||
|
|
@ -540,7 +536,7 @@ async def test_get_conversation_history_by_id(mock_context, mock_slack_client):
|
|||
response = await get_messages_in_conversation_by_id(mock_context, "C12345", limit=1)
|
||||
|
||||
assert response == {"messages": [{"text": "Hello, world!"}], "next_cursor": None}
|
||||
mock_slack_client.conversations_history.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_history.assert_called_once_with(
|
||||
channel="C12345",
|
||||
include_all_metadata=True,
|
||||
inclusive=True,
|
||||
|
|
@ -555,9 +551,12 @@ async def test_get_conversation_history_by_id(mock_context, mock_slack_client):
|
|||
@patch("arcade_slack.tools.chat.convert_relative_datetime_to_unix_timestamp")
|
||||
@patch("arcade_slack.tools.chat.datetime")
|
||||
async def test_get_conversation_history_by_id_with_relative_datetime_args(
|
||||
mock_datetime, mock_convert_relative_datetime_to_unix_timestamp, mock_context, mock_slack_client
|
||||
mock_datetime,
|
||||
mock_convert_relative_datetime_to_unix_timestamp,
|
||||
mock_context,
|
||||
mock_chat_slack_client,
|
||||
):
|
||||
mock_slack_client.conversations_history.return_value = {
|
||||
mock_chat_slack_client.conversations_history.return_value = {
|
||||
"ok": True,
|
||||
"messages": [{"text": "Hello, world!"}],
|
||||
}
|
||||
|
|
@ -584,7 +583,7 @@ async def test_get_conversation_history_by_id_with_relative_datetime_args(
|
|||
call("01:00:00", expected_current_unix_timestamp),
|
||||
call("02:00:00", expected_current_unix_timestamp),
|
||||
])
|
||||
mock_slack_client.conversations_history.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_history.assert_called_once_with(
|
||||
channel="C12345",
|
||||
include_all_metadata=True,
|
||||
inclusive=True,
|
||||
|
|
@ -600,9 +599,9 @@ async def test_get_conversation_history_by_id_with_relative_datetime_args(
|
|||
@pytest.mark.asyncio
|
||||
@patch("arcade_slack.tools.chat.convert_datetime_to_unix_timestamp")
|
||||
async def test_get_conversation_history_by_id_with_absolute_datetime_args(
|
||||
mock_convert_datetime_to_unix_timestamp, mock_context, mock_slack_client
|
||||
mock_convert_datetime_to_unix_timestamp, mock_context, mock_chat_slack_client
|
||||
):
|
||||
mock_slack_client.conversations_history.return_value = {
|
||||
mock_chat_slack_client.conversations_history.return_value = {
|
||||
"ok": True,
|
||||
"messages": [{"text": "Hello, world!"}],
|
||||
}
|
||||
|
|
@ -631,7 +630,7 @@ async def test_get_conversation_history_by_id_with_absolute_datetime_args(
|
|||
call("2025-01-02 00:00:00"),
|
||||
call("2025-01-01 00:00:00"),
|
||||
])
|
||||
mock_slack_client.conversations_history.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_history.assert_called_once_with(
|
||||
channel="C12345",
|
||||
include_all_metadata=True,
|
||||
inclusive=True,
|
||||
|
|
@ -644,7 +643,7 @@ async def test_get_conversation_history_by_id_with_absolute_datetime_args(
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conversation_history_by_id_with_messed_oldest_args(
|
||||
mock_context, mock_slack_client
|
||||
mock_context, mock_chat_slack_client
|
||||
):
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await get_messages_in_conversation_by_id(
|
||||
|
|
@ -657,7 +656,7 @@ async def test_get_conversation_history_by_id_with_messed_oldest_args(
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conversation_history_by_id_with_messed_latest_args(
|
||||
mock_context, mock_slack_client
|
||||
mock_context, mock_chat_slack_client
|
||||
):
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await get_messages_in_conversation_by_id(
|
||||
|
|
@ -669,8 +668,8 @@ async def test_get_conversation_history_by_id_with_messed_latest_args(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conversation_history_by_name(mock_context, mock_slack_client):
|
||||
mock_slack_client.conversations_list.return_value = {
|
||||
async def test_get_conversation_history_by_name(mock_context, mock_chat_slack_client):
|
||||
mock_chat_slack_client.conversations_list.return_value = {
|
||||
"ok": True,
|
||||
"channels": [
|
||||
{
|
||||
|
|
@ -681,7 +680,7 @@ async def test_get_conversation_history_by_name(mock_context, mock_slack_client)
|
|||
}
|
||||
],
|
||||
}
|
||||
mock_slack_client.conversations_history.return_value = {
|
||||
mock_chat_slack_client.conversations_history.return_value = {
|
||||
"ok": True,
|
||||
"messages": [{"text": "Hello, world!"}],
|
||||
}
|
||||
|
|
@ -689,6 +688,133 @@ async def test_get_conversation_history_by_name(mock_context, mock_slack_client)
|
|||
response = await get_messages_in_channel_by_name(mock_context, "general", limit=1)
|
||||
|
||||
assert response == {"messages": [{"text": "Hello, world!"}], "next_cursor": None}
|
||||
mock_slack_client.conversations_history.assert_called_once_with(
|
||||
mock_chat_slack_client.conversations_history.assert_called_once_with(
|
||||
channel="C12345", include_all_metadata=True, inclusive=True, limit=1, cursor=None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_slack.tools.chat.retrieve_conversations_by_user_ids")
|
||||
async def test_get_direct_message_conversation_metadata_by_username(
|
||||
mock_retrieve_conversations_by_user_ids,
|
||||
mock_context,
|
||||
mock_chat_slack_client,
|
||||
mock_users_slack_client,
|
||||
):
|
||||
mock_chat_slack_client.auth_test.return_value = {
|
||||
"ok": True,
|
||||
"user_id": "U1",
|
||||
"team_id": "T1",
|
||||
"user": "user1",
|
||||
}
|
||||
|
||||
mock_users_slack_client.users_list.return_value = {
|
||||
"ok": True,
|
||||
"members": [
|
||||
{"id": "U1", "name": "user1"},
|
||||
{"id": "U2", "name": "user2"},
|
||||
],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
}
|
||||
|
||||
conversation = {
|
||||
"id": "C12345",
|
||||
"type": ConversationTypeSlackName.IM.value,
|
||||
"is_im": True,
|
||||
"members": ["U1", "U2"],
|
||||
}
|
||||
|
||||
mock_retrieve_conversations_by_user_ids.return_value = [conversation]
|
||||
|
||||
response = await get_direct_message_conversation_metadata_by_username(
|
||||
context=mock_context, username="user2"
|
||||
)
|
||||
|
||||
assert response == conversation
|
||||
mock_retrieve_conversations_by_user_ids.assert_called_once_with(
|
||||
list_conversations_func=list_conversations_metadata,
|
||||
get_members_in_conversation_func=get_members_in_conversation_by_id,
|
||||
context=mock_context,
|
||||
conversation_types=[ConversationType.DIRECT_MESSAGE],
|
||||
user_ids=["U1", "U2"],
|
||||
exact_match=True,
|
||||
limit=1,
|
||||
next_cursor=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_slack.tools.chat.retrieve_conversations_by_user_ids")
|
||||
async def test_get_direct_message_conversation_metadata_by_username_username_not_found(
|
||||
mock_retrieve_conversations_by_user_ids,
|
||||
mock_context,
|
||||
mock_chat_slack_client,
|
||||
mock_users_slack_client,
|
||||
):
|
||||
mock_chat_slack_client.users_identity.return_value = {
|
||||
"ok": True,
|
||||
"user": {"id": "U1", "name": "user1"},
|
||||
"team": {"id": "T1", "name": "team1"},
|
||||
}
|
||||
|
||||
mock_users_slack_client.users_list.return_value = {
|
||||
"ok": True,
|
||||
"members": [
|
||||
{"id": "U1", "name": "user1"},
|
||||
{"id": "U2", "name": "user2"},
|
||||
],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
}
|
||||
|
||||
mock_retrieve_conversations_by_user_ids.side_effect = TimeoutError()
|
||||
|
||||
with pytest.raises(RetryableToolError):
|
||||
await get_direct_message_conversation_metadata_by_username(
|
||||
context=mock_context, username="user999"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_slack.tools.chat.get_messages_in_conversation_by_id")
|
||||
@patch("arcade_slack.tools.chat.get_direct_message_conversation_metadata_by_username")
|
||||
async def test_get_messages_in_direct_conversation_by_username(
|
||||
mock_get_direct_message_conversation_metadata_by_username,
|
||||
mock_get_messages_in_conversation_by_id,
|
||||
mock_context,
|
||||
):
|
||||
mock_get_direct_message_conversation_metadata_by_username.return_value = {
|
||||
"id": "C12345",
|
||||
}
|
||||
|
||||
response = await get_messages_in_direct_message_conversation_by_username(
|
||||
context=mock_context, username="user2"
|
||||
)
|
||||
|
||||
assert response == mock_get_messages_in_conversation_by_id.return_value
|
||||
mock_get_direct_message_conversation_metadata_by_username.assert_called_once_with(
|
||||
context=mock_context, username="user2"
|
||||
)
|
||||
mock_get_messages_in_conversation_by_id.assert_called_once_with(
|
||||
context=mock_context,
|
||||
conversation_id="C12345",
|
||||
oldest_relative=None,
|
||||
latest_relative=None,
|
||||
oldest_datetime=None,
|
||||
latest_datetime=None,
|
||||
limit=None,
|
||||
next_cursor=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("arcade_slack.tools.chat.get_direct_message_conversation_metadata_by_username")
|
||||
async def test_get_messages_in_direct_conversation_by_username_not_found(
|
||||
mock_get_direct_message_conversation_metadata_by_username,
|
||||
mock_context,
|
||||
):
|
||||
mock_get_direct_message_conversation_metadata_by_username.return_value = None
|
||||
|
||||
with pytest.raises(ToolExecutionError):
|
||||
await get_messages_in_direct_message_conversation_by_username(
|
||||
context=mock_context, username="user2"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,26 @@
|
|||
import asyncio
|
||||
from unittest.mock import AsyncMock, call, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, call, patch
|
||||
|
||||
import pytest
|
||||
from arcade.sdk import ToolContext
|
||||
from slack_sdk.errors import SlackApiError
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from arcade_slack.exceptions import PaginationTimeoutError
|
||||
from arcade_slack.utils import async_paginate
|
||||
from arcade_slack.models import (
|
||||
ConversationType,
|
||||
ConversationTypeSlackName,
|
||||
)
|
||||
from arcade_slack.tools.chat import (
|
||||
get_members_in_conversation_by_id,
|
||||
list_conversations_metadata,
|
||||
)
|
||||
from arcade_slack.utils import (
|
||||
async_paginate,
|
||||
convert_conversation_type_to_slack_name,
|
||||
filter_conversations_by_user_ids,
|
||||
retrieve_conversations_by_user_ids,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -185,3 +199,486 @@ async def test_async_paginate_with_timeout():
|
|||
str(exc_info.value)
|
||||
== f"The pagination process timed out after {max_pagination_timeout_seconds} seconds."
|
||||
)
|
||||
|
||||
|
||||
def test_filter_conversations_by_user_ids():
|
||||
conversations = [
|
||||
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}]},
|
||||
{"id": "456", "members": [{"id": "user2"}, {"id": "user3"}]},
|
||||
]
|
||||
response = filter_conversations_by_user_ids(
|
||||
conversations=conversations,
|
||||
user_ids=["user1", "user2"],
|
||||
exact_match=False,
|
||||
)
|
||||
assert response == [
|
||||
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}]},
|
||||
]
|
||||
|
||||
|
||||
def test_filter_conversations_by_user_ids_empty_response():
|
||||
conversations = [
|
||||
{"id": "123", "members": [{"id": "user1"}, {"id": "user3"}, {"id": "user4"}]},
|
||||
{"id": "456", "members": [{"id": "user2"}, {"id": "user3"}, {"id": "user4"}]},
|
||||
]
|
||||
response = filter_conversations_by_user_ids(
|
||||
conversations=conversations,
|
||||
user_ids=["user1", "user2"],
|
||||
exact_match=False,
|
||||
)
|
||||
assert response == []
|
||||
|
||||
|
||||
def test_filter_conversations_by_user_ids_multiple_matches():
|
||||
conversations = [
|
||||
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}]},
|
||||
{"id": "456", "members": [{"id": "user2"}, {"id": "user3"}]},
|
||||
{
|
||||
"id": "789",
|
||||
"members": [{"id": "user4"}, {"id": "user1"}, {"id": "user2"}, {"id": "user3"}],
|
||||
},
|
||||
]
|
||||
response = filter_conversations_by_user_ids(
|
||||
conversations=conversations,
|
||||
user_ids=["user1", "user2"],
|
||||
exact_match=False,
|
||||
)
|
||||
assert response == [
|
||||
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}]},
|
||||
{
|
||||
"id": "789",
|
||||
"members": [{"id": "user4"}, {"id": "user1"}, {"id": "user2"}, {"id": "user3"}],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_filter_conversations_by_user_ids_exact_match():
|
||||
conversations = [
|
||||
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}]},
|
||||
{"id": "456", "members": [{"id": "user2"}, {"id": "user3"}]},
|
||||
]
|
||||
response = filter_conversations_by_user_ids(
|
||||
conversations=conversations,
|
||||
user_ids=["user1", "user2"],
|
||||
exact_match=True,
|
||||
)
|
||||
assert response == [{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}]}]
|
||||
|
||||
|
||||
def test_filter_conversations_by_user_ids_exact_match_empty_response():
|
||||
conversations = [
|
||||
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}]},
|
||||
{"id": "456", "members": [{"id": "user2"}, {"id": "user3"}]},
|
||||
]
|
||||
response = filter_conversations_by_user_ids(
|
||||
conversations=conversations,
|
||||
user_ids=["user1", "user2"],
|
||||
exact_match=True,
|
||||
)
|
||||
assert response == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"search_user_ids, conversation_types, exact_match, limit, expected_conversation_ids",
|
||||
[
|
||||
(["U1", "U2"], [ConversationType.DIRECT_MESSAGE], False, 1, ["C1"]),
|
||||
(["U1", "U2"], [ConversationType.DIRECT_MESSAGE], True, 1, ["C1"]),
|
||||
(["U1", "U2", "U3"], [ConversationType.DIRECT_MESSAGE], False, 1, []),
|
||||
(
|
||||
["U1", "U2"],
|
||||
[ConversationType.DIRECT_MESSAGE, ConversationType.PUBLIC_CHANNEL],
|
||||
False,
|
||||
10,
|
||||
["C1", "C3", "C4"],
|
||||
),
|
||||
(
|
||||
["U1", "U2"],
|
||||
[ConversationType.DIRECT_MESSAGE, ConversationType.PUBLIC_CHANNEL],
|
||||
True,
|
||||
10,
|
||||
["C1", "C3"],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_retrieve_conversations_by_user_ids(
|
||||
mock_chat_slack_client,
|
||||
mock_users_slack_client,
|
||||
search_user_ids,
|
||||
conversation_types,
|
||||
exact_match,
|
||||
limit,
|
||||
expected_conversation_ids,
|
||||
):
|
||||
context = MagicMock(spec=ToolContext)
|
||||
context.authorization = MagicMock()
|
||||
context.authorization.token = MagicMock()
|
||||
|
||||
conversation_types_slack_name_str = [
|
||||
convert_conversation_type_to_slack_name(conv_type).value
|
||||
for conv_type in conversation_types or ConversationType
|
||||
]
|
||||
|
||||
conversations = [
|
||||
{
|
||||
"conversation": {
|
||||
"id": "C1",
|
||||
"type": ConversationTypeSlackName.IM.value,
|
||||
"name": "im-1",
|
||||
"is_channel": False,
|
||||
"is_im": True,
|
||||
"is_member": True,
|
||||
},
|
||||
"members": {
|
||||
"ok": True,
|
||||
"members": ["U1", "U2"],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
"users": [
|
||||
{"ok": True, "user": {"id": "U1", "team_id": "T123", "name": "user1"}},
|
||||
{"ok": True, "user": {"id": "U2", "team_id": "T123", "name": "user2"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"conversation": {
|
||||
"id": "C2",
|
||||
"type": ConversationTypeSlackName.IM.value,
|
||||
"name": "im-2",
|
||||
"is_channel": False,
|
||||
"is_im": True,
|
||||
"is_member": True,
|
||||
},
|
||||
"members": {
|
||||
"ok": True,
|
||||
"members": ["U2", "U3"],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
"users": [
|
||||
{"ok": True, "user": {"id": "U2", "team_id": "T123", "name": "user2"}},
|
||||
{"ok": True, "user": {"id": "U3", "team_id": "T123", "name": "user3"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"conversation": {
|
||||
"id": "C3",
|
||||
"type": ConversationTypeSlackName.PUBLIC_CHANNEL.value,
|
||||
"name": "general",
|
||||
"is_channel": True,
|
||||
"is_im": False,
|
||||
"is_member": True,
|
||||
},
|
||||
"members": {
|
||||
"ok": True,
|
||||
"members": ["U1", "U2"],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
"users": [
|
||||
{"ok": True, "user": {"id": "U1", "team_id": "T123", "name": "user1"}},
|
||||
{"ok": True, "user": {"id": "U2", "team_id": "T123", "name": "user2"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"conversation": {
|
||||
"id": "C4",
|
||||
"type": ConversationTypeSlackName.PUBLIC_CHANNEL.value,
|
||||
"name": "random",
|
||||
"is_channel": True,
|
||||
"is_im": False,
|
||||
"is_member": True,
|
||||
},
|
||||
"members": {
|
||||
"ok": True,
|
||||
"members": ["U1", "U2", "U3", "U4"],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
"users": [
|
||||
{"ok": True, "user": {"id": "U1", "team_id": "T123", "name": "user1"}},
|
||||
{"ok": True, "user": {"id": "U2", "team_id": "T123", "name": "user2"}},
|
||||
{"ok": True, "user": {"id": "U3", "team_id": "T123", "name": "user3"}},
|
||||
{"ok": True, "user": {"id": "U4", "team_id": "T123", "name": "user4"}},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
conversations_listed = [
|
||||
conversation
|
||||
for conversation in conversations
|
||||
if conversation["conversation"]["type"] in conversation_types_slack_name_str
|
||||
]
|
||||
|
||||
mock_chat_slack_client.conversations_list.return_value = {
|
||||
"ok": True,
|
||||
"channels": [conversation["conversation"] for conversation in conversations_listed],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
}
|
||||
|
||||
mock_chat_slack_client.conversations_members.side_effect = [
|
||||
conversation["members"] for conversation in conversations_listed
|
||||
]
|
||||
|
||||
mock_users_slack_client.users_info.side_effect = [
|
||||
user for conversation in conversations_listed for user in conversation["users"]
|
||||
]
|
||||
|
||||
conversations_found = await retrieve_conversations_by_user_ids(
|
||||
list_conversations_func=list_conversations_metadata,
|
||||
get_members_in_conversation_func=get_members_in_conversation_by_id,
|
||||
context=context,
|
||||
conversation_types=conversation_types,
|
||||
user_ids=search_user_ids,
|
||||
exact_match=exact_match,
|
||||
limit=limit,
|
||||
next_cursor=None,
|
||||
)
|
||||
|
||||
assert [conversation["id"] for conversation in conversations_found] == expected_conversation_ids
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"search_user_ids, conversation_types, exact_match, limit, "
|
||||
"expected_conversation_ids, expected_conversations_list_calls"
|
||||
),
|
||||
[
|
||||
(
|
||||
["U1", "U2", "U3"],
|
||||
[ConversationType.MULTI_PERSON_DIRECT_MESSAGE],
|
||||
False,
|
||||
None,
|
||||
["C1", "C3"],
|
||||
2,
|
||||
),
|
||||
(
|
||||
["U1", "U2", "U3"],
|
||||
[ConversationType.MULTI_PERSON_DIRECT_MESSAGE],
|
||||
True,
|
||||
None,
|
||||
["C1"],
|
||||
2,
|
||||
),
|
||||
(["U1", "U2", "U99"], [ConversationType.MULTI_PERSON_DIRECT_MESSAGE], False, None, [], 2),
|
||||
(
|
||||
["U1", "U2"],
|
||||
[ConversationType.MULTI_PERSON_DIRECT_MESSAGE, ConversationType.PUBLIC_CHANNEL],
|
||||
False,
|
||||
None,
|
||||
["C1", "C3", "C4", "C6"],
|
||||
2,
|
||||
),
|
||||
(
|
||||
["U1", "U2"],
|
||||
[ConversationType.MULTI_PERSON_DIRECT_MESSAGE, ConversationType.PUBLIC_CHANNEL],
|
||||
False,
|
||||
1,
|
||||
["C1"],
|
||||
2,
|
||||
),
|
||||
(
|
||||
["U1", "U2"],
|
||||
[ConversationType.MULTI_PERSON_DIRECT_MESSAGE, ConversationType.PUBLIC_CHANNEL],
|
||||
False,
|
||||
3,
|
||||
["C1", "C3", "C4"],
|
||||
2,
|
||||
),
|
||||
(
|
||||
["U1", "U2"],
|
||||
[ConversationType.MULTI_PERSON_DIRECT_MESSAGE, ConversationType.PUBLIC_CHANNEL],
|
||||
True,
|
||||
None,
|
||||
["C4"],
|
||||
2,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_retrieve_conversations_by_user_ids_with_pagination(
|
||||
mock_chat_slack_client,
|
||||
mock_users_slack_client,
|
||||
search_user_ids,
|
||||
conversation_types,
|
||||
exact_match,
|
||||
limit,
|
||||
expected_conversation_ids,
|
||||
expected_conversations_list_calls,
|
||||
):
|
||||
context = MagicMock(spec=ToolContext)
|
||||
context.authorization = MagicMock()
|
||||
context.authorization.token = MagicMock()
|
||||
|
||||
conversation_types_slack_name_str = [
|
||||
convert_conversation_type_to_slack_name(conv_type).value
|
||||
for conv_type in conversation_types or ConversationType
|
||||
]
|
||||
|
||||
conversations = [
|
||||
{
|
||||
"conversation": {
|
||||
"id": "C1",
|
||||
"type": ConversationTypeSlackName.MPIM.value,
|
||||
"name": "mpim-1",
|
||||
"is_channel": False,
|
||||
"is_im": False,
|
||||
"is_mpim": True,
|
||||
"is_member": True,
|
||||
},
|
||||
"members": {
|
||||
"ok": True,
|
||||
"members": ["U1", "U2", "U3"],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
"users": [
|
||||
{"ok": True, "user": {"id": "U1", "team_id": "T123", "name": "user1"}},
|
||||
{"ok": True, "user": {"id": "U2", "team_id": "T123", "name": "user2"}},
|
||||
{"ok": True, "user": {"id": "U3", "team_id": "T123", "name": "user3"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"conversation": {
|
||||
"id": "C2",
|
||||
"type": ConversationTypeSlackName.MPIM.value,
|
||||
"name": "mpim-2",
|
||||
"is_channel": False,
|
||||
"is_im": False,
|
||||
"is_mpim": True,
|
||||
"is_member": True,
|
||||
},
|
||||
"members": {
|
||||
"ok": True,
|
||||
"members": ["U2", "U3"],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
"users": [
|
||||
{"ok": True, "user": {"id": "U2", "team_id": "T123", "name": "user2"}},
|
||||
{"ok": True, "user": {"id": "U3", "team_id": "T123", "name": "user3"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"conversation": {
|
||||
"id": "C3",
|
||||
"type": ConversationTypeSlackName.MPIM.value,
|
||||
"name": "mpim-3",
|
||||
"is_channel": False,
|
||||
"is_im": False,
|
||||
"is_mpim": True,
|
||||
"is_member": True,
|
||||
},
|
||||
"members": {
|
||||
"ok": True,
|
||||
"members": ["U1", "U2", "U3", "U4"],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
"users": [
|
||||
{"ok": True, "user": {"id": "U1", "team_id": "T123", "name": "user1"}},
|
||||
{"ok": True, "user": {"id": "U2", "team_id": "T123", "name": "user2"}},
|
||||
{"ok": True, "user": {"id": "U3", "team_id": "T123", "name": "user3"}},
|
||||
{"ok": True, "user": {"id": "U4", "team_id": "T123", "name": "user4"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"conversation": {
|
||||
"id": "C4",
|
||||
"type": ConversationTypeSlackName.PUBLIC_CHANNEL.value,
|
||||
"name": "channel-4",
|
||||
"is_channel": True,
|
||||
"is_im": False,
|
||||
"is_member": True,
|
||||
},
|
||||
"members": {
|
||||
"ok": True,
|
||||
"members": ["U1", "U2"],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
"users": [
|
||||
{"ok": True, "user": {"id": "U1", "team_id": "T123", "name": "user1"}},
|
||||
{"ok": True, "user": {"id": "U2", "team_id": "T123", "name": "user2"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"conversation": {
|
||||
"id": "C5",
|
||||
"type": ConversationTypeSlackName.PUBLIC_CHANNEL.value,
|
||||
"name": "channel-5",
|
||||
"is_channel": True,
|
||||
"is_im": False,
|
||||
"is_member": True,
|
||||
},
|
||||
"members": {
|
||||
"ok": True,
|
||||
"members": ["U2", "U3", "U4"],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
"users": [
|
||||
{"ok": True, "user": {"id": "U2", "team_id": "T123", "name": "user2"}},
|
||||
{"ok": True, "user": {"id": "U3", "team_id": "T123", "name": "user3"}},
|
||||
{"ok": True, "user": {"id": "U4", "team_id": "T123", "name": "user4"}},
|
||||
],
|
||||
},
|
||||
{
|
||||
"conversation": {
|
||||
"id": "C6",
|
||||
"type": ConversationTypeSlackName.PUBLIC_CHANNEL.value,
|
||||
"name": "channel-6",
|
||||
"is_channel": True,
|
||||
"is_im": False,
|
||||
"is_member": True,
|
||||
},
|
||||
"members": {
|
||||
"ok": True,
|
||||
"members": ["U1", "U2", "U3", "U4"],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
"users": [
|
||||
{"ok": True, "user": {"id": "U1", "team_id": "T123", "name": "user1"}},
|
||||
{"ok": True, "user": {"id": "U2", "team_id": "T123", "name": "user2"}},
|
||||
{"ok": True, "user": {"id": "U3", "team_id": "T123", "name": "user3"}},
|
||||
{"ok": True, "user": {"id": "U4", "team_id": "T123", "name": "user4"}},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
conversations_listed = [
|
||||
conversation
|
||||
for conversation in conversations
|
||||
if conversation["conversation"]["type"] in conversation_types_slack_name_str
|
||||
]
|
||||
|
||||
split_size = len(conversations_listed) // 2
|
||||
|
||||
conversations_listed_1 = conversations_listed[:split_size]
|
||||
conversations_listed_2 = conversations_listed[split_size:]
|
||||
|
||||
mock_chat_slack_client.conversations_list.side_effect = [
|
||||
{
|
||||
"ok": True,
|
||||
"channels": [conversation["conversation"] for conversation in conversations_listed_1],
|
||||
"response_metadata": {"next_cursor": "cursor_1"},
|
||||
},
|
||||
{
|
||||
"ok": True,
|
||||
"channels": [conversation["conversation"] for conversation in conversations_listed_2],
|
||||
"response_metadata": {"next_cursor": None},
|
||||
},
|
||||
]
|
||||
|
||||
mock_chat_slack_client.conversations_members.side_effect = [
|
||||
conversation["members"] for conversation in conversations_listed
|
||||
]
|
||||
|
||||
mock_users_slack_client.users_info.side_effect = [
|
||||
user for conversation in conversations_listed for user in conversation["users"]
|
||||
]
|
||||
|
||||
conversations_found = await retrieve_conversations_by_user_ids(
|
||||
list_conversations_func=list_conversations_metadata,
|
||||
get_members_in_conversation_func=get_members_in_conversation_by_id,
|
||||
context=context,
|
||||
conversation_types=conversation_types,
|
||||
user_ids=search_user_ids,
|
||||
exact_match=exact_match,
|
||||
limit=limit,
|
||||
next_cursor=None,
|
||||
)
|
||||
|
||||
assert [conversation["id"] for conversation in conversations_found] == expected_conversation_ids
|
||||
assert mock_chat_slack_client.conversations_list.call_count == expected_conversations_list_calls
|
||||
|
|
|
|||
Loading…
Reference in a new issue