Skip to content

Commit c3bdb2d

Browse files
committed
feat: accept chunks as arguments to chat.{start,append,stop}Stream methods
1 parent 96c0f84 commit c3bdb2d

File tree

5 files changed

+198
-6
lines changed

5 files changed

+198
-6
lines changed

slack_sdk/models/messages/chunk.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import logging
2+
from typing import Any, Dict, Literal, Optional, Sequence, Set, Union
3+
4+
from slack_sdk.errors import SlackObjectFormationError
5+
from slack_sdk.models import show_unknown_key_warning
6+
from slack_sdk.models.basic_objects import JsonObject
7+
8+
LOGGER = logging.getLogger(__name__)
9+
10+
11+
class Chunk(JsonObject):
12+
"""
13+
Chunk for streaming messages.
14+
15+
https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming
16+
"""
17+
18+
attributes = {"type"}
19+
logger = logging.getLogger(__name__)
20+
21+
def __init__(
22+
self,
23+
*,
24+
type: Optional[str] = None,
25+
):
26+
self.type = type
27+
28+
@classmethod
29+
def parse(cls, chunk: Union[Dict, "Chunk"]) -> Optional["Chunk"]:
30+
if chunk is None:
31+
return None
32+
elif isinstance(chunk, Chunk):
33+
return chunk
34+
else:
35+
if "type" in chunk:
36+
type = chunk["type"]
37+
if type == MarkdownTextChunk.type:
38+
return MarkdownTextChunk(**chunk)
39+
elif type == TaskUpdateChunk.type:
40+
return TaskUpdateChunk(**chunk)
41+
else:
42+
cls.logger.warning(f"Unknown chunk detected and skipped ({chunk})")
43+
return None
44+
else:
45+
cls.logger.warning(f"Unknown chunk detected and skipped ({chunk})")
46+
return None
47+
48+
49+
class MarkdownTextChunk(Chunk):
50+
type = "markdown_text"
51+
52+
@property
53+
def attributes(self) -> Set[str]: # type: ignore[override]
54+
return super().attributes.union({"text"})
55+
56+
def __init__(
57+
self,
58+
*,
59+
text: str,
60+
**others: Dict,
61+
):
62+
"""Used for streaming text content with markdown formatting support.
63+
64+
https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming
65+
"""
66+
super().__init__(type=self.type)
67+
show_unknown_key_warning(self, others)
68+
69+
self.text = text
70+
71+
72+
class URLSource(JsonObject):
73+
type = "url"
74+
75+
@property
76+
def attributes(self) -> Set[str]: # type: ignore[override]
77+
return super().attributes.union(
78+
{
79+
"url",
80+
"text",
81+
"icon_url",
82+
}
83+
)
84+
85+
def __init__(
86+
self,
87+
*,
88+
url: str,
89+
text: str,
90+
icon_url: Optional[str] = None,
91+
**others: Dict,
92+
):
93+
show_unknown_key_warning(self, others)
94+
self._url = url
95+
self._text = text
96+
self._icon_url = icon_url
97+
98+
def to_dict(self) -> Dict[str, Any]:
99+
self.validate_json()
100+
json: Dict[str, Union[str, Dict]] = {
101+
"type": self.type,
102+
"url": self._url,
103+
"text": self._text,
104+
}
105+
if self._icon_url:
106+
json["icon_url"] = self._icon_url
107+
return json
108+
109+
110+
class TaskUpdateChunk(Chunk):
111+
type = "task_update"
112+
113+
@property
114+
def attributes(self) -> Set[str]: # type: ignore[override]
115+
return super().attributes.union(
116+
{
117+
"id",
118+
"title",
119+
"status",
120+
"details",
121+
"output",
122+
"sources",
123+
}
124+
)
125+
126+
def __init__(
127+
self,
128+
*,
129+
id: str,
130+
title: str,
131+
status: Literal["pending", "in_progress", "complete", "error"],
132+
details: Optional[str] = None,
133+
output: Optional[str] = None,
134+
sources: Optional[Sequence[Union[Dict, URLSource]]] = None,
135+
**others: Dict,
136+
):
137+
"""Used for displaying tool execution progress in a timeline-style UI.
138+
139+
https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming
140+
"""
141+
super().__init__(type=self.type)
142+
show_unknown_key_warning(self, others)
143+
144+
self.id = id
145+
self.title = title
146+
self.status = status
147+
self.details = details
148+
self.output = output
149+
if sources is not None:
150+
self.sources = []
151+
for src in sources:
152+
if isinstance(src, Dict):
153+
self.sources.append(src)
154+
elif isinstance(src, URLSource):
155+
self.sources.append(src.to_dict())
156+
else:
157+
raise SlackObjectFormationError(f"Unsupported type for source in task update chunk: {type(src)}")

