arcade-slack 0.4.6__py3-none-any.whl → 0.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arcade_slack/constants.py +7 -2
- arcade_slack/conversation_retrieval.py +74 -0
- arcade_slack/custom_types.py +4 -4
- arcade_slack/exceptions.py +0 -20
- arcade_slack/message_retrieval.py +76 -0
- arcade_slack/models.py +165 -1
- arcade_slack/tools/chat.py +675 -538
- arcade_slack/tools/users.py +59 -47
- arcade_slack/user_retrieval.py +214 -0
- arcade_slack/utils.py +238 -79
- {arcade_slack-0.4.6.dist-info → arcade_slack-0.5.1.dist-info}/METADATA +1 -1
- arcade_slack-0.5.1.dist-info/RECORD +17 -0
- arcade_slack-0.4.6.dist-info/RECORD +0 -14
- {arcade_slack-0.4.6.dist-info → arcade_slack-0.5.1.dist-info}/WHEEL +0 -0
- {arcade_slack-0.4.6.dist-info → arcade_slack-0.5.1.dist-info}/licenses/LICENSE +0 -0
arcade_slack/tools/users.py
CHANGED
|
@@ -2,12 +2,13 @@ from typing import Annotated, Any, cast
|
|
|
2
2
|
|
|
3
3
|
from arcade_tdk import ToolContext, tool
|
|
4
4
|
from arcade_tdk.auth import Slack
|
|
5
|
-
from arcade_tdk.errors import RetryableToolError
|
|
6
|
-
from slack_sdk.errors import SlackApiError
|
|
7
5
|
from slack_sdk.web.async_client import AsyncWebClient
|
|
8
6
|
|
|
9
7
|
from arcade_slack.constants import MAX_PAGINATION_TIMEOUT_SECONDS
|
|
10
|
-
from arcade_slack.models import
|
|
8
|
+
from arcade_slack.models import (
|
|
9
|
+
SlackPaginationNextCursor,
|
|
10
|
+
)
|
|
11
|
+
from arcade_slack.user_retrieval import get_users_by_id_username_or_email
|
|
11
12
|
from arcade_slack.utils import (
|
|
12
13
|
async_paginate,
|
|
13
14
|
extract_basic_user_info,
|
|
@@ -16,62 +17,58 @@ from arcade_slack.utils import (
|
|
|
16
17
|
)
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
@tool(
|
|
20
|
-
|
|
21
|
-
scopes=["users:read", "users:read.email"],
|
|
22
|
-
)
|
|
23
|
-
)
|
|
24
|
-
async def get_user_info_by_id(
|
|
20
|
+
@tool(requires_auth=Slack(scopes=["users:read", "users:read.email"]))
|
|
21
|
+
async def get_users_info(
|
|
25
22
|
context: ToolContext,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
user_ids: Annotated[list[str] | None, "The IDs of the users to get"] = None,
|
|
24
|
+
usernames: Annotated[
|
|
25
|
+
list[str] | None,
|
|
26
|
+
"The usernames of the users to get. Prefer retrieving by user_ids and/or emails, "
|
|
27
|
+
"when available, since the performance is better.",
|
|
28
|
+
] = None,
|
|
29
|
+
emails: Annotated[list[str] | None, "The emails of the users to get"] = None,
|
|
30
|
+
) -> Annotated[dict[str, Any], "The users' information"]:
|
|
31
|
+
"""Get the information of one or more users in Slack by ID, username, and/or email.
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
users = await list_users(context)
|
|
40
|
-
available_users = ", ".join(f"{user['id']} ({user['name']})" for user in users["users"])
|
|
33
|
+
Provide any combination of user_ids, usernames, and/or emails. If you need to retrieve
|
|
34
|
+
data about multiple users, DO NOT CALL THE TOOL MULTIPLE TIMES. Instead, call it once
|
|
35
|
+
with all the user_ids, usernames, and/or emails. IF YOU CALL THIS TOOL MULTIPLE TIMES
|
|
36
|
+
UNNECESSARILY, YOU WILL RELEASE MORE CO2 IN THE ATMOSPHERE AND CONTRIBUTE TO GLOBAL WARMING.
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
If you need to get metadata or messages of a conversation, use the
|
|
39
|
+
`Slack.GetConversationMetadata` or `Slack.GetMessages` tool instead. These
|
|
40
|
+
tools accept user_ids, usernames, and/or emails. Do not retrieve users' info first,
|
|
41
|
+
as it is inefficient, releases more CO2 in the atmosphere, and contributes to climate change.
|
|
42
|
+
"""
|
|
43
|
+
users = await get_users_by_id_username_or_email(context, user_ids, usernames, emails)
|
|
44
|
+
return {"users": users}
|
|
48
45
|
|
|
49
|
-
user_dict_raw: dict[str, Any] = response.get("user", {}) or {}
|
|
50
|
-
user_dict = cast(SlackUser, user_dict_raw)
|
|
51
|
-
user = SlackUser(**user_dict)
|
|
52
|
-
return dict(**extract_basic_user_info(user))
|
|
53
46
|
|
|
54
|
-
|
|
55
|
-
@tool(
|
|
56
|
-
requires_auth=Slack(
|
|
57
|
-
scopes=["users:read", "users:read.email"],
|
|
58
|
-
)
|
|
59
|
-
)
|
|
47
|
+
@tool(requires_auth=Slack(scopes=["users:read", "users:read.email"]))
|
|
60
48
|
async def list_users(
|
|
61
49
|
context: ToolContext,
|
|
62
|
-
exclude_bots: Annotated[
|
|
63
|
-
|
|
50
|
+
exclude_bots: Annotated[
|
|
51
|
+
bool | None, "Whether to exclude bots from the results. Defaults to True."
|
|
52
|
+
] = True,
|
|
53
|
+
limit: Annotated[
|
|
54
|
+
int,
|
|
55
|
+
# The user object is relatively small, so we allow a higher limit than the default of 200.
|
|
56
|
+
"The maximum number of users to return. Defaults to 200. Maximum is 500.",
|
|
57
|
+
] = 200,
|
|
64
58
|
next_cursor: Annotated[str | None, "The next cursor token to use for pagination."] = None,
|
|
65
59
|
) -> Annotated[dict, "The users' info"]:
|
|
66
|
-
"""List all users in the authenticated user's Slack team.
|
|
60
|
+
"""List all users in the authenticated user's Slack team.
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
|
|
62
|
+
If you need to get metadata or messages of a conversation, use the
|
|
63
|
+
`Slack.GetConversationMetadata` tool or `Slack.GetMessages` tool instead. These
|
|
64
|
+
tools accept a user_id, username, and/or email. Do not use this tool to first retrieve user(s),
|
|
65
|
+
as it is inefficient and releases more CO2 in the atmosphere, contributing to climate change.
|
|
66
|
+
"""
|
|
67
|
+
limit = max(1, min(limit, 500))
|
|
68
|
+
slack_client = AsyncWebClient(token=context.get_auth_token_or_empty())
|
|
72
69
|
|
|
73
70
|
users, next_cursor = await async_paginate(
|
|
74
|
-
func=
|
|
71
|
+
func=slack_client.users_list,
|
|
75
72
|
response_key="members",
|
|
76
73
|
limit=limit,
|
|
77
74
|
next_cursor=cast(SlackPaginationNextCursor, next_cursor),
|
|
@@ -85,3 +82,18 @@ async def list_users(
|
|
|
85
82
|
]
|
|
86
83
|
|
|
87
84
|
return {"users": users, "next_cursor": next_cursor}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# NOTE: This tool is kept here for backwards compatibility.
|
|
88
|
+
# Use the `Slack.GetUsersInfo` tool instead.
|
|
89
|
+
@tool(requires_auth=Slack(scopes=["users:read", "users:read.email"]))
|
|
90
|
+
async def get_user_info_by_id(
|
|
91
|
+
context: ToolContext,
|
|
92
|
+
user_id: Annotated[str, "The ID of the user to get"],
|
|
93
|
+
) -> Annotated[dict[str, Any], "The user's information"]:
|
|
94
|
+
"""Get the information of a user in Slack.
|
|
95
|
+
|
|
96
|
+
This tool is deprecated. Use the `Slack.GetUsersInfo` tool instead.
|
|
97
|
+
"""
|
|
98
|
+
users = await get_users_info(context, user_ids=[user_id])
|
|
99
|
+
return cast(dict[str, Any], users["users"][0])
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
from arcade_tdk import ToolContext
|
|
5
|
+
from arcade_tdk.errors import ToolExecutionError
|
|
6
|
+
from slack_sdk.errors import SlackApiError
|
|
7
|
+
from slack_sdk.web.async_client import AsyncWebClient
|
|
8
|
+
|
|
9
|
+
from arcade_slack.constants import MAX_CONCURRENT_REQUESTS, MAX_PAGINATION_TIMEOUT_SECONDS
|
|
10
|
+
from arcade_slack.models import (
|
|
11
|
+
FindMultipleUsersByIdSentinel,
|
|
12
|
+
FindMultipleUsersByUsernameSentinel,
|
|
13
|
+
GetUserByEmailCaller,
|
|
14
|
+
)
|
|
15
|
+
from arcade_slack.utils import (
|
|
16
|
+
async_paginate,
|
|
17
|
+
build_multiple_users_retrieval_response,
|
|
18
|
+
cast_user_dict,
|
|
19
|
+
gather_with_concurrency_limit,
|
|
20
|
+
is_user_a_bot,
|
|
21
|
+
is_valid_email,
|
|
22
|
+
short_user_info,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def get_users_by_id_username_or_email(
|
|
27
|
+
context: ToolContext,
|
|
28
|
+
user_ids: str | list[str] | None = None,
|
|
29
|
+
usernames: str | list[str] | None = None,
|
|
30
|
+
emails: str | list[str] | None = None,
|
|
31
|
+
semaphore: asyncio.Semaphore | None = None,
|
|
32
|
+
) -> list[dict[str, Any]]:
|
|
33
|
+
"""Get the metadata of a user by their ID, username, or email.
|
|
34
|
+
|
|
35
|
+
Provide any combination of user_ids, usernames, and/or emails. Always prefer providing user_ids
|
|
36
|
+
and/or emails, when available, since the performance is better.
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(user_ids, str):
|
|
39
|
+
user_ids = [user_ids]
|
|
40
|
+
if isinstance(usernames, str):
|
|
41
|
+
usernames = [usernames]
|
|
42
|
+
if isinstance(emails, str):
|
|
43
|
+
emails = [emails]
|
|
44
|
+
|
|
45
|
+
if not any([user_ids, usernames, emails]):
|
|
46
|
+
raise ToolExecutionError("At least one of user_ids, usernames, or emails must be provided")
|
|
47
|
+
|
|
48
|
+
if not semaphore:
|
|
49
|
+
semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
|
|
50
|
+
|
|
51
|
+
user_retrieval_calls = []
|
|
52
|
+
|
|
53
|
+
auth_token = context.get_auth_token_or_empty()
|
|
54
|
+
|
|
55
|
+
if user_ids:
|
|
56
|
+
user_retrieval_calls.append(get_users_by_id(auth_token, user_ids, semaphore))
|
|
57
|
+
|
|
58
|
+
if usernames:
|
|
59
|
+
user_retrieval_calls.append(get_users_by_username(auth_token, usernames, semaphore))
|
|
60
|
+
|
|
61
|
+
if emails:
|
|
62
|
+
user_retrieval_calls.append(get_users_by_email(auth_token, emails, semaphore))
|
|
63
|
+
|
|
64
|
+
responses = await asyncio.gather(*user_retrieval_calls)
|
|
65
|
+
|
|
66
|
+
return await build_multiple_users_retrieval_response(context, responses)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def get_users_by_id(
|
|
70
|
+
auth_token: str,
|
|
71
|
+
user_ids: list[str],
|
|
72
|
+
semaphore: asyncio.Semaphore | None = None,
|
|
73
|
+
) -> dict[str, list]:
|
|
74
|
+
user_ids = list(set(user_ids))
|
|
75
|
+
|
|
76
|
+
if len(user_ids) == 1:
|
|
77
|
+
user = await get_single_user_by_id(auth_token, user_ids[0])
|
|
78
|
+
if not user:
|
|
79
|
+
return {"users": [], "not_found": user_ids}
|
|
80
|
+
else:
|
|
81
|
+
return {"users": [user], "not_found": []}
|
|
82
|
+
|
|
83
|
+
if not semaphore:
|
|
84
|
+
semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
|
|
85
|
+
|
|
86
|
+
async with semaphore:
|
|
87
|
+
slack_client = AsyncWebClient(token=auth_token)
|
|
88
|
+
response, _ = await async_paginate(
|
|
89
|
+
func=slack_client.users_list,
|
|
90
|
+
response_key="members",
|
|
91
|
+
sentinel=FindMultipleUsersByIdSentinel(user_ids=user_ids),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
user_ids_pending = set(user_ids)
|
|
95
|
+
users = []
|
|
96
|
+
|
|
97
|
+
for user in response:
|
|
98
|
+
user_dict = cast(dict, user)
|
|
99
|
+
if user_dict["id"] in user_ids_pending:
|
|
100
|
+
users.append(cast_user_dict(user_dict))
|
|
101
|
+
user_ids_pending.remove(user_dict["id"])
|
|
102
|
+
|
|
103
|
+
return {"users": users, "not_found": list(user_ids_pending)}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def get_single_user_by_id(auth_token: str, user_id: str) -> dict[str, Any] | None:
|
|
107
|
+
slack_client = AsyncWebClient(token=auth_token)
|
|
108
|
+
try:
|
|
109
|
+
response = await slack_client.users_info(user=user_id)
|
|
110
|
+
if not response.get("ok"):
|
|
111
|
+
return None
|
|
112
|
+
return cast_user_dict(response["user"])
|
|
113
|
+
except SlackApiError as e:
|
|
114
|
+
if "not_found" in e.response.get("error", ""):
|
|
115
|
+
return None
|
|
116
|
+
else:
|
|
117
|
+
message = f"There was an error getting the user with ID {user_id}."
|
|
118
|
+
slack_error_message = e.response.get("error", "Unknown Slack API error")
|
|
119
|
+
raise ToolExecutionError(
|
|
120
|
+
message=message,
|
|
121
|
+
developer_message=f"{message}: {slack_error_message}",
|
|
122
|
+
) from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def get_users_by_username(
|
|
126
|
+
auth_token: str,
|
|
127
|
+
usernames: list[str],
|
|
128
|
+
semaphore: asyncio.Semaphore | None = None,
|
|
129
|
+
) -> dict[str, list[dict]]:
|
|
130
|
+
usernames = list(set(usernames))
|
|
131
|
+
|
|
132
|
+
if not semaphore:
|
|
133
|
+
semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
|
|
134
|
+
|
|
135
|
+
slack_client = AsyncWebClient(token=auth_token)
|
|
136
|
+
|
|
137
|
+
async with semaphore:
|
|
138
|
+
users, _ = await async_paginate(
|
|
139
|
+
func=slack_client.users_list,
|
|
140
|
+
response_key="members",
|
|
141
|
+
max_pagination_timeout_seconds=MAX_PAGINATION_TIMEOUT_SECONDS,
|
|
142
|
+
sentinel=FindMultipleUsersByUsernameSentinel(usernames=usernames),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
users_found = []
|
|
146
|
+
usernames_lower = {username.casefold() for username in usernames}
|
|
147
|
+
usernames_pending = set(usernames)
|
|
148
|
+
available_users = []
|
|
149
|
+
|
|
150
|
+
for user in users:
|
|
151
|
+
if is_user_a_bot(user):
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
available_users.append(short_user_info(user))
|
|
155
|
+
|
|
156
|
+
if not isinstance(user.get("name"), str):
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
username_lower = user["name"].casefold()
|
|
160
|
+
|
|
161
|
+
if username_lower in usernames_lower:
|
|
162
|
+
users_found.append(cast_user_dict(user))
|
|
163
|
+
# Username/handle is unique in Slack, we can ignore it after finding a match
|
|
164
|
+
for pending_username in usernames_pending:
|
|
165
|
+
if pending_username.casefold() == username_lower:
|
|
166
|
+
usernames_pending.remove(pending_username)
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
response: dict[str, Any] = {"users": users_found}
|
|
170
|
+
|
|
171
|
+
if usernames_pending:
|
|
172
|
+
response["not_found"] = list(usernames_pending)
|
|
173
|
+
response["available_users"] = available_users
|
|
174
|
+
|
|
175
|
+
return response
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def get_users_by_email(
|
|
179
|
+
auth_token: str,
|
|
180
|
+
emails: list[str],
|
|
181
|
+
semaphore: asyncio.Semaphore | None = None,
|
|
182
|
+
) -> dict[str, list[dict]]:
|
|
183
|
+
emails = list(set(emails))
|
|
184
|
+
|
|
185
|
+
for email in emails:
|
|
186
|
+
if not is_valid_email(email):
|
|
187
|
+
raise ToolExecutionError(f"Invalid email address: {email}")
|
|
188
|
+
|
|
189
|
+
if not semaphore:
|
|
190
|
+
semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
|
|
191
|
+
|
|
192
|
+
slack_client = AsyncWebClient(token=auth_token)
|
|
193
|
+
callers = [GetUserByEmailCaller(slack_client.users_lookupByEmail, email) for email in emails]
|
|
194
|
+
|
|
195
|
+
results = await gather_with_concurrency_limit(
|
|
196
|
+
coroutine_callers=callers,
|
|
197
|
+
semaphore=semaphore,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
users = []
|
|
201
|
+
emails_not_found = []
|
|
202
|
+
|
|
203
|
+
for result in results:
|
|
204
|
+
if result["user"]:
|
|
205
|
+
users.append(cast_user_dict(result["user"]))
|
|
206
|
+
else:
|
|
207
|
+
emails_not_found.append(result["email"])
|
|
208
|
+
|
|
209
|
+
response: dict[str, Any] = {"users": users}
|
|
210
|
+
|
|
211
|
+
if emails_not_found:
|
|
212
|
+
response["not_found"] = emails_not_found
|
|
213
|
+
|
|
214
|
+
return response
|