Skip to content

Commit c7adb46

Browse files
authored
fix: preserve agent prompt fields in button models (#2000)
1 parent 449cf2f commit c7adb46

3 files changed

Lines changed: 117 additions & 1 deletion

File tree

‎slack_sdk/models/blocks/block_elements.py‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,23 @@ class ButtonElement(InteractiveElement):
188188
text_max_length = 75
189189
url_max_length = 3000
190190
value_max_length = 2000
191+
agent_prompt_max_length = 4000
191192

192193
@property
193194
def attributes(self) -> Set[str]: # type: ignore[override]
194-
return super().attributes.union({"text", "url", "value", "style", "confirm", "accessibility_label"})
195+
return super().attributes.union(
196+
{
197+
"text",
198+
"url",
199+
"value",
200+
"style",
201+
"confirm",
202+
"accessibility_label",
203+
"agent_prompt",
204+
"agent_prompt_display",
205+
"visible_to_user_ids",
206+
}
207+
)
195208

196209
def __init__(
197210
self,
@@ -203,6 +216,9 @@ def __init__(
203216
style: Optional[str] = None, # primary, danger
204217
confirm: Optional[Union[dict, ConfirmObject]] = None,
205218
accessibility_label: Optional[str] = None,
219+
agent_prompt: Optional[str] = None,
220+
agent_prompt_display: Optional[str] = None,
221+
visible_to_user_ids: Optional[List[str]] = None,
206222
**others: dict,
207223
):
208224
"""An interactive element that inserts a button.
@@ -234,6 +250,12 @@ def __init__(
234250
accessibility_label: A label for longer descriptive text about a button element.
235251
This label will be read out by screen readers instead of the button text object.
236252
Maximum length for this field is 75 characters.
253+
agent_prompt: A prompt sent to Slackbot when the button is clicked, if Slackbot AI is available to the user.
254+
Maximum length for this field is 4000 characters.
255+
agent_prompt_display: Short text shown instead of the full agent_prompt in the user's Slackbot message.
256+
Only takes effect alongside agent_prompt and defaults to the button's text.
257+
visible_to_user_ids: User IDs for which the button appears.
258+
If not provided, the button is visible to all users.
237259
"""
238260
super().__init__(action_id=action_id, type=self.type)
239261
show_unknown_key_warning(self, others)
@@ -245,6 +267,9 @@ def __init__(
245267
self.style = style
246268
self.confirm = ConfirmObject.parse(confirm) # type: ignore[arg-type]
247269
self.accessibility_label = accessibility_label
270+
self.agent_prompt = agent_prompt
271+
self.agent_prompt_display = agent_prompt_display
272+
self.visible_to_user_ids = visible_to_user_ids
248273

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

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

270299
class LinkButtonElement(ButtonElement):
271300
def __init__(

‎tests/slack_sdk/models/test_elements.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,42 @@ def test_with_input_interactive_element(self):
9292

9393

9494
class ButtonElementTests(unittest.TestCase):
95+
def test_agent_prompt_and_visibility_fields(self):
96+
fields = {
97+
"agent_prompt": "Summarize this incident and suggest next steps.",
98+
"agent_prompt_display": "Summarize this incident",
99+
"visible_to_user_ids": ["U123456", "U789012"],
100+
}
101+
for optional_fields in ({}, *({key: value} for key, value in fields.items()), fields):
102+
with self.subTest(fields=list(optional_fields)):
103+
input = {
104+
"type": "button",
105+
"text": {"type": "plain_text", "text": "Summarize with Slackbot"},
106+
"action_id": "summarize_incident",
107+
**optional_fields,
108+
}
109+
self.assertDictEqual(input, ButtonElement(**input).to_dict())
110+
self.assertDictEqual(input, BlockElement.parse(input).to_dict())
111+
112+
def test_agent_prompt_and_visibility_fields_none_are_omitted(self):
113+
button = ButtonElement(
114+
text="Summarize with Slackbot",
115+
agent_prompt=None,
116+
agent_prompt_display=None,
117+
visible_to_user_ids=None,
118+
).to_dict()
119+
for field in ("agent_prompt", "agent_prompt_display", "visible_to_user_ids"):
120+
self.assertNotIn(field, button)
121+
122+
def test_agent_prompt_max_length(self):
123+
prompt = "a" * 4000
124+
button = ButtonElement(text="Summarize with Slackbot", agent_prompt=prompt).to_dict()
125+
self.assertEqual(prompt, button["agent_prompt"])
126+
127+
def test_agent_prompt_too_long(self):
128+
with self.assertRaisesRegex(SlackObjectFormationError, "agent_prompt"):
129+
ButtonElement(text="Summarize with Slackbot", agent_prompt="a" * 4001).to_dict()
130+
95131
def test_document_1(self):
96132
input = {
97133
"type": "button",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from copy import deepcopy
2+
import unittest
3+
from unittest.mock import patch
4+
5+
from slack_sdk import WebClient
6+
from slack_sdk.models.blocks import ActionsBlock, ButtonElement, SectionBlock
7+
from slack_sdk.models.views import View
8+
9+
10+
class TestViewsPublishAgentPrompt(unittest.TestCase):
11+
def test_models_send_the_same_button_fields_as_raw_json(self):
12+
button = {
13+
"type": "button",
14+
"text": {"type": "plain_text", "text": "Summarize with Slackbot"},
15+
"action_id": "summarize_incident",
16+
"style": "primary",
17+
"agent_prompt": "Summarize this incident and suggest next steps.",
18+
"agent_prompt_display": "Summarize this incident",
19+
"visible_to_user_ids": ["U123456"],
20+
}
21+
blocks = (
22+
{"type": "actions", "elements": [button]},
23+
{
24+
"type": "section",
25+
"text": {"type": "mrkdwn", "text": "Incident details"},
26+
"accessory": button,
27+
},
28+
)
29+
client = WebClient(token="xoxb-api_test")
30+
for block in blocks:
31+
block_class = ActionsBlock if block["type"] == "actions" else SectionBlock
32+
raw_view = {"type": "home", "blocks": [block]}
33+
explicit_button_block = deepcopy(block)
34+
if block["type"] == "actions":
35+
explicit_button_block["elements"] = [ButtonElement(**deepcopy(button))]
36+
else:
37+
explicit_button_block["accessory"] = ButtonElement(**deepcopy(button))
38+
views = {
39+
"raw_json": deepcopy(raw_view),
40+
"button_model": View(type="home", blocks=[block_class(**explicit_button_block)]),
41+
"raw_button_in_block_model": View(type="home", blocks=[block_class(**deepcopy(block))]),
42+
"raw_blocks_in_view_model": View(**deepcopy(raw_view)),
43+
}
44+
for representation, view in views.items():
45+
with self.subTest(placement=block["type"], representation=representation):
46+
# Keep the SDK's serialization intact; intercept only the HTTP transport.
47+
with patch.object(client, "_sync_send", return_value={"ok": True}) as transport:
48+
client.views_publish(user_id="U123456", view=view)
49+
transport.assert_called_once()
50+
request = transport.call_args[1]["req_args"]["json"]
51+
self.assertDictEqual({"user_id": "U123456", "view": raw_view}, request)

0 commit comments

Comments
 (0)