slack_sdk/web/async_client.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
from typing import Any, Dict, List, Optional, Sequence, Union
1818

1919
import slack_sdk.errors as e
20+
from slack_sdk.models.messages.chunk import Chunk
2021
from slack_sdk.models.views import View
2122
from slack_sdk.web.async_chat_stream import AsyncChatStream
2223

2324
from ..models.attachments import Attachment
2425
from ..models.blocks import Block, RichTextBlock
25-
from ..models.metadata import Metadata, EntityMetadata, EventAndEntityMetadata
26+
from ..models.metadata import EntityMetadata, EventAndEntityMetadata, Metadata
2627
from .async_base_client import AsyncBaseClient, AsyncSlackResponse
2728
from .internal_utils import (
2829
_parse_web_class_objects,
@@ -2631,6 +2632,7 @@ async def chat_appendStream(
26312632
channel: str,
26322633
ts: str,
26332634
markdown_text: str,
2635+
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
26342636
**kwargs,
26352637
) -> AsyncSlackResponse:
26362638
"""Appends text to an existing streaming conversation.
@@ -2641,8 +2643,10 @@ async def chat_appendStream(
26412643
"channel": channel,
26422644
"ts": ts,
26432645
"markdown_text": markdown_text,
2646+
"chunks": chunks,
26442647
}
26452648
)
2649+
_parse_web_class_objects(kwargs)
26462650
kwargs = _remove_none_values(kwargs)
26472651
return await self.api_call("chat.appendStream", json=kwargs)
26482652

@@ -2884,6 +2888,7 @@ async def chat_startStream(
28842888
markdown_text: Optional[str] = None,
28852889
recipient_team_id: Optional[str] = None,
28862890
recipient_user_id: Optional[str] = None,
2891+
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
28872892
**kwargs,
28882893
) -> AsyncSlackResponse:
28892894
"""Starts a new streaming conversation.
@@ -2896,8 +2901,10 @@ async def chat_startStream(
28962901
"markdown_text": markdown_text,
28972902
"recipient_team_id": recipient_team_id,
28982903
"recipient_user_id": recipient_user_id,
2904+
"chunks": chunks,
28992905
}
29002906
)
2907+
_parse_web_class_objects(kwargs)
29012908
kwargs = _remove_none_values(kwargs)
29022909
return await self.api_call("chat.startStream", json=kwargs)
29032910

@@ -2909,6 +2916,7 @@ async def chat_stopStream(
29092916
markdown_text: Optional[str] = None,
29102917
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
29112918
metadata: Optional[Union[Dict, Metadata]] = None,
2919+
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
29122920
**kwargs,
29132921
) -> AsyncSlackResponse:
29142922
"""Stops a streaming conversation.
@@ -2921,6 +2929,7 @@ async def chat_stopStream(
29212929
"markdown_text": markdown_text,
29222930
"blocks": blocks,
29232931
"metadata": metadata,
2932+
"chunks": chunks,
29242933
}
29252934
)
29262935
_parse_web_class_objects(kwargs)

slack_sdk/web/client.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
from typing import Any, Dict, List, Optional, Sequence, Union
88

99
import slack_sdk.errors as e
10+
from slack_sdk.models.messages.chunk import Chunk
1011
from slack_sdk.models.views import View
1112
from slack_sdk.web.chat_stream import ChatStream
1213

1314
from ..models.attachments import Attachment
1415
from ..models.blocks import Block, RichTextBlock
15-
from ..models.metadata import Metadata, EntityMetadata, EventAndEntityMetadata
16+
from ..models.metadata import EntityMetadata, EventAndEntityMetadata, Metadata
1617
from .base_client import BaseClient, SlackResponse
1718
from .internal_utils import (
1819
_parse_web_class_objects,
@@ -2621,6 +2622,7 @@ def chat_appendStream(
26212622
channel: str,
26222623
ts: str,
26232624
markdown_text: str,
2625+
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
26242626
**kwargs,
26252627
) -> SlackResponse:
26262628
"""Appends text to an existing streaming conversation.
@@ -2631,8 +2633,10 @@ def chat_appendStream(
26312633
"channel": channel,
26322634
"ts": ts,
26332635
"markdown_text": markdown_text,
2636+
"chunks": chunks,
26342637
}
26352638
)
2639+
_parse_web_class_objects(kwargs)
26362640
kwargs = _remove_none_values(kwargs)
26372641
return self.api_call("chat.appendStream", json=kwargs)
26382642

