arcade-slack 0.1.6__py3-none-any.whl → 0.4.6__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 +13 -0
- arcade_slack/critics.py +34 -0
- arcade_slack/custom_types.py +26 -0
- arcade_slack/exceptions.py +30 -0
- arcade_slack/models.py +206 -0
- arcade_slack/tools/chat.py +868 -56
- arcade_slack/tools/users.py +87 -0
- arcade_slack/utils.py +447 -0
- arcade_slack-0.4.6.dist-info/METADATA +23 -0
- arcade_slack-0.4.6.dist-info/RECORD +14 -0
- {arcade_slack-0.1.6.dist-info → arcade_slack-0.4.6.dist-info}/WHEEL +1 -1
- arcade_slack-0.4.6.dist-info/licenses/LICENSE +21 -0
- arcade_slack-0.1.6.dist-info/METADATA +0 -14
- arcade_slack-0.1.6.dist-info/RECORD +0 -6
arcade_slack/tools/chat.py
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Annotated, cast
|
|
2
4
|
|
|
3
|
-
from
|
|
5
|
+
from arcade_tdk import ToolContext, tool
|
|
6
|
+
from arcade_tdk.auth import Slack
|
|
7
|
+
from arcade_tdk.errors import RetryableToolError, ToolExecutionError
|
|
4
8
|
from slack_sdk.errors import SlackApiError
|
|
9
|
+
from slack_sdk.web.async_client import AsyncWebClient
|
|
5
10
|
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
|
|
11
|
+
from arcade_slack.constants import MAX_PAGINATION_TIMEOUT_SECONDS
|
|
12
|
+
from arcade_slack.exceptions import (
|
|
13
|
+
ItemNotFoundError,
|
|
14
|
+
UsernameNotFoundError,
|
|
15
|
+
)
|
|
16
|
+
from arcade_slack.models import (
|
|
17
|
+
ConversationType,
|
|
18
|
+
SlackUserList,
|
|
19
|
+
)
|
|
20
|
+
from arcade_slack.tools.users import get_user_info_by_id, list_users
|
|
21
|
+
from arcade_slack.utils import (
|
|
22
|
+
async_paginate,
|
|
23
|
+
convert_conversation_type_to_slack_name,
|
|
24
|
+
convert_datetime_to_unix_timestamp,
|
|
25
|
+
convert_relative_datetime_to_unix_timestamp,
|
|
26
|
+
enrich_message_datetime,
|
|
27
|
+
extract_conversation_metadata,
|
|
28
|
+
format_users,
|
|
29
|
+
get_user_by_username,
|
|
30
|
+
retrieve_conversations_by_user_ids,
|
|
31
|
+
)
|
|
9
32
|
|
|
10
33
|
|
|
11
34
|
@tool(
|
|
@@ -18,24 +41,33 @@ from arcade.sdk.errors import RetryableToolError, ToolExecutionError
|
|
|
18
41
|
],
|
|
19
42
|
)
|
|
20
43
|
)
|
|
21
|
-
def send_dm_to_user(
|
|
44
|
+
async def send_dm_to_user(
|
|
22
45
|
context: ToolContext,
|
|
23
46
|
user_name: Annotated[
|
|
24
47
|
str,
|
|
25
|
-
|
|
48
|
+
(
|
|
49
|
+
"The Slack username of the person you want to message. "
|
|
50
|
+
"Slack usernames are ALWAYS lowercase."
|
|
51
|
+
),
|
|
26
52
|
],
|
|
27
53
|
message: Annotated[str, "The message you want to send"],
|
|
28
|
-
):
|
|
54
|
+
) -> Annotated[dict, "The response from the Slack API"]:
|
|
29
55
|
"""Send a direct message to a user in Slack."""
|
|
30
56
|
|
|
31
|
-
|
|
57
|
+
token = (
|
|
58
|
+
context.authorization.token if context.authorization and context.authorization.token else ""
|
|
59
|
+
)
|
|
60
|
+
slackClient = AsyncWebClient(token=token)
|
|
32
61
|
|
|
33
62
|
try:
|
|
34
63
|
# Step 1: Retrieve the user's Slack ID based on their username
|
|
35
|
-
|
|
64
|
+
user_list_response = await slackClient.users_list()
|
|
36
65
|
user_id = None
|
|
37
|
-
for user in
|
|
38
|
-
|
|
66
|
+
for user in user_list_response["members"]:
|
|
67
|
+
response_user_name = (
|
|
68
|
+
"" if not isinstance(user.get("name"), str) else user["name"].lower()
|
|
69
|
+
)
|
|
70
|
+
if response_user_name == user_name.lower():
|
|
39
71
|
user_id = user["id"]
|
|
40
72
|
break
|
|
41
73
|
|
|
@@ -43,16 +75,16 @@ def send_dm_to_user(
|
|
|
43
75
|
raise RetryableToolError(
|
|
44
76
|
"User not found",
|
|
45
77
|
developer_message=f"User with username '{user_name}' not found.",
|
|
46
|
-
additional_prompt_content=format_users(
|
|
78
|
+
additional_prompt_content=format_users(cast(SlackUserList, user_list_response)),
|
|
47
79
|
retry_after_ms=500, # Play nice with Slack API rate limits
|
|
48
80
|
)
|
|
49
81
|
|
|
50
82
|
# Step 2: Retrieve the DM channel ID with the user
|
|
51
|
-
im_response = slackClient.conversations_open(users=[user_id])
|
|
83
|
+
im_response = await slackClient.conversations_open(users=[user_id])
|
|
52
84
|
dm_channel_id = im_response["channel"]["id"]
|
|
53
85
|
|
|
54
86
|
# Step 3: Send the message as if it's from you (because we're using a user token)
|
|
55
|
-
slackClient.chat_postMessage(channel=dm_channel_id, text=message)
|
|
87
|
+
response = await slackClient.chat_postMessage(channel=dm_channel_id, text=message)
|
|
56
88
|
|
|
57
89
|
except SlackApiError as e:
|
|
58
90
|
error_message = e.response["error"] if "error" in e.response else str(e)
|
|
@@ -60,16 +92,8 @@ def send_dm_to_user(
|
|
|
60
92
|
"Error sending message",
|
|
61
93
|
developer_message=f"Slack API Error: {error_message}",
|
|
62
94
|
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def format_users(userListResponse: dict) -> str:
|
|
66
|
-
csv_string = "All active Slack users:\n\nname,real_name\n"
|
|
67
|
-
for user in userListResponse["members"]:
|
|
68
|
-
if not user.get("deleted", False):
|
|
69
|
-
name = user.get("name", "")
|
|
70
|
-
real_name = user.get("profile", {}).get("real_name", "")
|
|
71
|
-
csv_string += f"{name},{real_name}\n"
|
|
72
|
-
return csv_string.strip()
|
|
95
|
+
else:
|
|
96
|
+
return {"response": response.data}
|
|
73
97
|
|
|
74
98
|
|
|
75
99
|
@tool(
|
|
@@ -81,37 +105,24 @@ def format_users(userListResponse: dict) -> str:
|
|
|
81
105
|
],
|
|
82
106
|
)
|
|
83
107
|
)
|
|
84
|
-
def send_message_to_channel(
|
|
108
|
+
async def send_message_to_channel(
|
|
85
109
|
context: ToolContext,
|
|
86
|
-
channel_name: Annotated[
|
|
87
|
-
str,
|
|
88
|
-
"The Slack channel name where you want to send the message. Slack channel names are ALWAYS lowercase.",
|
|
89
|
-
],
|
|
110
|
+
channel_name: Annotated[str, "The Slack channel name where you want to send the message. "],
|
|
90
111
|
message: Annotated[str, "The message you want to send"],
|
|
91
|
-
):
|
|
112
|
+
) -> Annotated[dict, "The response from the Slack API"]:
|
|
92
113
|
"""Send a message to a channel in Slack."""
|
|
93
114
|
|
|
94
|
-
slackClient = WebClient(token=context.authorization.token)
|
|
95
|
-
|
|
96
115
|
try:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
channel_id = channel["id"]
|
|
103
|
-
break
|
|
116
|
+
slackClient = AsyncWebClient(
|
|
117
|
+
token=context.authorization.token
|
|
118
|
+
if context.authorization and context.authorization.token
|
|
119
|
+
else ""
|
|
120
|
+
)
|
|
104
121
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"Channel not found",
|
|
108
|
-
developer_message=f"Channel with name '{channel_name}' not found.",
|
|
109
|
-
additional_prompt_content=format_channels(channels_response),
|
|
110
|
-
retry_after_ms=500, # Play nice with Slack API rate limits
|
|
111
|
-
)
|
|
122
|
+
channel = await get_channel_metadata_by_name(context=context, channel_name=channel_name)
|
|
123
|
+
channel_id = channel["id"]
|
|
112
124
|
|
|
113
|
-
|
|
114
|
-
slackClient.chat_postMessage(channel=channel_id, text=message)
|
|
125
|
+
response = await slackClient.chat_postMessage(channel=channel_id, text=message)
|
|
115
126
|
|
|
116
127
|
except SlackApiError as e:
|
|
117
128
|
error_message = e.response["error"] if "error" in e.response else str(e)
|
|
@@ -119,12 +130,813 @@ def send_message_to_channel(
|
|
|
119
130
|
"Error sending message",
|
|
120
131
|
developer_message=f"Slack API Error: {error_message}",
|
|
121
132
|
)
|
|
133
|
+
else:
|
|
134
|
+
return {"response": response.data}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@tool(
|
|
138
|
+
requires_auth=Slack(
|
|
139
|
+
scopes=[
|
|
140
|
+
"channels:read",
|
|
141
|
+
"groups:read",
|
|
142
|
+
"im:read",
|
|
143
|
+
"mpim:read",
|
|
144
|
+
"users:read",
|
|
145
|
+
"users:read.email",
|
|
146
|
+
],
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
async def get_members_in_conversation_by_id(
|
|
150
|
+
context: ToolContext,
|
|
151
|
+
conversation_id: Annotated[str, "The ID of the conversation to get members for"],
|
|
152
|
+
limit: Annotated[int | None, "The maximum number of members to return."] = None,
|
|
153
|
+
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
|
|
154
|
+
) -> Annotated[dict, "Information about each member in the conversation"]:
|
|
155
|
+
"""Get the members of a conversation in Slack by the conversation's ID."""
|
|
156
|
+
token = (
|
|
157
|
+
context.authorization.token if context.authorization and context.authorization.token else ""
|
|
158
|
+
)
|
|
159
|
+
slackClient = AsyncWebClient(token=token)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
member_ids, next_cursor = await async_paginate(
|
|
163
|
+
slackClient.conversations_members,
|
|
164
|
+
"members",
|
|
165
|
+
limit=limit,
|
|
166
|
+
next_cursor=next_cursor,
|
|
167
|
+
channel=conversation_id,
|
|
168
|
+
)
|
|
169
|
+
except SlackApiError as e:
|
|
170
|
+
if e.response["error"] == "channel_not_found":
|
|
171
|
+
conversations = await list_conversations_metadata(context)
|
|
172
|
+
available_conversations = ", ".join(
|
|
173
|
+
f"{conversation['id']} ({conversation['name']})"
|
|
174
|
+
for conversation in conversations["conversations"]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
raise RetryableToolError(
|
|
178
|
+
"Conversation not found",
|
|
179
|
+
developer_message=f"Conversation with ID '{conversation_id}' not found.",
|
|
180
|
+
additional_prompt_content=f"Available conversations: {available_conversations}",
|
|
181
|
+
retry_after_ms=500,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Get the members' info
|
|
185
|
+
# TODO: This will probably hit rate limits. We should probably call list_users() and
|
|
186
|
+
# then filter the results instead.
|
|
187
|
+
members = await asyncio.gather(*[
|
|
188
|
+
get_user_info_by_id(context, member_id) for member_id in member_ids
|
|
189
|
+
])
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"members": [member for member in members if not member.get("is_bot")],
|
|
193
|
+
"next_cursor": next_cursor,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@tool(
|
|
198
|
+
requires_auth=Slack(
|
|
199
|
+
scopes=[
|
|
200
|
+
"channels:read",
|
|
201
|
+
"groups:read",
|
|
202
|
+
"im:read",
|
|
203
|
+
"mpim:read",
|
|
204
|
+
"users:read",
|
|
205
|
+
"users:read.email",
|
|
206
|
+
],
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
async def get_members_in_channel_by_name(
|
|
210
|
+
context: ToolContext,
|
|
211
|
+
channel_name: Annotated[str, "The name of the channel to get members for"],
|
|
212
|
+
limit: Annotated[int | None, "The maximum number of members to return."] = None,
|
|
213
|
+
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
|
|
214
|
+
) -> Annotated[dict, "The channel members' IDs and Names"]:
|
|
215
|
+
"""Get the members of a conversation in Slack by the conversation's name."""
|
|
216
|
+
channel = await get_channel_metadata_by_name(context=context, channel_name=channel_name)
|
|
217
|
+
|
|
218
|
+
return await get_members_in_conversation_by_id( # type: ignore[no-any-return]
|
|
219
|
+
context=context,
|
|
220
|
+
conversation_id=channel["id"],
|
|
221
|
+
limit=limit,
|
|
222
|
+
next_cursor=next_cursor,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# TODO: make the function accept a current unix timestamp argument to allow testing without
|
|
227
|
+
# mocking. Have to wait until arcade.core.annotations.Inferrable is implemented, so that we
|
|
228
|
+
# can avoid exposing this arg to the LLM.
|
|
229
|
+
@tool(
|
|
230
|
+
requires_auth=Slack(
|
|
231
|
+
scopes=["channels:history", "groups:history", "im:history", "mpim:history"],
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
async def get_messages_in_conversation_by_id(
|
|
235
|
+
context: ToolContext,
|
|
236
|
+
conversation_id: Annotated[str, "The ID of the conversation to get history for"],
|
|
237
|
+
oldest_relative: Annotated[
|
|
238
|
+
str | None,
|
|
239
|
+
(
|
|
240
|
+
"The oldest message to include in the results, specified as a time offset from the "
|
|
241
|
+
"current time in the format 'DD:HH:MM'"
|
|
242
|
+
),
|
|
243
|
+
] = None,
|
|
244
|
+
latest_relative: Annotated[
|
|
245
|
+
str | None,
|
|
246
|
+
(
|
|
247
|
+
"The latest message to include in the results, specified as a time offset from the "
|
|
248
|
+
"current time in the format 'DD:HH:MM'"
|
|
249
|
+
),
|
|
250
|
+
] = None,
|
|
251
|
+
oldest_datetime: Annotated[
|
|
252
|
+
str | None,
|
|
253
|
+
(
|
|
254
|
+
"The oldest message to include in the results, specified as a datetime object in the "
|
|
255
|
+
"format 'YYYY-MM-DD HH:MM:SS'"
|
|
256
|
+
),
|
|
257
|
+
] = None,
|
|
258
|
+
latest_datetime: Annotated[
|
|
259
|
+
str | None,
|
|
260
|
+
(
|
|
261
|
+
"The latest message to include in the results, specified as a datetime object in the "
|
|
262
|
+
"format 'YYYY-MM-DD HH:MM:SS'"
|
|
263
|
+
),
|
|
264
|
+
] = None,
|
|
265
|
+
limit: Annotated[int | None, "The maximum number of messages to return."] = None,
|
|
266
|
+
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
|
|
267
|
+
) -> Annotated[
|
|
268
|
+
dict,
|
|
269
|
+
(
|
|
270
|
+
"The messages in a conversation and next cursor for paginating results (when "
|
|
271
|
+
"there are additional messages to retrieve)."
|
|
272
|
+
),
|
|
273
|
+
]:
|
|
274
|
+
"""Get the messages in a conversation by the conversation's ID.
|
|
275
|
+
|
|
276
|
+
A conversation can be a channel, a DM, or a group DM.
|
|
277
|
+
|
|
278
|
+
To filter by an absolute datetime, use 'oldest_datetime' and/or 'latest_datetime'. If
|
|
279
|
+
only 'oldest_datetime' is provided, it returns messages from the oldest_datetime to the
|
|
280
|
+
current time. If only 'latest_datetime' is provided, it returns messages since the
|
|
281
|
+
beginning of the conversation to the latest_datetime.
|
|
282
|
+
|
|
283
|
+
To filter by a relative datetime (e.g. 3 days ago, 1 hour ago, etc.), use
|
|
284
|
+
'oldest_relative' and/or 'latest_relative'. If only 'oldest_relative' is provided, it returns
|
|
285
|
+
messages from the oldest_relative to the current time. If only 'latest_relative' is provided,
|
|
286
|
+
it returns messages from the current time to the latest_relative.
|
|
287
|
+
|
|
288
|
+
Do not provide both 'oldest_datetime' and 'oldest_relative' or both 'latest_datetime' and
|
|
289
|
+
'latest_relative'.
|
|
290
|
+
|
|
291
|
+
Leave all arguments with the default None to get messages without date/time filtering"""
|
|
292
|
+
error_message = None
|
|
293
|
+
if oldest_datetime and oldest_relative:
|
|
294
|
+
error_message = "Cannot specify both 'oldest_datetime' and 'oldest_relative'."
|
|
295
|
+
|
|
296
|
+
if latest_datetime and latest_relative:
|
|
297
|
+
error_message = "Cannot specify both 'latest_datetime' and 'latest_relative'."
|
|
298
|
+
|
|
299
|
+
if error_message:
|
|
300
|
+
raise ToolExecutionError(error_message, developer_message=error_message)
|
|
301
|
+
|
|
302
|
+
current_unix_timestamp = int(datetime.now(timezone.utc).timestamp())
|
|
303
|
+
|
|
304
|
+
if latest_relative:
|
|
305
|
+
latest_timestamp = convert_relative_datetime_to_unix_timestamp(
|
|
306
|
+
latest_relative, current_unix_timestamp
|
|
307
|
+
)
|
|
308
|
+
elif latest_datetime:
|
|
309
|
+
latest_timestamp = convert_datetime_to_unix_timestamp(latest_datetime)
|
|
310
|
+
else:
|
|
311
|
+
latest_timestamp = None
|
|
312
|
+
|
|
313
|
+
if oldest_relative:
|
|
314
|
+
oldest_timestamp = convert_relative_datetime_to_unix_timestamp(
|
|
315
|
+
oldest_relative, current_unix_timestamp
|
|
316
|
+
)
|
|
317
|
+
elif oldest_datetime:
|
|
318
|
+
oldest_timestamp = convert_datetime_to_unix_timestamp(oldest_datetime)
|
|
319
|
+
else:
|
|
320
|
+
oldest_timestamp = None
|
|
321
|
+
|
|
322
|
+
token = (
|
|
323
|
+
context.authorization.token if context.authorization and context.authorization.token else ""
|
|
324
|
+
)
|
|
325
|
+
slackClient = AsyncWebClient(token=token)
|
|
326
|
+
|
|
327
|
+
datetime_args = {}
|
|
328
|
+
if oldest_timestamp:
|
|
329
|
+
datetime_args["oldest"] = oldest_timestamp
|
|
330
|
+
if latest_timestamp:
|
|
331
|
+
datetime_args["latest"] = latest_timestamp
|
|
332
|
+
|
|
333
|
+
response, next_cursor = await async_paginate(
|
|
334
|
+
slackClient.conversations_history,
|
|
335
|
+
"messages",
|
|
336
|
+
limit=limit,
|
|
337
|
+
next_cursor=next_cursor,
|
|
338
|
+
channel=conversation_id,
|
|
339
|
+
include_all_metadata=True,
|
|
340
|
+
inclusive=True, # Include messages at the start and end of the time range
|
|
341
|
+
**datetime_args,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
messages = [enrich_message_datetime(message) for message in response]
|
|
345
|
+
|
|
346
|
+
return {"messages": messages, "next_cursor": next_cursor}
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# TODO: make the function accept a current unix timestamp argument to allow testing without
|
|
350
|
+
# mocking. Have to wait until arcade.core.annotations.Inferrable is implemented, so that we
|
|
351
|
+
# can avoid exposing this arg to the LLM.
|
|
352
|
+
@tool(
|
|
353
|
+
requires_auth=Slack(
|
|
354
|
+
scopes=[
|
|
355
|
+
"channels:history",
|
|
356
|
+
"channels:read",
|
|
357
|
+
"groups:history",
|
|
358
|
+
"groups:read",
|
|
359
|
+
"im:history",
|
|
360
|
+
"im:read",
|
|
361
|
+
"mpim:history",
|
|
362
|
+
"mpim:read",
|
|
363
|
+
],
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
async def get_messages_in_channel_by_name(
|
|
367
|
+
context: ToolContext,
|
|
368
|
+
channel_name: Annotated[str, "The name of the channel"],
|
|
369
|
+
oldest_relative: Annotated[
|
|
370
|
+
str | None,
|
|
371
|
+
(
|
|
372
|
+
"The oldest message to include in the results, specified as a time offset from the "
|
|
373
|
+
"current time in the format 'DD:HH:MM'"
|
|
374
|
+
),
|
|
375
|
+
] = None,
|
|
376
|
+
latest_relative: Annotated[
|
|
377
|
+
str | None,
|
|
378
|
+
(
|
|
379
|
+
"The latest message to include in the results, specified as a time offset from the "
|
|
380
|
+
"current time in the format 'DD:HH:MM'"
|
|
381
|
+
),
|
|
382
|
+
] = None,
|
|
383
|
+
oldest_datetime: Annotated[
|
|
384
|
+
str | None,
|
|
385
|
+
(
|
|
386
|
+
"The oldest message to include in the results, specified as a datetime object in the "
|
|
387
|
+
"format 'YYYY-MM-DD HH:MM:SS'"
|
|
388
|
+
),
|
|
389
|
+
] = None,
|
|
390
|
+
latest_datetime: Annotated[
|
|
391
|
+
str | None,
|
|
392
|
+
(
|
|
393
|
+
"The latest message to include in the results, specified as a datetime object in the "
|
|
394
|
+
"format 'YYYY-MM-DD HH:MM:SS'"
|
|
395
|
+
),
|
|
396
|
+
] = None,
|
|
397
|
+
limit: Annotated[int | None, "The maximum number of messages to return."] = None,
|
|
398
|
+
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
|
|
399
|
+
) -> Annotated[
|
|
400
|
+
dict,
|
|
401
|
+
(
|
|
402
|
+
"The messages in a channel and next cursor for paginating results (when "
|
|
403
|
+
"there are additional messages to retrieve)."
|
|
404
|
+
),
|
|
405
|
+
]:
|
|
406
|
+
"""Get the messages in a channel by the channel's name.
|
|
407
|
+
|
|
408
|
+
To filter messages by an absolute datetime, use 'oldest_datetime' and/or 'latest_datetime'. If
|
|
409
|
+
only 'oldest_datetime' is provided, it will return messages from the oldest_datetime to the
|
|
410
|
+
current time. If only 'latest_datetime' is provided, it will return messages since the
|
|
411
|
+
beginning of the channel to the latest_datetime.
|
|
412
|
+
|
|
413
|
+
To filter messages by a relative datetime (e.g. 3 days ago, 1 hour ago, etc.), use
|
|
414
|
+
'oldest_relative' and/or 'latest_relative'. If only 'oldest_relative' is provided, it will
|
|
415
|
+
return messages from the oldest_relative to the current time. If only 'latest_relative' is
|
|
416
|
+
provided, it will return messages from the current time to the latest_relative.
|
|
417
|
+
|
|
418
|
+
Do not provide both 'oldest_datetime' and 'oldest_relative' or both 'latest_datetime' and
|
|
419
|
+
'latest_relative'.
|
|
420
|
+
|
|
421
|
+
Leave all arguments with the default None to get messages without date/time filtering"""
|
|
422
|
+
channel = await get_channel_metadata_by_name(context=context, channel_name=channel_name)
|
|
423
|
+
|
|
424
|
+
return await get_messages_in_conversation_by_id( # type: ignore[no-any-return]
|
|
425
|
+
context=context,
|
|
426
|
+
conversation_id=channel["id"],
|
|
427
|
+
oldest_relative=oldest_relative,
|
|
428
|
+
latest_relative=latest_relative,
|
|
429
|
+
oldest_datetime=oldest_datetime,
|
|
430
|
+
latest_datetime=latest_datetime,
|
|
431
|
+
limit=limit,
|
|
432
|
+
next_cursor=next_cursor,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@tool(requires_auth=Slack(scopes=["im:history", "im:read"]))
|
|
437
|
+
async def get_messages_in_direct_message_conversation_by_username(
|
|
438
|
+
context: ToolContext,
|
|
439
|
+
username: Annotated[str, "The username of the user to get messages from"],
|
|
440
|
+
oldest_relative: Annotated[
|
|
441
|
+
str | None,
|
|
442
|
+
(
|
|
443
|
+
"The oldest message to include in the results, specified as a time offset from the "
|
|
444
|
+
"current time in the format 'DD:HH:MM'"
|
|
445
|
+
),
|
|
446
|
+
] = None,
|
|
447
|
+
latest_relative: Annotated[
|
|
448
|
+
str | None,
|
|
449
|
+
(
|
|
450
|
+
"The latest message to include in the results, specified as a time offset from the "
|
|
451
|
+
"current time in the format 'DD:HH:MM'"
|
|
452
|
+
),
|
|
453
|
+
] = None,
|
|
454
|
+
oldest_datetime: Annotated[
|
|
455
|
+
str | None,
|
|
456
|
+
(
|
|
457
|
+
"The oldest message to include in the results, specified as a datetime object in the "
|
|
458
|
+
"format 'YYYY-MM-DD HH:MM:SS'"
|
|
459
|
+
),
|
|
460
|
+
] = None,
|
|
461
|
+
latest_datetime: Annotated[
|
|
462
|
+
str | None,
|
|
463
|
+
(
|
|
464
|
+
"The latest message to include in the results, specified as a datetime object in the "
|
|
465
|
+
"format 'YYYY-MM-DD HH:MM:SS'"
|
|
466
|
+
),
|
|
467
|
+
] = None,
|
|
468
|
+
limit: Annotated[int | None, "The maximum number of messages to return."] = None,
|
|
469
|
+
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
|
|
470
|
+
) -> Annotated[
|
|
471
|
+
dict,
|
|
472
|
+
(
|
|
473
|
+
"The messages in a direct message conversation and next cursor for paginating results "
|
|
474
|
+
"when there are additional messages to retrieve."
|
|
475
|
+
),
|
|
476
|
+
]:
|
|
477
|
+
"""Get the messages in a direct conversation by the user's name.
|
|
478
|
+
|
|
479
|
+
To filter messages by an absolute datetime, use 'oldest_datetime' and/or 'latest_datetime'. If
|
|
480
|
+
only 'oldest_datetime' is provided, it will return messages from the oldest_datetime to the
|
|
481
|
+
current time. If only 'latest_datetime' is provided, it will return messages since the
|
|
482
|
+
beginning of the conversation to the latest_datetime.
|
|
483
|
+
|
|
484
|
+
To filter messages by a relative datetime (e.g. 3 days ago, 1 hour ago, etc.), use
|
|
485
|
+
'oldest_relative' and/or 'latest_relative'. If only 'oldest_relative' is provided, it will
|
|
486
|
+
return messages from the oldest_relative to the current time. If only 'latest_relative' is
|
|
487
|
+
provided, it will return messages from the current time to the latest_relative.
|
|
488
|
+
|
|
489
|
+
Do not provide both 'oldest_datetime' and 'oldest_relative' or both 'latest_datetime' and
|
|
490
|
+
'latest_relative'.
|
|
491
|
+
|
|
492
|
+
Leave all arguments with the default None to get messages without date/time filtering"""
|
|
493
|
+
direct_conversation = await get_direct_message_conversation_metadata_by_username(
|
|
494
|
+
context=context, username=username
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
return await get_messages_in_conversation_by_id( # type: ignore[no-any-return]
|
|
498
|
+
context=context,
|
|
499
|
+
conversation_id=direct_conversation["id"],
|
|
500
|
+
oldest_relative=oldest_relative,
|
|
501
|
+
latest_relative=latest_relative,
|
|
502
|
+
oldest_datetime=oldest_datetime,
|
|
503
|
+
latest_datetime=latest_datetime,
|
|
504
|
+
limit=limit,
|
|
505
|
+
next_cursor=next_cursor,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@tool(requires_auth=Slack(scopes=["im:history", "im:read"]))
|
|
510
|
+
async def get_messages_in_multi_person_dm_conversation_by_usernames(
|
|
511
|
+
context: ToolContext,
|
|
512
|
+
usernames: Annotated[list[str], "The usernames of the users to get messages from"],
|
|
513
|
+
oldest_relative: Annotated[
|
|
514
|
+
str | None,
|
|
515
|
+
(
|
|
516
|
+
"The oldest message to include in the results, specified as a time offset from the "
|
|
517
|
+
"current time in the format 'DD:HH:MM'"
|
|
518
|
+
),
|
|
519
|
+
] = None,
|
|
520
|
+
latest_relative: Annotated[
|
|
521
|
+
str | None,
|
|
522
|
+
(
|
|
523
|
+
"The latest message to include in the results, specified as a time offset from the "
|
|
524
|
+
"current time in the format 'DD:HH:MM'"
|
|
525
|
+
),
|
|
526
|
+
] = None,
|
|
527
|
+
oldest_datetime: Annotated[
|
|
528
|
+
str | None,
|
|
529
|
+
(
|
|
530
|
+
"The oldest message to include in the results, specified as a datetime object in the "
|
|
531
|
+
"format 'YYYY-MM-DD HH:MM:SS'"
|
|
532
|
+
),
|
|
533
|
+
] = None,
|
|
534
|
+
latest_datetime: Annotated[
|
|
535
|
+
str | None,
|
|
536
|
+
(
|
|
537
|
+
"The latest message to include in the results, specified as a datetime object in the "
|
|
538
|
+
"format 'YYYY-MM-DD HH:MM:SS'"
|
|
539
|
+
),
|
|
540
|
+
] = None,
|
|
541
|
+
limit: Annotated[int | None, "The maximum number of messages to return."] = None,
|
|
542
|
+
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
|
|
543
|
+
) -> Annotated[
|
|
544
|
+
dict,
|
|
545
|
+
(
|
|
546
|
+
"The messages in a multi-person direct message conversation and next cursor for "
|
|
547
|
+
"paginating results (when there are additional messages to retrieve)."
|
|
548
|
+
),
|
|
549
|
+
]:
|
|
550
|
+
"""Get the messages in a multi-person direct message conversation by the usernames.
|
|
551
|
+
|
|
552
|
+
To filter messages by an absolute datetime, use 'oldest_datetime' and/or 'latest_datetime'. If
|
|
553
|
+
only 'oldest_datetime' is provided, it will return messages from the oldest_datetime to the
|
|
554
|
+
current time. If only 'latest_datetime' is provided, it will return messages since the
|
|
555
|
+
beginning of the conversation to the latest_datetime.
|
|
556
|
+
|
|
557
|
+
To filter messages by a relative datetime (e.g. 3 days ago, 1 hour ago, etc.), use
|
|
558
|
+
'oldest_relative' and/or 'latest_relative'. If only 'oldest_relative' is provided, it will
|
|
559
|
+
return messages from the oldest_relative to the current time. If only 'latest_relative' is
|
|
560
|
+
provided, it will return messages from the current time to the latest_relative.
|
|
561
|
+
|
|
562
|
+
Do not provide both 'oldest_datetime' and 'oldest_relative' or both 'latest_datetime' and
|
|
563
|
+
'latest_relative'.
|
|
564
|
+
|
|
565
|
+
Leave all arguments with the default None to get messages without date/time filtering"""
|
|
566
|
+
direct_conversation = await get_multi_person_dm_conversation_metadata_by_usernames(
|
|
567
|
+
context=context, usernames=usernames
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
return await get_messages_in_conversation_by_id( # type: ignore[no-any-return]
|
|
571
|
+
context=context,
|
|
572
|
+
conversation_id=direct_conversation["id"],
|
|
573
|
+
oldest_relative=oldest_relative,
|
|
574
|
+
latest_relative=latest_relative,
|
|
575
|
+
oldest_datetime=oldest_datetime,
|
|
576
|
+
latest_datetime=latest_datetime,
|
|
577
|
+
limit=limit,
|
|
578
|
+
next_cursor=next_cursor,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
@tool(
|
|
583
|
+
requires_auth=Slack(
|
|
584
|
+
scopes=["channels:read", "groups:read", "im:read", "mpim:read"],
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
async def get_conversation_metadata_by_id(
|
|
588
|
+
context: ToolContext,
|
|
589
|
+
conversation_id: Annotated[str, "The ID of the conversation to get metadata for"],
|
|
590
|
+
) -> Annotated[dict, "The conversation metadata"]:
|
|
591
|
+
"""Get the metadata of a conversation in Slack searching by its ID.
|
|
592
|
+
|
|
593
|
+
This tool does not return the messages in a conversation. To get the messages, use the
|
|
594
|
+
`get_messages_in_conversation_by_id` tool."""
|
|
595
|
+
token = (
|
|
596
|
+
context.authorization.token if context.authorization and context.authorization.token else ""
|
|
597
|
+
)
|
|
598
|
+
slackClient = AsyncWebClient(token=token)
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
response = await slackClient.conversations_info(
|
|
602
|
+
channel=conversation_id,
|
|
603
|
+
include_locale=True,
|
|
604
|
+
include_num_members=True,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
except SlackApiError as e:
|
|
608
|
+
if e.response.get("error") == "channel_not_found":
|
|
609
|
+
conversations = await list_conversations_metadata(context)
|
|
610
|
+
available_conversations = ", ".join(
|
|
611
|
+
f"{conversation['id']} ({conversation['name']})"
|
|
612
|
+
for conversation in conversations["conversations"]
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
raise RetryableToolError(
|
|
616
|
+
"Conversation not found",
|
|
617
|
+
developer_message=f"Conversation with ID '{conversation_id}' not found.",
|
|
618
|
+
additional_prompt_content=f"Available conversations: {available_conversations}",
|
|
619
|
+
retry_after_ms=500,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
raise
|
|
623
|
+
|
|
624
|
+
return dict(**extract_conversation_metadata(response["channel"]))
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@tool(requires_auth=Slack(scopes=["channels:read", "groups:read"]))
|
|
628
|
+
async def get_channel_metadata_by_name(
|
|
629
|
+
context: ToolContext,
|
|
630
|
+
channel_name: Annotated[str, "The name of the channel to get metadata for"],
|
|
631
|
+
next_cursor: Annotated[
|
|
632
|
+
str | None,
|
|
633
|
+
"The cursor to use for pagination, if continuing from a previous search.",
|
|
634
|
+
] = None,
|
|
635
|
+
) -> Annotated[dict, "The channel metadata"]:
|
|
636
|
+
"""Get the metadata of a channel in Slack searching by its name.
|
|
637
|
+
|
|
638
|
+
This tool does not return the messages in a channel. To get the messages, use the
|
|
639
|
+
`get_messages_in_channel_by_name` tool."""
|
|
640
|
+
channel_names: list[str] = []
|
|
641
|
+
|
|
642
|
+
async def find_channel() -> dict:
|
|
643
|
+
nonlocal channel_names, channel_name, next_cursor
|
|
644
|
+
should_continue = True
|
|
645
|
+
|
|
646
|
+
while should_continue:
|
|
647
|
+
response = await list_conversations_metadata(
|
|
648
|
+
context=context,
|
|
649
|
+
conversation_types=[
|
|
650
|
+
ConversationType.PUBLIC_CHANNEL,
|
|
651
|
+
ConversationType.PRIVATE_CHANNEL,
|
|
652
|
+
],
|
|
653
|
+
next_cursor=next_cursor,
|
|
654
|
+
)
|
|
655
|
+
next_cursor = response.get("next_cursor")
|
|
656
|
+
|
|
657
|
+
for channel in response["conversations"]:
|
|
658
|
+
response_channel_name = (
|
|
659
|
+
"" if not isinstance(channel.get("name"), str) else channel["name"].lower()
|
|
660
|
+
)
|
|
661
|
+
if response_channel_name == channel_name.lower():
|
|
662
|
+
return channel # type: ignore[no-any-return]
|
|
663
|
+
channel_names.append(channel["name"])
|
|
664
|
+
|
|
665
|
+
if not next_cursor:
|
|
666
|
+
should_continue = False
|
|
667
|
+
|
|
668
|
+
raise ItemNotFoundError()
|
|
669
|
+
|
|
670
|
+
try:
|
|
671
|
+
return await asyncio.wait_for(find_channel(), timeout=MAX_PAGINATION_TIMEOUT_SECONDS)
|
|
672
|
+
except ItemNotFoundError:
|
|
673
|
+
raise RetryableToolError(
|
|
674
|
+
"Channel not found",
|
|
675
|
+
developer_message=f"Channel with name '{channel_name}' not found.",
|
|
676
|
+
additional_prompt_content=f"Available channel names: {channel_names}",
|
|
677
|
+
retry_after_ms=500,
|
|
678
|
+
)
|
|
679
|
+
except TimeoutError:
|
|
680
|
+
raise RetryableToolError(
|
|
681
|
+
"Channel not found, search timed out.",
|
|
682
|
+
developer_message=(
|
|
683
|
+
f"Channel with name '{channel_name}' not found. "
|
|
684
|
+
f"Search timed out after {MAX_PAGINATION_TIMEOUT_SECONDS} seconds."
|
|
685
|
+
),
|
|
686
|
+
additional_prompt_content=(
|
|
687
|
+
f"Other channel names found are: {channel_names}. "
|
|
688
|
+
"The list is potentially non-exhaustive, since the search process timed out. "
|
|
689
|
+
f"Use the '{list_conversations_metadata.__tool_name__}' tool to get"
|
|
690
|
+
"a comprehensive list of channels."
|
|
691
|
+
),
|
|
692
|
+
retry_after_ms=500,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@tool(requires_auth=Slack(scopes=["im:read"]))
|
|
697
|
+
async def get_direct_message_conversation_metadata_by_username(
|
|
698
|
+
context: ToolContext,
|
|
699
|
+
username: Annotated[str, "The username of the user/person to get messages with"],
|
|
700
|
+
next_cursor: Annotated[
|
|
701
|
+
str | None,
|
|
702
|
+
"The cursor to use for pagination, if continuing from a previous search.",
|
|
703
|
+
] = None,
|
|
704
|
+
) -> Annotated[
|
|
705
|
+
dict | None,
|
|
706
|
+
"The direct message conversation metadata.",
|
|
707
|
+
]:
|
|
708
|
+
"""Get the metadata of a direct message conversation in Slack by the username.
|
|
122
709
|
|
|
710
|
+
This tool does not return the messages in a conversation. To get the messages, use the
|
|
711
|
+
`get_messages_in_direct_message_conversation_by_username` tool."""
|
|
712
|
+
try:
|
|
713
|
+
token = (
|
|
714
|
+
context.authorization.token
|
|
715
|
+
if context.authorization and context.authorization.token
|
|
716
|
+
else ""
|
|
717
|
+
)
|
|
718
|
+
slack_client = AsyncWebClient(token=token)
|
|
719
|
+
|
|
720
|
+
current_user, list_users_response = await asyncio.gather(
|
|
721
|
+
slack_client.auth_test(), list_users(context)
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
other_user = get_user_by_username(username, list_users_response["users"])
|
|
725
|
+
|
|
726
|
+
conversations_found = await retrieve_conversations_by_user_ids(
|
|
727
|
+
list_conversations_func=list_conversations_metadata,
|
|
728
|
+
get_members_in_conversation_func=get_members_in_conversation_by_id,
|
|
729
|
+
context=context,
|
|
730
|
+
conversation_types=[ConversationType.DIRECT_MESSAGE],
|
|
731
|
+
user_ids=[current_user["user_id"], other_user["id"]],
|
|
732
|
+
exact_match=True,
|
|
733
|
+
limit=1,
|
|
734
|
+
next_cursor=next_cursor,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
return None if not conversations_found else conversations_found[0]
|
|
738
|
+
|
|
739
|
+
except UsernameNotFoundError as e:
|
|
740
|
+
raise RetryableToolError(
|
|
741
|
+
f"Username '{e.username_not_found}' not found",
|
|
742
|
+
developer_message=f"User with username '{e.username_not_found}' not found.",
|
|
743
|
+
additional_prompt_content=f"Available users: {e.usernames_found}",
|
|
744
|
+
retry_after_ms=500,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
@tool(requires_auth=Slack(scopes=["im:read"]))
|
|
749
|
+
async def get_multi_person_dm_conversation_metadata_by_usernames(
|
|
750
|
+
context: ToolContext,
|
|
751
|
+
usernames: Annotated[list[str], "The usernames of the users/people to get messages with"],
|
|
752
|
+
next_cursor: Annotated[
|
|
753
|
+
str | None,
|
|
754
|
+
"The cursor to use for pagination, if continuing from a previous search.",
|
|
755
|
+
] = None,
|
|
756
|
+
) -> Annotated[
|
|
757
|
+
dict | None,
|
|
758
|
+
"The multi-person direct message conversation metadata.",
|
|
759
|
+
]:
|
|
760
|
+
"""Get the metadata of a multi-person direct message conversation in Slack by the usernames.
|
|
761
|
+
|
|
762
|
+
This tool does not return the messages in a conversation. To get the messages, use the
|
|
763
|
+
`get_messages_in_multi_person_dm_conversation_by_usernames` tool.
|
|
764
|
+
"""
|
|
765
|
+
try:
|
|
766
|
+
token = (
|
|
767
|
+
context.authorization.token
|
|
768
|
+
if context.authorization and context.authorization.token
|
|
769
|
+
else ""
|
|
770
|
+
)
|
|
771
|
+
slack_client = AsyncWebClient(token=token)
|
|
772
|
+
|
|
773
|
+
current_user, list_users_response = await asyncio.gather(
|
|
774
|
+
slack_client.auth_test(), list_users(context)
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
other_users = [
|
|
778
|
+
get_user_by_username(username, list_users_response["users"]) for username in usernames
|
|
779
|
+
]
|
|
780
|
+
|
|
781
|
+
conversations_found = await retrieve_conversations_by_user_ids(
|
|
782
|
+
list_conversations_func=list_conversations_metadata,
|
|
783
|
+
get_members_in_conversation_func=get_members_in_conversation_by_id,
|
|
784
|
+
context=context,
|
|
785
|
+
conversation_types=[ConversationType.MULTI_PERSON_DIRECT_MESSAGE],
|
|
786
|
+
user_ids=[
|
|
787
|
+
current_user["user_id"],
|
|
788
|
+
*[user["id"] for user in other_users if user["id"] != current_user["user_id"]],
|
|
789
|
+
],
|
|
790
|
+
exact_match=True,
|
|
791
|
+
limit=1,
|
|
792
|
+
next_cursor=next_cursor,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
if not conversations_found:
|
|
796
|
+
raise RetryableToolError(
|
|
797
|
+
"Conversation not found with the usernames provided",
|
|
798
|
+
developer_message="Conversation not found with the usernames provided",
|
|
799
|
+
retry_after_ms=500,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
return conversations_found[0]
|
|
803
|
+
|
|
804
|
+
except UsernameNotFoundError as e:
|
|
805
|
+
raise RetryableToolError(
|
|
806
|
+
f"Username '{e.username_not_found}' not found",
|
|
807
|
+
developer_message=f"User with username '{e.username_not_found}' not found.",
|
|
808
|
+
additional_prompt_content=f"Available users: {e.usernames_found}",
|
|
809
|
+
retry_after_ms=500,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
@tool(
|
|
814
|
+
requires_auth=Slack(
|
|
815
|
+
scopes=["channels:read", "groups:read", "im:read", "mpim:read"],
|
|
816
|
+
)
|
|
817
|
+
)
|
|
818
|
+
async def list_conversations_metadata(
|
|
819
|
+
context: ToolContext,
|
|
820
|
+
conversation_types: Annotated[
|
|
821
|
+
list[ConversationType] | None,
|
|
822
|
+
"The type(s) of conversations to list. Defaults to all types.",
|
|
823
|
+
] = None,
|
|
824
|
+
limit: Annotated[int | None, "The maximum number of conversations to list."] = None,
|
|
825
|
+
next_cursor: Annotated[str | None, "The cursor to use for pagination."] = None,
|
|
826
|
+
) -> Annotated[
|
|
827
|
+
dict,
|
|
828
|
+
(
|
|
829
|
+
"The conversations metadata list and a pagination 'next_cursor', if there are more "
|
|
830
|
+
"conversations to retrieve."
|
|
831
|
+
),
|
|
832
|
+
]:
|
|
833
|
+
"""
|
|
834
|
+
List metadata for Slack conversations (channels and/or direct messages) that the user
|
|
835
|
+
is a member of.
|
|
836
|
+
"""
|
|
837
|
+
if isinstance(conversation_types, ConversationType):
|
|
838
|
+
conversation_types = [conversation_types]
|
|
839
|
+
|
|
840
|
+
conversation_types_filter = ",".join(
|
|
841
|
+
convert_conversation_type_to_slack_name(conv_type).value
|
|
842
|
+
for conv_type in conversation_types or ConversationType
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
token = (
|
|
846
|
+
context.authorization.token if context.authorization and context.authorization.token else ""
|
|
847
|
+
)
|
|
848
|
+
slackClient = AsyncWebClient(token=token)
|
|
849
|
+
|
|
850
|
+
results, next_cursor = await async_paginate(
|
|
851
|
+
slackClient.conversations_list,
|
|
852
|
+
"channels",
|
|
853
|
+
limit=limit,
|
|
854
|
+
next_cursor=next_cursor,
|
|
855
|
+
types=conversation_types_filter,
|
|
856
|
+
exclude_archived=True,
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
"conversations": [
|
|
861
|
+
dict(**extract_conversation_metadata(conversation))
|
|
862
|
+
for conversation in results
|
|
863
|
+
if conversation.get("is_im") or conversation.get("is_member")
|
|
864
|
+
],
|
|
865
|
+
"next_cursor": next_cursor,
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
@tool(
|
|
870
|
+
requires_auth=Slack(
|
|
871
|
+
scopes=["channels:read"],
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
async def list_public_channels_metadata(
|
|
875
|
+
context: ToolContext,
|
|
876
|
+
limit: Annotated[int | None, "The maximum number of channels to list."] = None,
|
|
877
|
+
) -> Annotated[dict, "The public channels"]:
|
|
878
|
+
"""List metadata for public channels in Slack that the user is a member of."""
|
|
879
|
+
|
|
880
|
+
return await list_conversations_metadata( # type: ignore[no-any-return]
|
|
881
|
+
context,
|
|
882
|
+
conversation_types=[ConversationType.PUBLIC_CHANNEL],
|
|
883
|
+
limit=limit,
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
@tool(
|
|
888
|
+
requires_auth=Slack(
|
|
889
|
+
scopes=["groups:read"],
|
|
890
|
+
)
|
|
891
|
+
)
|
|
892
|
+
async def list_private_channels_metadata(
|
|
893
|
+
context: ToolContext,
|
|
894
|
+
limit: Annotated[int | None, "The maximum number of channels to list."] = None,
|
|
895
|
+
) -> Annotated[dict, "The private channels"]:
|
|
896
|
+
"""List metadata for private channels in Slack that the user is a member of."""
|
|
897
|
+
|
|
898
|
+
return await list_conversations_metadata( # type: ignore[no-any-return]
|
|
899
|
+
context,
|
|
900
|
+
conversation_types=[ConversationType.PRIVATE_CHANNEL],
|
|
901
|
+
limit=limit,
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
@tool(
|
|
906
|
+
requires_auth=Slack(
|
|
907
|
+
scopes=["mpim:read"],
|
|
908
|
+
)
|
|
909
|
+
)
|
|
910
|
+
async def list_group_direct_message_conversations_metadata(
|
|
911
|
+
context: ToolContext,
|
|
912
|
+
limit: Annotated[int | None, "The maximum number of conversations to list."] = None,
|
|
913
|
+
) -> Annotated[dict, "The group direct message conversations metadata"]:
|
|
914
|
+
"""List metadata for group direct message conversations that the user is a member of."""
|
|
915
|
+
|
|
916
|
+
return await list_conversations_metadata( # type: ignore[no-any-return]
|
|
917
|
+
context,
|
|
918
|
+
conversation_types=[ConversationType.MULTI_PERSON_DIRECT_MESSAGE],
|
|
919
|
+
limit=limit,
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
# Note: Bots are included in the results.
|
|
924
|
+
# Note: Direct messages with no conversation history are included in the results.
|
|
925
|
+
@tool(
|
|
926
|
+
requires_auth=Slack(
|
|
927
|
+
scopes=["im:read"],
|
|
928
|
+
)
|
|
929
|
+
)
|
|
930
|
+
async def list_direct_message_conversations_metadata(
|
|
931
|
+
context: ToolContext,
|
|
932
|
+
limit: Annotated[int | None, "The maximum number of conversations to list."] = None,
|
|
933
|
+
) -> Annotated[dict, "The direct message conversations metadata"]:
|
|
934
|
+
"""List metadata for direct message conversations in Slack that the user is a member of."""
|
|
935
|
+
|
|
936
|
+
response = await list_conversations_metadata(
|
|
937
|
+
context,
|
|
938
|
+
conversation_types=[ConversationType.DIRECT_MESSAGE],
|
|
939
|
+
limit=limit,
|
|
940
|
+
)
|
|
123
941
|
|
|
124
|
-
|
|
125
|
-
csv_string = "All active Slack channels:\n\nname\n"
|
|
126
|
-
for channel in channels_response["channels"]:
|
|
127
|
-
if not channel.get("is_archived", False):
|
|
128
|
-
name = channel.get("name", "")
|
|
129
|
-
csv_string += f"{name}\n"
|
|
130
|
-
return csv_string.strip()
|
|
942
|
+
return response # type: ignore[no-any-return]
|