Skip to content
Draft
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
155 changes: 155 additions & 0 deletions slack_sdk/models/messages/chunk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import logging
from typing import Any, 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


class Chunk(JsonObject):
"""
Chunk for streaming messages.

https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming
"""

attributes = {"type"}
logger = logging.getLogger(__name__)

def __init__(
self,
*,
type: Optional[str] = None,
):
self.type = type

@classmethod
def parse(cls, chunk: Union[Dict, "Chunk"]) -> Optional["Chunk"]:
if chunk is None:
return None
elif isinstance(chunk, Chunk):
return chunk
else:
if "type" in chunk:
type = chunk["type"]
if type == MarkdownTextChunk.type:
return MarkdownTextChunk(**chunk)
elif type == TaskUpdateChunk.type:
return TaskUpdateChunk(**chunk)
else:
cls.logger.warning(f"Unknown chunk detected and skipped ({chunk})")
return None
else:
cls.logger.warning(f"Unknown chunk detected and skipped ({chunk})")
return None


class MarkdownTextChunk(Chunk):
type = "markdown_text"

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

def __init__(
self,
*,
text: str,
**others: Dict,
):
"""Used for streaming text content with markdown formatting support.

https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming
"""
super().__init__(type=self.type)
show_unknown_key_warning(self, others)

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"

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

def __init__(
self,
*,
id: str,
title: str,
status: str, # "pending", "in_progress", "complete", "error"
details: Optional[str] = None,
output: Optional[str] = None,
sources: Optional[Sequence[Union[Dict, URLSource]]] = None,
**others: Dict,
):
"""Used for displaying tool execution progress in a timeline-style UI.

https://docs.slack.dev/messaging/sending-and-scheduling-messages#text-streaming
"""
super().__init__(type=self.type)
show_unknown_key_warning(self, others)

self.id = id
self.title = title
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)}")
11 changes: 10 additions & 1 deletion slack_sdk/web/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
from typing import Any, Dict, List, Optional, Sequence, Union

import slack_sdk.errors as e
from slack_sdk.models.messages.chunk import Chunk
from slack_sdk.models.views import View
from slack_sdk.web.async_chat_stream import AsyncChatStream

