Skip to content
Open
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
6 changes: 6 additions & 0 deletions slack_sdk/models/blocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
StaticSelectElement,
TimePickerElement,
UrlInputElement,
UrlSourceElement,
UserMultiSelectElement,
UserSelectElement,
)
Expand All @@ -70,9 +71,11 @@
ImageBlock,
InputBlock,
MarkdownBlock,
PlanBlock,
RichTextBlock,
SectionBlock,
TableBlock,
TaskCardBlock,
VideoBlock,
)

Expand Down Expand Up @@ -111,6 +114,7 @@
"PlainTextInputElement",
"EmailInputElement",
"UrlInputElement",
"UrlSourceElement",
"NumberInputElement",
"RadioButtonsElement",
"SelectElement",
Expand All @@ -135,8 +139,10 @@
"ImageBlock",
"InputBlock",
"MarkdownBlock",
"PlanBlock",
"SectionBlock",
"TableBlock",
"TaskCardBlock",
"VideoBlock",
"RichTextBlock",
]
42 changes: 42 additions & 0 deletions slack_sdk/models/blocks/block_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -1654,6 +1654,48 @@ def __init__(
self.dispatch_action_config = dispatch_action_config


# -------------------------------------------------
# Url Source Element
# -------------------------------------------------


class UrlSourceElement(BlockElement):
type = "url"

@property
def attributes(self) -> Set[str]: # type: ignore[override]
return super().attributes.union(
{
"url",
"text",
"icon_url",
}
)

def __init__(
self,
*,
url: str,
text: str,
icon_url: Optional[str] = None,
**others: Dict,
):
"""
A URL source element to reference in a task card block.
https://docs.slack.dev/reference/block-kit/block-elements/url-source-element

Args:
url (required): The URL type source.
text (required): Display text for the URL.
icon_url: Optional icon URL to display with the source.
"""
super().__init__(type=self.type)
show_unknown_key_warning(self, others)
self.url = url
self.text = text
self.icon_url = icon_url


# -------------------------------------------------
# Number Input Element
# -------------------------------------------------
Expand Down
106 changes: 106 additions & 0 deletions slack_sdk/models/blocks/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
InputInteractiveElement,
InteractiveElement,
RichTextElement,
UrlSourceElement,
)