@@ -2874,6 +2878,7 @@ def chat_startStream(
28742878
markdown_text: Optional[str] = None,
28752879
recipient_team_id: Optional[str] = None,
28762880
recipient_user_id: Optional[str] = None,
2881+
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
28772882
**kwargs,
28782883
) -> SlackResponse:
28792884
"""Starts a new streaming conversation.
@@ -2886,8 +2891,10 @@ def chat_startStream(
28862891
"markdown_text": markdown_text,
28872892
"recipient_team_id": recipient_team_id,
28882893
"recipient_user_id": recipient_user_id,
2894+
"chunks": chunks,
28892895
}
28902896
)
2897+
_parse_web_class_objects(kwargs)
28912898
kwargs = _remove_none_values(kwargs)
28922899
return self.api_call("chat.startStream", json=kwargs)
28932900

@@ -2899,6 +2906,7 @@ def chat_stopStream(
28992906
markdown_text: Optional[str] = None,
29002907
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
29012908
metadata: Optional[Union[Dict, Metadata]] = None,
2909+
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
29022910
**kwargs,
29032911
) -> SlackResponse:
29042912
"""Stops a streaming conversation.
@@ -2911,6 +2919,7 @@ def chat_stopStream(
29112919
"markdown_text": markdown_text,
29122920
"blocks": blocks,
29132921
"metadata": metadata,
2922+
"chunks": chunks,
29142923
}
29152924
)
29162925
_parse_web_class_objects(kwargs)

slack_sdk/web/internal_utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
from ssl import SSLContext
1212
from typing import Any, Dict, Optional, Sequence, Union
1313
from urllib.parse import urljoin
14-
from urllib.request import OpenerDirector, ProxyHandler, HTTPSHandler, Request, urlopen
14+
from urllib.request import HTTPSHandler, OpenerDirector, ProxyHandler, Request, urlopen
1515

1616
from slack_sdk import version
1717
from slack_sdk.errors import SlackRequestError
1818
from slack_sdk.models.attachments import Attachment
1919
from slack_sdk.models.blocks import Block
20-
from slack_sdk.models.metadata import Metadata, EventAndEntityMetadata, EntityMetadata
20+
from slack_sdk.models.messages.chunk import Chunk
21+
from slack_sdk.models.metadata import EntityMetadata, EventAndEntityMetadata, Metadata
2122

2223

2324
def convert_bool_to_0_or_1(params: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
@@ -187,11 +188,13 @@ def _build_req_args(
187188

188189

189190
def _parse_web_class_objects(kwargs) -> None:
190-
def to_dict(obj: Union[Dict, Block, Attachment, Metadata, EventAndEntityMetadata, EntityMetadata]):
191+
def to_dict(obj: Union[Dict, Block, Attachment, Chunk, Metadata, EventAndEntityMetadata, EntityMetadata]):
191192
if isinstance(obj, Block):
192193
return obj.to_dict()
193194
if isinstance(obj, Attachment):
194195
return obj.to_dict()
196+
if isinstance(obj, Chunk):
197+
return obj.to_dict()
195198
if isinstance(obj, Metadata):
196199
return obj.to_dict()
197200
if isinstance(obj, EventAndEntityMetadata):
@@ -211,6 +214,11 @@ def to_dict(obj: Union[Dict, Block, Attachment, Metadata, EventAndEntityMetadata
211214
dict_attachments = [to_dict(a) for a in attachments]
212215
kwargs.update({"attachments": dict_attachments})
213216

217+
chunks = kwargs.get("chunks", None)
218+
if chunks is not None and isinstance(chunks, Sequence) and (not isinstance(chunks, str)):
219+
dict_chunks = [to_dict(c) for c in chunks]
220+
kwargs.update({"chunks": dict_chunks})
221+
214222
metadata = kwargs.get("metadata", None)
215223
if metadata is not None and (
216224
isinstance(metadata, Metadata)

0 commit comments

Comments
 (0)