from ..models.attachments import Attachment
from ..models.blocks import Block, RichTextBlock
from ..models.metadata import Metadata, EntityMetadata, EventAndEntityMetadata
from ..models.metadata import EntityMetadata, EventAndEntityMetadata, Metadata
from .async_base_client import AsyncBaseClient, AsyncSlackResponse
from .internal_utils import (
_parse_web_class_objects,
Expand Down Expand Up @@ -2631,6 +2632,7 @@ async def chat_appendStream(
channel: str,
ts: str,
markdown_text: str,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> AsyncSlackResponse:
"""Appends text to an existing streaming conversation.
Expand All @@ -2641,8 +2643,10 @@ async def chat_appendStream(
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
"chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return await self.api_call("chat.appendStream", json=kwargs)

Expand Down Expand Up @@ -2884,6 +2888,7 @@ async def chat_startStream(
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> AsyncSlackResponse:
"""Starts a new streaming conversation.
Expand All @@ -2896,8 +2901,10 @@ async def chat_startStream(
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
"chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return await self.api_call("chat.startStream", json=kwargs)

Expand All @@ -2909,6 +2916,7 @@ async def chat_stopStream(
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> AsyncSlackResponse:
"""Stops a streaming conversation.
Expand All @@ -2921,6 +2929,7 @@ async def chat_stopStream(
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
"chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
Expand Down
11 changes: 10 additions & 1 deletion slack_sdk/web/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
from typing import Any, Dict, List, Optional, Sequence, Union

import slack_sdk.errors as e
from slack_sdk.models.messages.chunk import Chunk
from slack_sdk.models.views import View
from slack_sdk.web.chat_stream import ChatStream

from ..models.attachments import Attachment
from ..models.blocks import Block, RichTextBlock
from ..models.metadata import Metadata, EntityMetadata, EventAndEntityMetadata
from ..models.metadata import EntityMetadata, EventAndEntityMetadata, Metadata
from .base_client import BaseClient, SlackResponse
from .internal_utils import (
_parse_web_class_objects,
Expand Down Expand Up @@ -2621,6 +2622,7 @@ def chat_appendStream(
channel: str,
ts: str,
markdown_text: str,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Appends text to an existing streaming conversation.
Expand All @@ -2631,8 +2633,10 @@ def chat_appendStream(
"channel": channel,
"ts": ts,
"markdown_text": markdown_text,
"chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.appendStream", json=kwargs)

Expand Down Expand Up @@ -2874,6 +2878,7 @@ def chat_startStream(
markdown_text: Optional[str] = None,
recipient_team_id: Optional[str] = None,
recipient_user_id: Optional[str] = None,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Starts a new streaming conversation.
Expand All @@ -2886,8 +2891,10 @@ def chat_startStream(
"markdown_text": markdown_text,
"recipient_team_id": recipient_team_id,
"recipient_user_id": recipient_user_id,
"chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
kwargs = _remove_none_values(kwargs)
return self.api_call("chat.startStream", json=kwargs)

Expand All @@ -2899,6 +2906,7 @@ def chat_stopStream(
markdown_text: Optional[str] = None,
blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
metadata: Optional[Union[Dict, Metadata]] = None,
chunks: Optional[Sequence[Union[Dict, Chunk]]] = None,
**kwargs,
) -> SlackResponse:
"""Stops a streaming conversation.
Expand All @@ -2911,6 +2919,7 @@ def chat_stopStream(
"markdown_text": markdown_text,
"blocks": blocks,
"metadata": metadata,
"chunks": chunks,
}
)
_parse_web_class_objects(kwargs)
Expand Down
14 changes: 11 additions & 3 deletions slack_sdk/web/internal_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
from ssl import SSLContext
from typing import Any, Dict, Optional, Sequence, Union
from urllib.parse import urljoin
from urllib.request import OpenerDirector, ProxyHandler, HTTPSHandler, Request, urlopen
from urllib.request import HTTPSHandler, OpenerDirector, ProxyHandler, Request, urlopen

from slack_sdk import version
from slack_sdk.errors import SlackRequestError
from slack_sdk.models.attachments import Attachment
from slack_sdk.models.blocks import Block
from slack_sdk.models.metadata import Metadata, EventAndEntityMetadata, EntityMetadata
from slack_sdk.models.messages.chunk import Chunk
from slack_sdk.models.metadata import EntityMetadata, EventAndEntityMetadata, Metadata


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


def _parse_web_class_objects(kwargs) -> None:
def to_dict(obj: Union[Dict, Block, Attachment, Metadata, EventAndEntityMetadata, EntityMetadata]):
def to_dict(obj: Union[Dict, Block, Attachment, Chunk, Metadata, EventAndEntityMetadata, EntityMetadata]):
if isinstance(obj, Block):
return obj.to_dict()
if isinstance(obj, Attachment):
return obj.to_dict()
if isinstance(obj, Chunk):
return obj.to_dict()
if isinstance(obj, Metadata):
return obj.to_dict()
if isinstance(obj, EventAndEntityMetadata):
Expand All @@ -211,6 +214,11 @@ def to_dict(obj: Union[Dict, Block, Attachment, Metadata, EventAndEntityMetadata
dict_attachments = [to_dict(a) for a in attachments]
kwargs.update({"attachments": dict_attachments})

chunks = kwargs.get("chunks", None)
if chunks is not None and isinstance(chunks, Sequence) and (not isinstance(chunks, str)):
dict_chunks = [to_dict(c) for c in chunks]
kwargs.update({"chunks": dict_chunks})

metadata = kwargs.get("metadata", None)
if metadata is not None and (
isinstance(metadata, Metadata)
Expand Down
Loading