# -------------------------------------------------
Expand Down Expand Up @@ -97,6 +98,10 @@ def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]:
return RichTextBlock(**block)
elif type == TableBlock.type:
return TableBlock(**block)
elif type == TaskCardBlock.type:
return TaskCardBlock(**block)
elif type == PlanBlock.type:
return PlanBlock(**block)
else:
cls.logger.warning(f"Unknown block detected and skipped ({block})")
return None
Expand Down Expand Up @@ -777,3 +782,104 @@ def __init__(
@JsonValidator("rows attribute must be specified")
def _validate_rows(self):
return self.rows is not None and len(self.rows) > 0


class TaskCardBlock(Block):
type = "task_card"

@property
def attributes(self) -> Set[str]: # type: ignore[override]
return super().attributes.union(
{
"task_id",
"title",
"details",
"output",
"sources",
"status",
}
)

def __init__(
self,
*,
task_id: str,
title: str,
details: Optional[Union[RichTextBlock, dict]] = None,
output: Optional[Union[RichTextBlock, dict]] = None,
sources: Optional[Sequence[Union[UrlSourceElement, dict]]] = None,
status: str, # pending, in_progress, complete, error
block_id: Optional[str] = None,
**others: dict,
):
"""A discrete action or tool call.
https://docs.slack.dev/reference/block-kit/blocks/task-card-block/

Args:
block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
Maximum length for this field is 255 characters.
block_id should be unique for each message and each iteration of a message.
If a message is updated, use a new block_id.
task_id (required): ID for the task
title (required): Title of the task in plain text
details: Details of the task in the form of a single "rich_text" entity.
output: Output of the task in the form of a single "rich_text" entity.
sources: List of sources used to generate a response
status: The state of a task. Either "pending" or "in_progress" or "complete" or "error".
"""
super().__init__(type=self.type, block_id=block_id)
show_unknown_key_warning(self, others)

self.task_id = task_id
self.title = title
self.details = details
self.output = output
self.sources = sources
self.status = status

@JsonValidator("status must be an expected value (pending, in_progress, complete, or error)")
def _validate_rows(self):
return self.status in ["pending", "in_progress", "complete", "error"]


class PlanBlock(Block):
type = "plan"

@property
def attributes(self) -> Set[str]: # type: ignore[override]
return super().attributes.union(
{
"plan_id",
"title",
"tasks",
}
)

def __init__(
self,
*,
plan_id: str,
title: str,
tasks: Optional[Sequence[Union[Dict, TaskCardBlock]]] = None,
block_id: Optional[str] = None,
**others: dict,
):
"""A collection of related tasks.
https://docs.slack.dev/reference/block-kit/blocks/plan-block/

Args:
block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
Maximum length for this field is 255 characters.
block_id should be unique for each message and each iteration of a message.
If a message is updated, use a new block_id.
plan_id (required): ID for the plan (May be removed / made optional, feel free to pass in a random UUID
for now)
title (required): Title of the plan in plain text
tasks: Details of the task in the form of a single "rich_text" entity.
"""
super().__init__(type=self.type, block_id=block_id)
show_unknown_key_warning(self, others)

self.plan_id = plan_id
self.title = title
self.tasks = tasks
54 changes: 4 additions & 50 deletions slack_sdk/models/messages/chunk.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
from typing import Any, Dict, Optional, Sequence, Set, Union
from typing import Dict, Optional, Sequence, Set, Union

from slack_sdk.errors import SlackObjectFormationError
from slack_sdk.models import show_unknown_key_warning
from slack_sdk.models.basic_objects import JsonObject
from slack_sdk.models.blocks.block_elements import UrlSourceElement


class Chunk(JsonObject):
Expand Down Expand Up @@ -67,44 +67,6 @@ def __init__(
self.text = text


class URLSource(JsonObject):
type = "url"

@property
def attributes(self) -> Set[str]:
return super().attributes.union(
{
"url",
"text",
"icon_url",
}
)

def __init__(
self,
*,
url: str,
text: str,
icon_url: Optional[str] = None,
**others: Dict,
):
show_unknown_key_warning(self, others)
self._url = url
self._text = text
self._icon_url = icon_url

def to_dict(self) -> Dict[str, Any]:
self.validate_json()
json: Dict[str, Union[str, Dict]] = {
"type": self.type,
"url": self._url,
"text": self._text,
}
if self._icon_url:
json["icon_url"] = self._icon_url
return json


class TaskUpdateChunk(Chunk):
type = "task_update"

Expand All @@ -129,7 +91,7 @@ def __init__(
status: str, # "pending", "in_progress", "complete", "error"
details: Optional[str] = None,
output: Optional[str] = None,
sources: Optional[Sequence[Union[Dict, URLSource]]] = None,
sources: Optional[Sequence[Union[Dict, UrlSourceElement]]] = None,
**others: Dict,
):
"""Used for displaying tool execution progress in a timeline-style UI.
Expand All @@ -144,12 +106,4 @@ def __init__(
self.status = status
self.details = details
self.output = output
if sources is not None:
self.sources = []
for src in sources:
if isinstance(src, Dict):
self.sources.append(src)
elif isinstance(src, URLSource):
self.sources.append(src.to_dict())
else:
raise SlackObjectFormationError(f"Unsupported type for source in task update chunk: {type(src)}")
self.sources = sources
83 changes: 83 additions & 0 deletions tests/slack_sdk/models/test_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
Option,
OverflowMenuElement,
PlainTextObject,
PlanBlock,
RawTextObject,
RichTextBlock,
RichTextElementParts,
Expand All @@ -31,6 +32,7 @@
SectionBlock,
StaticSelectElement,
TableBlock,
TaskCardBlock,
VideoBlock,
)
from slack_sdk.models.blocks.basic_components import FeedbackButtonObject, SlackFile
Expand Down Expand Up @@ -890,6 +892,87 @@ def test_text_length_12001(self):
MarkdownBlock(**input).validate_json()


# ----------------------------------------------
# Plan
# ----------------------------------------------


class PlanBlockTests(unittest.TestCase):
def test_document(self):
input = {
"type": "plan",
"plan_id": "plan_1",
"title": "Thinking completed",
"tasks": [
{
"task_id": "call_001",
"title": "Fetched user profile information",
"status": "in_progress",
"details": {
"type": "rich_text",
"elements": [
{"type": "rich_text_section", "elements": [{"type": "text", "text": "Searched database..."}]}
],
},
"output": {
"type": "rich_text",
"elements": [
{"type": "rich_text_section", "elements": [{"type": "text", "text": "Profile data loaded"}]}
],
},
},
{
"task_id": "call_002",
"title": "Checked user permissions",
"status": "pending",
},
{
"task_id": "call_003",
"title": "Generated comprehensive user report",
"status": "complete",
"output": {
"type": "rich_text",
"elements": [
{"type": "rich_text_section", "elements": [{"type": "text", "text": "15 data points compiled"}]}
],
},
},
],
}
self.assertDictEqual(input, PlanBlock(**input).to_dict())
self.assertDictEqual(input, Block.parse(input).to_dict())


# ----------------------------------------------
# Task card
# ----------------------------------------------


class TaskCardBlockTests(unittest.TestCase):
def test_document(self):
input = {
"type": "task_card",
"task_id": "task_1",
"title": "Fetching weather data",
"status": "pending",
"output": {
"type": "rich_text",
"elements": [
{
"type": "rich_text_section",
"elements": [{"type": "text", "text": "Found weather data for Chicago from 2 sources"}],
}
],
},
"sources": [
{"type": "url", "url": "https://weather.com/", "text": "weather.com"},
{"type": "url", "url": "https://www.accuweather.com/", "text": "accuweather.com"},
],
}
self.assertDictEqual(input, TaskCardBlock(**input).to_dict())
self.assertDictEqual(input, Block.parse(input).to_dict())


# ----------------------------------------------
# Video
# ----------------------------------------------
Expand Down
Loading