arcade-slack 0.1.6__py3-none-any.whl → 0.5.0__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/utils.py ADDED
@@ -0,0 +1,606 @@
1
+ import asyncio
2
+ import json
3
+ import re
4
+ from collections.abc import Callable, Sequence
5
+ from datetime import datetime, timezone
6
+ from typing import Any, cast
7
+
8
+ from arcade_tdk import ToolContext
9
+ from arcade_tdk.errors import RetryableToolError
10
+
11
+ from arcade_slack.constants import (
12
+ MAX_CONCURRENT_REQUESTS,
13
+ MAX_PAGINATION_SIZE_LIMIT,
14
+ MAX_PAGINATION_TIMEOUT_SECONDS,
15
+ )
16
+ from arcade_slack.custom_types import SlackPaginationNextCursor
17
+ from arcade_slack.exceptions import PaginationTimeoutError
18
+ from arcade_slack.models import (
19
+ AbstractConcurrencySafeCoroutineCaller,
20
+ BasicUserInfo,
21
+ ConversationMetadata,
22
+ ConversationType,
23
+ ConversationTypeSlackName,
24
+ Message,
25
+ PaginationSentinel,
26
+ SlackConversation,
27
+ SlackConversationPurpose,
28
+ SlackMessage,
29
+ SlackUser,
30
+ SlackUserList,
31
+ )
32
+
33
+
34
+ def format_users(user_list_response: SlackUserList) -> str:
35
+ """Format a list of Slack users into a CSV string.
36
+
37
+ Args:
38
+ userListResponse: The response from the Slack API's users_list method.
39
+
40
+ Returns:
41
+ A CSV string with two columns: the users' name and real name, each user in a new line.
42
+ The first line is the header with column names 'name' and 'real_name'.
43
+ """
44
+ csv_string = "name,real_name\n"
45
+ for user in user_list_response["members"]:
46
+ if not user.get("deleted", False):
47
+ name = user.get("name", "")
48
+ profile = user.get("profile", {})
49
+ real_name = "" if not profile else profile.get("real_name", "")
50
+ csv_string += f"{name},{real_name}\n"
51
+ return csv_string.strip()
52
+
53
+
54
+ def format_conversations_as_csv(conversations: dict) -> str:
55
+ """Format a list of Slack conversations into a CSV string.
56
+
57
+ Args:
58
+ conversations: The response from the Slack API's conversations_list method.
59
+
60
+ Returns:
61
+ A CSV string with the conversations' names.
62
+ """
63
+ csv_string = "All active Slack conversations:\n\nname\n"
64
+ for conversation in conversations["channels"]:
65
+ if not conversation.get("is_archived", False):
66
+ name = conversation.get("name", "")
67
+ csv_string += f"{name}\n"
68
+ return csv_string.strip()
69
+
70
+
71
+ def remove_none_values(params: dict) -> dict:
72
+ """Remove key/value pairs from a dictionary where the value is None.
73
+
74
+ Args:
75
+ params: The dictionary to remove None values from.
76
+
77
+ Returns:
78
+ A dictionary with None values removed.
79
+ """
80
+ return {k: v for k, v in params.items() if v is not None}
81
+
82
+
83
+ def get_slack_conversation_type_as_str(channel: SlackConversation) -> str | None:
84
+ """Get the type of conversation from a Slack channel's dictionary.
85
+
86
+ Args:
87
+ channel: The Slack channel's dictionary.
88
+
89
+ Returns:
90
+ The type of conversation string in Slack naming standard.
91
+ """
92
+ if channel.get("is_channel"):
93
+ return ConversationTypeSlackName.PUBLIC_CHANNEL.value
94
+ if channel.get("is_group"):
95
+ return ConversationTypeSlackName.PRIVATE_CHANNEL.value
96
+ if channel.get("is_im"):
97
+ return ConversationTypeSlackName.IM.value
98
+ if channel.get("is_mpim"):
99
+ return ConversationTypeSlackName.MPIM.value
100
+ raise ValueError(f"Invalid conversation type in channel: {json.dumps(channel)}")
101
+
102
+
103
+ def convert_conversation_type_to_slack_name(
104
+ conversation_type: ConversationType,
105
+ ) -> ConversationTypeSlackName:
106
+ """Convert a conversation type to another using Slack naming standard.
107
+
108
+ Args:
109
+ conversation_type: The conversation type enum value.
110
+
111
+ Returns:
112
+ The corresponding conversation type enum value using Slack naming standard.
113
+ """
114
+ mapping = {
115
+ ConversationType.PUBLIC_CHANNEL: ConversationTypeSlackName.PUBLIC_CHANNEL,
116
+ ConversationType.PRIVATE_CHANNEL: ConversationTypeSlackName.PRIVATE_CHANNEL,
117
+ ConversationType.MULTI_PERSON_DIRECT_MESSAGE: ConversationTypeSlackName.MPIM,
118
+ ConversationType.DIRECT_MESSAGE: ConversationTypeSlackName.IM,
119
+ }
120
+ return mapping[conversation_type]
121
+
122
+
123
+ def extract_conversation_metadata(conversation: SlackConversation) -> ConversationMetadata:
124
+ """Extract conversation metadata from a Slack conversation object.
125
+
126
+ Args:
127
+ conversation: The Slack conversation dictionary.
128
+
129
+ Returns:
130
+ A dictionary with the conversation metadata.
131
+ """
132
+ conversation_type = get_slack_conversation_type_as_str(conversation)
133
+
134
+ purpose: SlackConversationPurpose | None = conversation.get("purpose")
135
+ purpose_value = "" if not purpose else purpose.get("value", "")
136
+
137
+ metadata = ConversationMetadata(
138
+ id=conversation.get("id"),
139
+ name=conversation.get("name"),
140
+ conversation_type=conversation_type,
141
+ is_private=conversation.get("is_private", True),
142
+ is_archived=conversation.get("is_archived", False),
143
+ is_member=conversation.get("is_member", True),
144
+ purpose=purpose_value,
145
+ num_members=conversation.get("num_members", 0),
146
+ )
147
+
148
+ if conversation_type == ConversationTypeSlackName.IM.value:
149
+ metadata["num_members"] = 2
150
+ metadata["user"] = conversation.get("user")
151
+ metadata["is_user_deleted"] = conversation.get("is_user_deleted")
152
+ elif conversation_type == ConversationTypeSlackName.MPIM.value:
153
+ conversation_name = conversation.get("name", "")
154
+ if conversation_name:
155
+ metadata["num_members"] = len(conversation_name.split("--"))
156
+ else:
157
+ metadata["num_members"] = None
158
+
159
+ return metadata
160
+
161
+
162
+ def extract_basic_user_info(user_info: SlackUser) -> BasicUserInfo:
163
+ """Extract a user's basic info from a Slack user dictionary.
164
+
165
+ Args:
166
+ user_info: The Slack user dictionary.
167
+
168
+ Returns:
169
+ A dictionary with the user's basic info.
170
+
171
+ See https://api.slack.com/types/user for the structure of the user object.
172
+ """
173
+ profile = user_info.get("profile", {})
174
+ display_name = None if not profile else profile.get("display_name")
175
+ email = None if not profile else profile.get("email")
176
+ return BasicUserInfo(
177
+ id=user_info.get("id"),
178
+ name=user_info.get("name"),
179
+ is_bot=user_info.get("is_bot"),
180
+ email=email,
181
+ display_name=display_name,
182
+ real_name=user_info.get("real_name"),
183
+ timezone=user_info.get("tz"),
184
+ )
185
+
186
+
187
+ def filter_conversations_by_user_ids(
188
+ conversations: list[dict],
189
+ user_ids: list[str],
190
+ exact_match: bool = False,
191
+ ) -> list[dict]:
192
+ """
193
+ Filter conversations by the members' user IDs.
194
+
195
+ Args:
196
+ conversations: The list of conversations to filter.
197
+ user_ids: The user IDs to filter conversations for.
198
+ exact_match: Whether to match the exact number of members in the conversations.
199
+
200
+ Returns:
201
+ The list of conversations found.
202
+ """
203
+ matches = []
204
+ for conversation in conversations:
205
+ member_ids = [member["id"] for member in conversation["members"]]
206
+ if exact_match:
207
+ same_length = len(user_ids) == len(member_ids)
208
+ has_all_members = all(user_id in member_ids for user_id in user_ids)
209
+ if same_length and has_all_members:
210
+ matches.append(conversation)
211
+ else:
212
+ if all(user_id in member_ids for user_id in user_ids):
213
+ matches.append(conversation)
214
+
215
+ return matches
216
+
217
+
218
+ def is_user_a_bot(user: SlackUser) -> bool:
219
+ """Check if a Slack user represents a bot.
220
+
221
+ Args:
222
+ user: The Slack user dictionary.
223
+
224
+ Returns:
225
+ True if the user is a bot, False otherwise.
226
+
227
+ Bots are users with the "is_bot" flag set to true.
228
+ USLACKBOT is the user object for the Slack bot itself and is a special case.
229
+
230
+ See https://api.slack.com/types/user for the structure of the user object.
231
+ """
232
+ return user.get("is_bot") or user.get("id") == "USLACKBOT"
233
+
234
+
235
+ def is_user_deleted(user: SlackUser) -> bool:
236
+ """Check if a Slack user represents a deleted user.
237
+
238
+ Args:
239
+ user: The Slack user dictionary.
240
+
241
+ Returns:
242
+ True if the user is deleted, False otherwise.
243
+
244
+ See https://api.slack.com/types/user for the structure of the user object.
245
+ """
246
+ is_deleted = user.get("deleted")
247
+
248
+ return is_deleted if isinstance(is_deleted, bool) else False
249
+
250
+
251
+ async def async_paginate(
252
+ func: Callable,
253
+ response_key: str | None = None,
254
+ limit: int | None = None,
255
+ next_cursor: SlackPaginationNextCursor | None = None,
256
+ max_pagination_timeout_seconds: int = MAX_PAGINATION_TIMEOUT_SECONDS,
257
+ sentinel: PaginationSentinel | None = None,
258
+ *args: Any,
259
+ **kwargs: Any,
260
+ ) -> tuple[list, SlackPaginationNextCursor | None]:
261
+ """Paginate a Slack AsyncWebClient's method results.
262
+
263
+ The purpose is to abstract the pagination work and make it easier for the LLM to retrieve the
264
+ amount of items requested by the user, regardless of limits imposed by the Slack API. We still
265
+ return the next cursor, if needed to paginate further.
266
+
267
+ Args:
268
+ func: The Slack AsyncWebClient's method to paginate.
269
+ response_key: The key in the response dictionary to extract the items from (optional). If
270
+ not provided, the entire response dictionary is used.
271
+ limit: The maximum number of items to retrieve (defaults to Slack's suggested limit).
272
+ next_cursor: The cursor to use for pagination (optional).
273
+ max_pagination_timeout_seconds: The maximum timeout for the pagination loop (defaults to
274
+ MAX_PAGINATION_TIMEOUT_SECONDS).
275
+ sentinel: Control whether the pagination should continue after each iteration (optional).
276
+ If provided, the pagination will stop when the sentinel function returns True.
277
+ *args: Positional arguments to pass to the Slack method.
278
+ **kwargs: Keyword arguments to pass to the Slack method.
279
+
280
+ Returns:
281
+ A tuple containing the list of items and the next cursor, if needed to paginate further.
282
+ """
283
+ results: list[Any] = []
284
+
285
+ async def paginate_loop() -> list[Any]:
286
+ nonlocal results, next_cursor
287
+ should_continue = True
288
+
289
+ """
290
+ The slack_limit variable makes the Slack API return no more than the appropriate
291
+ amount of items. The loop extends results with the items returned and continues
292
+ iterating if it hasn't reached the limit, and Slack indicates there're more
293
+ items to retrieve.
294
+ """
295
+
296
+ while should_continue:
297
+ iteration_limit = limit - len(results) if limit else MAX_PAGINATION_SIZE_LIMIT
298
+ slack_limit = min(iteration_limit, MAX_PAGINATION_SIZE_LIMIT)
299
+ iteration_kwargs = {**kwargs, "limit": slack_limit, "cursor": next_cursor}
300
+ response = await func(*args, **iteration_kwargs)
301
+
302
+ try:
303
+ result = dict(response.data) if not response_key else response[response_key]
304
+ results.extend(result)
305
+ except KeyError:
306
+ raise ValueError(f"Response key {response_key} not found in Slack response")
307
+
308
+ next_cursor = response.get("response_metadata", {}).get("next_cursor")
309
+
310
+ if (
311
+ (sentinel and sentinel(last_result=result))
312
+ or (limit and len(results) >= limit)
313
+ or not next_cursor
314
+ ):
315
+ should_continue = False
316
+
317
+ return results
318
+
319
+ try:
320
+ results = await asyncio.wait_for(paginate_loop(), timeout=max_pagination_timeout_seconds)
321
+ # asyncio.TimeoutError for Python <= 3.10, TimeoutError for Python >= 3.11
322
+ except (TimeoutError, asyncio.TimeoutError):
323
+ raise PaginationTimeoutError(max_pagination_timeout_seconds)
324
+ else:
325
+ return results, next_cursor
326
+
327
+
328
+ def enrich_message_datetime(message: SlackMessage) -> Message:
329
+ """Enrich message metadata with formatted datetime.
330
+
331
+ It helps LLMs when they need to display the date/time in human-readable format. Slack
332
+ will only return a unix-formatted timestamp (it's not actually UTC Unix timestamp, but
333
+ the Unix timestamp in the user's timezone - I know, odd, but it is what it is).
334
+
335
+ Args:
336
+ message: The Slack message dictionary.
337
+
338
+ Returns:
339
+ The enriched message dictionary.
340
+ """
341
+ message = Message(**message)
342
+ ts = message.get("ts")
343
+ if isinstance(ts, str):
344
+ try:
345
+ timestamp = float(ts)
346
+ message["datetime_timestamp"] = datetime.fromtimestamp(timestamp).strftime(
347
+ "%Y-%m-%d %H:%M:%S"
348
+ )
349
+ except ValueError:
350
+ pass
351
+ return message
352
+
353
+
354
+ def convert_datetime_to_unix_timestamp(datetime_str: str) -> int:
355
+ """Convert a datetime string to a unix timestamp.
356
+
357
+ Args:
358
+ datetime_str: The datetime string ('YYYY-MM-DD HH:MM:SS') to convert to a unix timestamp.
359
+
360
+ Returns:
361
+ The unix timestamp integer.
362
+ """
363
+ try:
364
+ dt = datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")
365
+ return int(dt.timestamp())
366
+ except ValueError:
367
+ raise RetryableToolError(
368
+ "Invalid datetime format",
369
+ developer_message=f"The datetime '{datetime_str}' is invalid. "
370
+ "Please provide a datetime string in the format 'YYYY-MM-DD HH:MM:SS'.",
371
+ retry_after_ms=500,
372
+ )
373
+
374
+
375
+ def convert_relative_datetime_to_unix_timestamp(
376
+ relative_datetime: str,
377
+ current_unix_timestamp: int | None = None,
378
+ ) -> int:
379
+ """Convert a relative datetime string in the format 'DD:HH:MM' to unix timestamp.
380
+
381
+ Args:
382
+ relative_datetime: The relative datetime string ('DD:HH:MM') to convert to a unix timestamp.
383
+ current_unix_timestamp: The current unix timestamp (optional). If not provided, the
384
+ current unix timestamp from datetime.now is used.
385
+
386
+ Returns:
387
+ The unix timestamp integer.
388
+ """
389
+ if not current_unix_timestamp:
390
+ current_unix_timestamp = int(datetime.now(timezone.utc).timestamp())
391
+
392
+ days, hours, minutes = map(int, relative_datetime.split(":"))
393
+ seconds = days * 86400 + hours * 3600 + minutes * 60
394
+ return int(current_unix_timestamp - seconds)
395
+
396
+
397
+ def short_user_info(user: dict) -> dict[str, str | None]:
398
+ data = {"id": user.get("id")}
399
+ if user.get("name"):
400
+ data["name"] = user["name"]
401
+ if isinstance(user.get("profile"), dict) and user["profile"].get("email"):
402
+ data["email"] = user["profile"]["email"]
403
+ elif user.get("email"):
404
+ data["email"] = user["email"]
405
+ return data
406
+
407
+
408
+ def short_human_users_info(users: list[dict]) -> list[dict[str, str | None]]:
409
+ return [short_user_info(user) for user in users if not user.get("is_bot")]
410
+
411
+
412
+ def is_valid_email(email: str) -> bool:
413
+ """Validate an email address using regex.
414
+
415
+ Args:
416
+ email: The email address to validate.
417
+
418
+ Returns:
419
+ True if the email is valid, False otherwise.
420
+ """
421
+ email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
422
+ return bool(re.match(email_pattern, email))
423
+
424
+
425
+ async def build_multiple_users_retrieval_response(
426
+ context: ToolContext,
427
+ users_responses: list[dict[str, Any]],
428
+ ) -> list[dict[str, Any]]:
429
+ """Builds response list for the get_multiple_users_by_usernames_or_emails function."""
430
+ await raise_for_users_not_found(context, users_responses)
431
+
432
+ users = []
433
+
434
+ for users_response in users_responses:
435
+ users.extend(users_response["users"])
436
+
437
+ return cast(list[dict[str, Any]], users)
438
+
439
+
440
+ async def raise_for_users_not_found(
441
+ context: ToolContext, users_responses: list[dict[str, Any]]
442
+ ) -> None:
443
+ """Raise an error if any user was not found in the responses."""
444
+ users_not_found, available_users = collect_users_not_found_in_responses(users_responses)
445
+
446
+ if users_not_found:
447
+ not_found_message = ", ".join(users_not_found)
448
+ s = "" if len(users_not_found) == 1 else "s"
449
+ message = f"User{s} not found: {not_found_message}"
450
+ available_users_prompt = await get_available_users_prompt(context, available_users)
451
+
452
+ raise RetryableToolError(
453
+ message=message,
454
+ developer_message=message,
455
+ additional_prompt_content=available_users_prompt,
456
+ retry_after_ms=500,
457
+ )
458
+
459
+
460
+ def collect_users_not_found_in_responses(
461
+ responses: list[dict[str, Any]],
462
+ ) -> tuple[list[str], list[dict[str, Any]]]:
463
+ users_not_found = []
464
+ available_users = []
465
+
466
+ for response in responses:
467
+ if response.get("not_found"):
468
+ users_not_found.extend(response["not_found"])
469
+ if response.get("available_users"):
470
+ available_users = response["available_users"]
471
+
472
+ return users_not_found, available_users
473
+
474
+
475
+ async def get_available_users_prompt(
476
+ context: ToolContext,
477
+ available_users: list[dict] | None = None,
478
+ limit: int = 100,
479
+ ) -> str:
480
+ try:
481
+ from arcade_slack.tools.users import list_users # Avoid circular import
482
+
483
+ if isinstance(available_users, list) and available_users:
484
+ available_users = [
485
+ user for user in available_users if not is_user_a_bot(SlackUser(**user))
486
+ ]
487
+ available_users_str = json.dumps(short_human_users_info(available_users))
488
+ next_cursor = None
489
+ potentially_more_users = True
490
+ else:
491
+ users = await list_users(context, limit=limit, exclude_bots=True)
492
+ next_cursor = users["next_cursor"]
493
+ available_users_str = json.dumps(short_human_users_info(users["users"]))
494
+ potentially_more_users = bool(next_cursor)
495
+
496
+ if not potentially_more_users:
497
+ return f"The users available are: {available_users_str}"
498
+ else:
499
+ msg = (
500
+ f"Some of the available users are: {available_users_str}. Potentially more users "
501
+ f"can be retrieved by calling the 'Slack.{list_users.__tool_name__}' tool"
502
+ )
503
+ if next_cursor:
504
+ msg += f" using the next cursor: '{next_cursor}' to continue pagination."
505
+ return msg
506
+ except Exception as e:
507
+ return (
508
+ "The tool tried to retrieve a list of available users, but failed with error: "
509
+ f"{type(e).__name__}: {e!s}. Use the 'Slack.{list_users.__tool_name__}' tool "
510
+ "to get a list of users."
511
+ )
512
+
513
+
514
+ async def gather_with_concurrency_limit(
515
+ coroutine_callers: Sequence[AbstractConcurrencySafeCoroutineCaller],
516
+ semaphore: asyncio.Semaphore | None = None,
517
+ max_concurrent_requests: int = MAX_CONCURRENT_REQUESTS,
518
+ ) -> list[Any]:
519
+ if not semaphore:
520
+ semaphore = asyncio.Semaphore(max_concurrent_requests)
521
+
522
+ return await asyncio.gather(*[caller(semaphore) for caller in coroutine_callers]) # type: ignore[no-any-return]
523
+
524
+
525
+ def cast_user_dict(user: dict[str, Any]) -> dict[str, Any]:
526
+ slack_user = SlackUser(**cast(dict, user))
527
+ return dict(**extract_basic_user_info(slack_user))
528
+
529
+
530
+ async def populate_users_in_messages(auth_token: str, messages: list[dict]) -> list[dict]:
531
+ if not messages:
532
+ return messages
533
+
534
+ users = await get_users_from_messages(auth_token, messages)
535
+ users_by_id = {user["id"]: {"id": user["id"], "name": user["name"]} for user in users}
536
+
537
+ for message in messages:
538
+ if message.get("type") != "message":
539
+ continue
540
+
541
+ # Message author
542
+ message["user"] = users_by_id.get(
543
+ message.get("user"), {"id": message["user"], "name": None}
544
+ )
545
+
546
+ # User mentions in the message text
547
+ text_mentions = re.findall(r"<@([A-Z0-9]+)>", message.get("text", ""))
548
+ for user_id in text_mentions:
549
+ if user_id in users_by_id:
550
+ user = users_by_id.get(user_id, {"id": user_id, "name": None})
551
+ name = user.get("name")
552
+ message["text"] = message["text"].replace(
553
+ f"<@{user_id}>", f"<@{name} (id:{user_id})>" if name else f"<@{user_id}>"
554
+ )
555
+
556
+ # User mentions in reactions
557
+ reactions = message.get("reactions")
558
+ if isinstance(reactions, list):
559
+ for reaction in reactions:
560
+ reaction_users = []
561
+ for user_id in reaction.get("users", []):
562
+ reaction_users.append(users_by_id.get(user_id, {"id": user_id, "name": None}))
563
+ reaction["users"] = reaction_users
564
+
565
+ return messages
566
+
567
+
568
+ async def get_users_from_messages(auth_token: str, messages: list[dict]) -> list[dict[str, Any]]:
569
+ if not messages:
570
+ return []
571
+
572
+ from arcade_slack.user_retrieval import get_users_by_id # Avoid circular import
573
+
574
+ user_ids = get_user_ids_from_messages(messages)
575
+ response = await get_users_by_id(auth_token, user_ids)
576
+ print("\n\n\nresponse:", response, "\n\n\n")
577
+ return response["users"]
578
+
579
+
580
+ def get_user_ids_from_messages(messages: list[dict]) -> list[str]:
581
+ if not messages:
582
+ return []
583
+
584
+ user_ids = []
585
+
586
+ for message in messages:
587
+ if message.get("type") != "message":
588
+ continue
589
+
590
+ # Message author
591
+ user = message.get("user")
592
+ if isinstance(user, str) and user:
593
+ user_ids.append(user)
594
+
595
+ # User mentions in the message text
596
+ text = message.get("text")
597
+ if isinstance(text, str) and text:
598
+ user_ids.extend(re.findall(r"<@([A-Z0-9]+)>", text))
599
+
600
+ # User mentions in reactions
601
+ reactions = message.get("reactions")
602
+ if isinstance(reactions, list):
603
+ for reaction in reactions:
604
+ user_ids.extend(reaction.get("users", []))
605
+
606
+ return user_ids
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcade_slack
3
+ Version: 0.5.0
4
+ Summary: Arcade.dev LLM tools for Slack
5
+ Author-email: Arcade <dev@arcade.dev>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: aiodns<2.0.0,>=1.0
9
+ Requires-Dist: aiohttp<4.0.0,>=3.7.3
10
+ Requires-Dist: arcade-tdk<3.0.0,>=2.0.0
11
+ Requires-Dist: slack-sdk<4.0.0,>=3.31.0
12
+ Requires-Dist: typing; python_version < '3.7'
13
+ Provides-Extra: dev
14
+ Requires-Dist: arcade-ai[evals]<3.0.0,>=2.0.0; extra == 'dev'
15
+ Requires-Dist: arcade-serve<3.0.0,>=2.0.0; extra == 'dev'
16
+ Requires-Dist: mypy<1.6.0,>=1.5.1; extra == 'dev'
17
+ Requires-Dist: pre-commit<3.5.0,>=3.4.0; extra == 'dev'
18
+ Requires-Dist: pytest-asyncio<0.25.0,>=0.24.0; extra == 'dev'
19
+ Requires-Dist: pytest-cov<4.1.0,>=4.0.0; extra == 'dev'
20
+ Requires-Dist: pytest-mock<3.12.0,>=3.11.1; extra == 'dev'
21
+ Requires-Dist: pytest<8.4.0,>=8.3.0; extra == 'dev'
22
+ Requires-Dist: ruff<0.8.0,>=0.7.4; extra == 'dev'
23
+ Requires-Dist: tox<4.12.0,>=4.11.1; extra == 'dev'
@@ -0,0 +1,17 @@
1
+ arcade_slack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ arcade_slack/constants.py,sha256=ANAZOmNSqrbZHDqEvQfau7_91if9EeKV-G5D0VBB9vo,524
3
+ arcade_slack/conversation_retrieval.py,sha256=jsd64-IDpgTpuq-4Hbg1317VatxKnIy8edWhHB6bN7E,2338
4
+ arcade_slack/critics.py,sha256=Sx0nOmvym4LFNX1gZ34ndHkAKAB--X1zRPePcbiVzDo,1209
5
+ arcade_slack/custom_types.py,sha256=-23JnfXjMQ-H8CyRL3CEv9opPMySox1KINH5rrPIOt0,928
6
+ arcade_slack/exceptions.py,sha256=YQ4CTa1LbR-G7sjnQM8LB7LruxZDqPzvo-cptFYW7E8,385
7
+ arcade_slack/message_retrieval.py,sha256=XdzWLJdKtc5DL7H_qQBei6qJ-rz2uGs-Q0HfYqx7EG4,2496
8
+ arcade_slack/models.py,sha256=-zlkAxaDNEcIkQqePYTZXAqcRX4gO5jocJIixeWpK7A,12213
9
+ arcade_slack/user_retrieval.py,sha256=cdfzBbm2fsVNCztt2XbIV2UKuCdHaotfFBdo10EpBcE,6774
10
+ arcade_slack/utils.py,sha256=nKfvpeVmLeIun0y9poPG27jLBji2WMH4h1oWgemPIQ8,21438
11
+ arcade_slack/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ arcade_slack/tools/chat.py,sha256=8hsMpFI_H-0lyz1qmin3qivgHArThaB65B1riE4Yl9A,38119
13
+ arcade_slack/tools/users.py,sha256=Kw3MWNOwuUY17X5HGf3JMAH_mfloLRdRzXSBC7Zo7ug,4233
14
+ arcade_slack-0.5.0.dist-info/METADATA,sha256=LD3Bpc8WismmhE9WaGO5ZKd3h0AEgwR_KQz7mNSA99Q,953
15
+ arcade_slack-0.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ arcade_slack-0.5.0.dist-info/licenses/LICENSE,sha256=f4Q0XUZJ2MqZBO1XsqqHhuZfSs2ar1cZEJ45150zERo,1067
17
+ arcade_slack-0.5.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025, Arcade AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,14 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: arcade_slack
3
- Version: 0.1.6
4
- Summary: Slack tools for LLMs
5
- Author: Arcade AI
6
- Author-email: dev@arcade-ai.com
7
- Requires-Python: >=3.10,<4.0
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.10
10
- Classifier: Programming Language :: Python :: 3.11
11
- Classifier: Programming Language :: Python :: 3.12
12
- Classifier: Programming Language :: Python :: 3.13
13
- Requires-Dist: arcade-ai (==0.1.6)
14
- Requires-Dist: slack-sdk (>=3.31.0,<4.0.0)