Skip to content

Commit 1c84f7f

Browse files
authored
feat: accept chunks as arguments to chat.{start,append,stop}Stream methods (#1806)
1 parent 96c0f84 commit 1c84f7f

7 files changed

Lines changed: 295 additions & 10 deletions

File tree

slack_sdk/models/messages/chunk.py

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