From 7435f41432cb91f3509fd50f5dfece2c3165b8a1 Mon Sep 17 00:00:00 2001 From: Sam Kantor Date: Wed, 28 Oct 2026 11:38:56 -0400 Subject: [PATCH] fix: preserve agent prompt fields in button models --- slack_sdk/models/blocks/block_elements.py | 31 ++++++++++- tests/slack_sdk/models/test_elements.py | 36 +++++++++++++ .../web/test_views_publish_agent_prompt.py | 51 +++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 tests/slack_sdk/web/test_views_publish_agent_prompt.py diff --git a/slack_sdk/models/blocks/block_elements.py b/slack_sdk/models/blocks/block_elements.py index 987100dc8..b904814a3 100644 --- a/slack_sdk/models/blocks/block_elements.py +++ b/slack_sdk/models/blocks/block_elements.py @@ -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, @@ -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. @@ -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) @@ -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: @@ -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__( diff --git a/tests/slack_sdk/models/test_elements.py b/tests/slack_sdk/models/test_elements.py index 4a5a062d3..2102db125 100644 --- a/tests/slack_sdk/models/test_elements.py +++ b/tests/slack_sdk/models/test_elements.py @@ -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", diff --git a/tests/slack_sdk/web/test_views_publish_agent_prompt.py b/tests/slack_sdk/web/test_views_publish_agent_prompt.py new file mode 100644 index 000000000..06289cd70 --- /dev/null +++ b/tests/slack_sdk/web/test_views_publish_agent_prompt.py @@ -0,0 +1,51 @@ +from copy import deepcopy +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)