Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion slack_sdk/models/blocks/block_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,23 @@ class ButtonElement(InteractiveElement):
text_max_length = 75
url_max_length = 3000
value_max_length = 2000
agent_prompt_max_length = 4000

@property
def attributes(self) -> Set[str]: # type: ignore[override]
return super().attributes.union({"text", "url", "value", "style", "confirm", "accessibility_label"})
return super().attributes.union(
{
"text",
"url",
"value",
"style",
"confirm",
"accessibility_label",
"agent_prompt",
"agent_prompt_display",
"visible_to_user_ids",
}
)

def __init__(
self,
Expand All @@ -203,6 +216,9 @@ def __init__(
style: Optional[str] = None, # primary, danger
confirm: Optional[Union[dict, ConfirmObject]] = None,
accessibility_label: Optional[str] = None,
agent_prompt: Optional[str] = None,
agent_prompt_display: Optional[str] = None,
visible_to_user_ids: Optional[List[str]] = None,
**others: dict,
):
"""An interactive element that inserts a button.
Expand Down Expand Up @@ -234,6 +250,12 @@ def __init__(
accessibility_label: A label for longer descriptive text about a button element.
This label will be read out by screen readers instead of the button text object.
Maximum length for this field is 75 characters.
agent_prompt: A prompt sent to Slackbot when the button is clicked, if Slackbot AI is available to the user.
Maximum length for this field is 4000 characters.
agent_prompt_display: Short text shown instead of the full agent_prompt in the user's Slackbot message.
Only takes effect alongside agent_prompt and defaults to the button's text.
visible_to_user_ids: User IDs for which the button appears.
If not provided, the button is visible to all users.
"""
super().__init__(action_id=action_id, type=self.type)
show_unknown_key_warning(self, others)
Expand All @@ -245,6 +267,9 @@ def __init__(
self.style = style
self.confirm = ConfirmObject.parse(confirm) # type: ignore[arg-type]
self.accessibility_label = accessibility_label
self.agent_prompt = agent_prompt
self.agent_prompt_display = agent_prompt_display
self.visible_to_user_ids = visible_to_user_ids

@JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
def _validate_text_length(self) -> bool:
Expand All @@ -266,6 +291,10 @@ def _validate_style_valid(self):
def _validate_accessibility_label_length(self) -> bool:
return self.accessibility_label is None or len(self.accessibility_label) <= self.text_max_length

@JsonValidator(f"agent_prompt attribute cannot exceed {agent_prompt_max_length} characters")
def _validate_agent_prompt_length(self) -> bool:
return self.agent_prompt is None or len(self.agent_prompt) <= self.agent_prompt_max_length


class LinkButtonElement(ButtonElement):
def __init__(
Expand Down
36 changes: 36 additions & 0 deletions tests/slack_sdk/models/test_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,42 @@ def test_with_input_interactive_element(self):


class ButtonElementTests(unittest.TestCase):
def test_agent_prompt_and_visibility_fields(self):
fields = {
"agent_prompt": "Summarize this incident and suggest next steps.",
"agent_prompt_display": "Summarize this incident",
"visible_to_user_ids": ["U123456", "U789012"],
}
for optional_fields in ({}, *({key: value} for key, value in fields.items()), fields):
with self.subTest(fields=list(optional_fields)):
input = {
"type": "button",
"text": {"type": "plain_text", "text": "Summarize with Slackbot"},
"action_id": "summarize_incident",
**optional_fields,
}
self.assertDictEqual(input, ButtonElement(**input).to_dict())
self.assertDictEqual(input, BlockElement.parse(input).to_dict())

def test_agent_prompt_and_visibility_fields_none_are_omitted(self):
button = ButtonElement(
text="Summarize with Slackbot",
agent_prompt=None,
agent_prompt_display=None,
visible_to_user_ids=None,
).to_dict()
for field in ("agent_prompt", "agent_prompt_display", "visible_to_user_ids"):
self.assertNotIn(field, button)

def test_agent_prompt_max_length(self):
prompt = "a" * 4000
button = ButtonElement(text="Summarize with Slackbot", agent_prompt=prompt).to_dict()
self.assertEqual(prompt, button["agent_prompt"])

def test_agent_prompt_too_long(self):
with self.assertRaisesRegex(SlackObjectFormationError, "agent_prompt"):
ButtonElement(text="Summarize with Slackbot", agent_prompt="a" * 4001).to_dict()

def test_document_1(self):
input = {
"type": "button",
Expand Down
51 changes: 51 additions & 0 deletions tests/slack_sdk/web/test_views_publish_agent_prompt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from copy import deepcopy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: The model tests in test_elements.py already cover the round trip, and this one patches the private _sync_send, which our other web tests don't do. Not blocking, so I'd merge as-is and leave it to @WilliamBergamin to keep or move it in a follow-up PR.

import unittest
from unittest.mock import patch

from slack_sdk import WebClient
from slack_sdk.models.blocks import ActionsBlock, ButtonElement, SectionBlock
from slack_sdk.models.views import View


class TestViewsPublishAgentPrompt(unittest.TestCase):
def test_models_send_the_same_button_fields_as_raw_json(self):
button = {
"type": "button",
"text": {"type": "plain_text", "text": "Summarize with Slackbot"},
"action_id": "summarize_incident",
"style": "primary",
"agent_prompt": "Summarize this incident and suggest next steps.",
"agent_prompt_display": "Summarize this incident",
"visible_to_user_ids": ["U123456"],
}
blocks = (
{"type": "actions", "elements": [button]},
{
"type": "section",
"text": {"type": "mrkdwn", "text": "Incident details"},
"accessory": button,
},
)
client = WebClient(token="xoxb-api_test")
for block in blocks:
block_class = ActionsBlock if block["type"] == "actions" else SectionBlock
raw_view = {"type": "home", "blocks": [block]}
explicit_button_block = deepcopy(block)
if block["type"] == "actions":
explicit_button_block["elements"] = [ButtonElement(**deepcopy(button))]
else:
explicit_button_block["accessory"] = ButtonElement(**deepcopy(button))
views = {
"raw_json": deepcopy(raw_view),
"button_model": View(type="home", blocks=[block_class(**explicit_button_block)]),
"raw_button_in_block_model": View(type="home", blocks=[block_class(**deepcopy(block))]),
"raw_blocks_in_view_model": View(**deepcopy(raw_view)),
}
for representation, view in views.items():
with self.subTest(placement=block["type"], representation=representation):
# Keep the SDK's serialization intact; intercept only the HTTP transport.
with patch.object(client, "_sync_send", return_value={"ok": True}) as transport:
client.views_publish(user_id="U123456", view=view)
transport.assert_called_once()
request = transport.call_args[1]["req_args"]["json"]
self.assertDictEqual({"user_id": "U123456", "view": raw_view}, request)
Loading