arcade-slack 2.0.0__py3-none-any.whl → 2.1.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/critics.py +1 -1
- arcade_slack/tools/chat.py +96 -0
- arcade_slack/utils.py +114 -2
- {arcade_slack-2.0.0.dist-info → arcade_slack-2.1.0.dist-info}/METADATA +1 -1
- {arcade_slack-2.0.0.dist-info → arcade_slack-2.1.0.dist-info}/RECORD +7 -7
- {arcade_slack-2.0.0.dist-info → arcade_slack-2.1.0.dist-info}/WHEEL +1 -1
- {arcade_slack-2.0.0.dist-info → arcade_slack-2.1.0.dist-info}/licenses/LICENSE +0 -0
arcade_slack/critics.py
CHANGED
|
@@ -4,7 +4,7 @@ from arcade_evals import BinaryCritic
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class RelativeTimeBinaryCritic(BinaryCritic):
|
|
7
|
-
def evaluate(self, expected: Any, actual: Any) -> dict[str,
|
|
7
|
+
def evaluate(self, expected: Any, actual: Any) -> dict[str, Any]:
|
|
8
8
|
"""
|
|
9
9
|
Evaluates whether the expected and actual relative time strings are equivalent after
|
|
10
10
|
casting.
|
arcade_slack/tools/chat.py
CHANGED
|
@@ -21,6 +21,7 @@ from arcade_slack.user_retrieval import (
|
|
|
21
21
|
from arcade_slack.utils import (
|
|
22
22
|
async_paginate,
|
|
23
23
|
extract_conversation_metadata,
|
|
24
|
+
invite_users_to_conversation,
|
|
24
25
|
populate_users_in_messages,
|
|
25
26
|
raise_for_users_not_found,
|
|
26
27
|
)
|
|
@@ -418,3 +419,98 @@ async def list_conversations(
|
|
|
418
419
|
],
|
|
419
420
|
"next_cursor": next_cursor,
|
|
420
421
|
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
@tool(
|
|
425
|
+
requires_auth=Slack(
|
|
426
|
+
scopes=[
|
|
427
|
+
"channels:read",
|
|
428
|
+
"groups:read",
|
|
429
|
+
"channels:write",
|
|
430
|
+
"groups:write",
|
|
431
|
+
"users:read",
|
|
432
|
+
"users:read.email",
|
|
433
|
+
],
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
async def invite_users_to_channel(
|
|
437
|
+
context: ToolContext,
|
|
438
|
+
channel_id: Annotated[
|
|
439
|
+
str | None,
|
|
440
|
+
"The ID of the Slack channel or MPIM (multi-person direct message) to invite users to. "
|
|
441
|
+
"Provide exactly one of channel_id OR channel_name.",
|
|
442
|
+
] = None,
|
|
443
|
+
channel_name: Annotated[
|
|
444
|
+
str | None,
|
|
445
|
+
"The name of the channel to invite users to. Prefer providing a channel_id when "
|
|
446
|
+
"available for better performance. Note: MPIMs don't have names, so use channel_id "
|
|
447
|
+
"for MPIMs.",
|
|
448
|
+
] = None,
|
|
449
|
+
user_ids: Annotated[
|
|
450
|
+
list[str] | None,
|
|
451
|
+
"The Slack user IDs of the people to invite. Up to 100 users may be listed. "
|
|
452
|
+
"Provide at least one of user_ids, usernames, or emails.",
|
|
453
|
+
] = None,
|
|
454
|
+
usernames: Annotated[
|
|
455
|
+
list[str] | None,
|
|
456
|
+
"The Slack usernames of the people to invite. Prefer providing user_ids "
|
|
457
|
+
"and/or emails when available for better performance.",
|
|
458
|
+
] = None,
|
|
459
|
+
emails: Annotated[
|
|
460
|
+
list[str] | None,
|
|
461
|
+
"The email addresses of the people to invite.",
|
|
462
|
+
] = None,
|
|
463
|
+
) -> Annotated[dict, "The response from inviting users to the conversation"]:
|
|
464
|
+
"""Invite users to a Slack channel or MPIM (multi-person direct message).
|
|
465
|
+
|
|
466
|
+
This tool invites specified users to join a Slack conversation. It works with:
|
|
467
|
+
- Public channels
|
|
468
|
+
- Private channels
|
|
469
|
+
- MPIMs (multi-person direct messages / group DMs)
|
|
470
|
+
|
|
471
|
+
You can specify users by their user IDs, usernames, or email addresses.
|
|
472
|
+
|
|
473
|
+
Provide exactly one of channel_id or channel_name, and at least one of user_ids, usernames,
|
|
474
|
+
or emails.
|
|
475
|
+
|
|
476
|
+
The tool will resolve usernames and emails to user IDs before inviting them.
|
|
477
|
+
Up to 100 users may be invited at once.
|
|
478
|
+
"""
|
|
479
|
+
# XOR validation: exactly one of channel_id or channel_name must be provided
|
|
480
|
+
if not ((channel_id is None) ^ (channel_name is None)):
|
|
481
|
+
raise ToolExecutionError("Provide exactly one of channel_id OR channel_name.")
|
|
482
|
+
|
|
483
|
+
if not any([user_ids, usernames, emails]):
|
|
484
|
+
raise ToolExecutionError(
|
|
485
|
+
"Provide at least one of user_ids, usernames, or emails to invite."
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
auth_token = context.get_auth_token_or_empty()
|
|
489
|
+
|
|
490
|
+
# Resolve channel name to ID if needed
|
|
491
|
+
resolved_channel_id = channel_id
|
|
492
|
+
if not resolved_channel_id:
|
|
493
|
+
channel = await get_channel_by_name(auth_token, cast(str, channel_name))
|
|
494
|
+
resolved_channel_id = channel["id"]
|
|
495
|
+
|
|
496
|
+
# Use the shared helper to invite users
|
|
497
|
+
invite_result = await invite_users_to_conversation(
|
|
498
|
+
context=context,
|
|
499
|
+
conversation_id=cast(str, resolved_channel_id),
|
|
500
|
+
user_ids=user_ids,
|
|
501
|
+
usernames=usernames,
|
|
502
|
+
emails=emails,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
invited_channel = invite_result.get("channel", {})
|
|
506
|
+
channel_display = invited_channel.get("name", resolved_channel_id)
|
|
507
|
+
message = f"Successfully invited {invite_result['invited_count']} user(s) to {channel_display}"
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
"success": True,
|
|
511
|
+
"channel_id": resolved_channel_id,
|
|
512
|
+
"channel_name": invited_channel.get("name"),
|
|
513
|
+
"invited_user_ids": invite_result["invited_user_ids"],
|
|
514
|
+
"invited_count": invite_result["invited_count"],
|
|
515
|
+
"message": message,
|
|
516
|
+
}
|
arcade_slack/utils.py
CHANGED
|
@@ -7,7 +7,8 @@ from datetime import datetime, timezone
|
|
|
7
7
|
from typing import Any, cast
|
|
8
8
|
|
|
9
9
|
from arcade_tdk import ToolContext
|
|
10
|
-
from arcade_tdk.errors import RetryableToolError
|
|
10
|
+
from arcade_tdk.errors import RetryableToolError, ToolExecutionError
|
|
11
|
+
from slack_sdk.web.async_client import AsyncWebClient
|
|
11
12
|
|
|
12
13
|
from arcade_slack.constants import (
|
|
13
14
|
MAX_CONCURRENT_REQUESTS,
|
|
@@ -569,7 +570,7 @@ async def populate_users_in_messages(auth_token: str, messages: list[dict]) -> l
|
|
|
569
570
|
reaction["users"] = reaction_users
|
|
570
571
|
# If any data is missing, just leave the message as it is
|
|
571
572
|
except Exception as exc:
|
|
572
|
-
logger.exception(exc)
|
|
573
|
+
logger.exception(exc)
|
|
573
574
|
|
|
574
575
|
return messages
|
|
575
576
|
|
|
@@ -613,3 +614,114 @@ def get_user_ids_from_messages(messages: list[dict]) -> list[str]:
|
|
|
613
614
|
user_ids.extend(reaction.get("users", []))
|
|
614
615
|
|
|
615
616
|
return user_ids
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def format_invite_errors(errors: Any) -> str:
|
|
620
|
+
"""Format error messages from Slack invite API partial errors."""
|
|
621
|
+
if isinstance(errors, list):
|
|
622
|
+
formatted_errors = []
|
|
623
|
+
for error in errors:
|
|
624
|
+
if isinstance(error, dict):
|
|
625
|
+
error_code = error.get("error") or error.get("code") or error.get("message")
|
|
626
|
+
user_id = error.get("user") or error.get("user_id")
|
|
627
|
+
if error_code and user_id:
|
|
628
|
+
formatted_errors.append(f"{error_code} ({user_id})")
|
|
629
|
+
elif error_code:
|
|
630
|
+
formatted_errors.append(str(error_code))
|
|
631
|
+
elif user_id:
|
|
632
|
+
formatted_errors.append(f"unknown_error ({user_id})")
|
|
633
|
+
else:
|
|
634
|
+
formatted_errors.append(str(error))
|
|
635
|
+
else:
|
|
636
|
+
formatted_errors.append(str(error))
|
|
637
|
+
if formatted_errors:
|
|
638
|
+
return ", ".join(formatted_errors)
|
|
639
|
+
|
|
640
|
+
return str(errors)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def raise_for_invite_errors(errors: Any, conversation_id: str | None) -> None:
|
|
644
|
+
"""Raise ToolExecutionError for partial invite errors from a 200 response.
|
|
645
|
+
|
|
646
|
+
Slack's conversations.invite API can return ok: true with an errors array
|
|
647
|
+
for partial failures. This function handles those cases.
|
|
648
|
+
"""
|
|
649
|
+
if not errors:
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
error_message = format_invite_errors(errors)
|
|
653
|
+
raise ToolExecutionError(f"Failed to invite some users to conversation: {error_message}")
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
async def invite_users_to_conversation(
|
|
657
|
+
context: ToolContext,
|
|
658
|
+
conversation_id: str,
|
|
659
|
+
user_ids: list[str] | None = None,
|
|
660
|
+
usernames: list[str] | None = None,
|
|
661
|
+
emails: list[str] | None = None,
|
|
662
|
+
) -> dict[str, Any]:
|
|
663
|
+
"""Internal helper to invite users to a conversation (channel or MPIM).
|
|
664
|
+
|
|
665
|
+
This function handles the core logic of resolving users and inviting them.
|
|
666
|
+
It does not wrap errors in try/except - SlackApiError exceptions will bubble
|
|
667
|
+
up to be handled by the SlackErrorAdapter.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
context: The tool context
|
|
671
|
+
conversation_id: The ID of the conversation to invite users to
|
|
672
|
+
user_ids: Optional list of Slack user IDs
|
|
673
|
+
usernames: Optional list of Slack usernames
|
|
674
|
+
emails: Optional list of email addresses
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
A dict with invited_user_ids and invited_count
|
|
678
|
+
|
|
679
|
+
Raises:
|
|
680
|
+
ToolExecutionError: If no valid users found or if partial errors occur
|
|
681
|
+
"""
|
|
682
|
+
from arcade_slack.user_retrieval import get_users_by_id_username_or_email
|
|
683
|
+
|
|
684
|
+
# Resolve usernames and emails to user IDs
|
|
685
|
+
resolved_user_ids = user_ids.copy() if user_ids else []
|
|
686
|
+
|
|
687
|
+
if usernames or emails:
|
|
688
|
+
users = await get_users_by_id_username_or_email(
|
|
689
|
+
context=context,
|
|
690
|
+
usernames=usernames,
|
|
691
|
+
emails=emails,
|
|
692
|
+
)
|
|
693
|
+
resolved_user_ids.extend([user["id"] for user in users])
|
|
694
|
+
|
|
695
|
+
if not resolved_user_ids:
|
|
696
|
+
raise ToolExecutionError("No valid users found to invite to the conversation.")
|
|
697
|
+
|
|
698
|
+
# Remove duplicates
|
|
699
|
+
resolved_user_ids = list(set(resolved_user_ids))
|
|
700
|
+
|
|
701
|
+
# Limit to 100 users per Slack API requirements
|
|
702
|
+
if len(resolved_user_ids) > 100:
|
|
703
|
+
user_count = len(resolved_user_ids)
|
|
704
|
+
raise ToolExecutionError(
|
|
705
|
+
f"Cannot invite more than 100 users at once. You provided {user_count} users."
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
slack_client = AsyncWebClient(token=context.get_auth_token_or_empty())
|
|
709
|
+
|
|
710
|
+
# Slack expects a comma-separated string of user IDs
|
|
711
|
+
users_str = ",".join(resolved_user_ids)
|
|
712
|
+
response = await slack_client.conversations_invite(
|
|
713
|
+
channel=conversation_id,
|
|
714
|
+
users=users_str,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
response_data = cast(dict[str, Any], response.data)
|
|
718
|
+
|
|
719
|
+
# Handle partial errors (200 response with errors array)
|
|
720
|
+
invite_errors = response_data.get("errors")
|
|
721
|
+
raise_for_invite_errors(invite_errors, conversation_id)
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
"invited_user_ids": resolved_user_ids,
|
|
725
|
+
"invited_count": len(resolved_user_ids),
|
|
726
|
+
"channel": response_data.get("channel", {}),
|
|
727
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
arcade_slack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
arcade_slack/constants.py,sha256=ANAZOmNSqrbZHDqEvQfau7_91if9EeKV-G5D0VBB9vo,524
|
|
3
3
|
arcade_slack/conversation_retrieval.py,sha256=VKc0Cc0k4UZ0OON_1l-HxWRzk_nSCzgoP5lgGyUxLFs,1902
|
|
4
|
-
arcade_slack/critics.py,sha256=
|
|
4
|
+
arcade_slack/critics.py,sha256=ZP3jxptwA-8hubUBdhO4cxHrtf_DUAcYt0-TVCXF2AQ,1200
|
|
5
5
|
arcade_slack/custom_types.py,sha256=-23JnfXjMQ-H8CyRL3CEv9opPMySox1KINH5rrPIOt0,928
|
|
6
6
|
arcade_slack/exceptions.py,sha256=YQ4CTa1LbR-G7sjnQM8LB7LruxZDqPzvo-cptFYW7E8,385
|
|
7
7
|
arcade_slack/message_retrieval.py,sha256=ZSspuVfM0yeMMwMFL_M7DeoPnLnUgX_N4_eNPGc4oKk,2488
|
|
8
8
|
arcade_slack/models.py,sha256=R0dze0YMQW_v_GqarA2F3ZMR3ueN_ant9K855N9uqlE,11958
|
|
9
9
|
arcade_slack/user_retrieval.py,sha256=27PzjG4C_5B17-6hh6BjX7tCNOdAKnJ-tZ8sif6IKnY,6452
|
|
10
|
-
arcade_slack/utils.py,sha256=
|
|
10
|
+
arcade_slack/utils.py,sha256=WQXxUMB75GpYZkqhhXBc3NRZOYFlmGFrSM_aT2EtvUo,25840
|
|
11
11
|
arcade_slack/who_am_i_util.py,sha256=Nf6EdRiURva5jMhVx1lFOoKQ-2SuMwjXYbPCmtj6Zcw,3670
|
|
12
12
|
arcade_slack/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
arcade_slack/tools/chat.py,sha256=
|
|
13
|
+
arcade_slack/tools/chat.py,sha256=g7H9aJs8VZod-rMHSaSiAwBUKPY96jvib8DFlmvTTPE,18681
|
|
14
14
|
arcade_slack/tools/system_context.py,sha256=l2gZh_WhrIVL2LzttGAz5yiv0Cl6q_6AFyBBUBskz3E,961
|
|
15
15
|
arcade_slack/tools/users.py,sha256=SZAevnpyI9Q-dSFGZCTOL8zwkLRR0iKh0HE3hk54eUY,3642
|
|
16
|
-
arcade_slack-2.
|
|
17
|
-
arcade_slack-2.
|
|
18
|
-
arcade_slack-2.
|
|
19
|
-
arcade_slack-2.
|
|
16
|
+
arcade_slack-2.1.0.dist-info/METADATA,sha256=lzqZsfeggbNG1lF-CJdX-T7BHXK0-jN2OZXSZFpMi54,1016
|
|
17
|
+
arcade_slack-2.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
+
arcade_slack-2.1.0.dist-info/licenses/LICENSE,sha256=ixeE7aL9b2B-_ZYHTY1vQcJB4NufKeo-LWwKNObGDN0,1960
|
|
19
|
+
arcade_slack-2.1.0.dist-info/RECORD,,
|
|
File without changes
|