kosong.message

  1from abc import ABC
  2from typing import Any, ClassVar, Literal, cast, override
  3
  4from pydantic import BaseModel, GetCoreSchemaHandler, field_serializer, field_validator
  5from pydantic_core import core_schema
  6
  7from kosong.utils.typing import JsonType
  8
  9
 10class MergeableMixin:
 11    def merge_in_place(self, other: Any) -> bool:
 12        """Merge the other part into the current part. Return True if the merge is successful."""
 13        return False
 14
 15
 16class ContentPart(BaseModel, ABC, MergeableMixin):
 17    """
 18    A part of a message content.
 19
 20    This is the abstract base class for all supported content parts. Subclasses must define a `type`
 21    field of type `str` and optional other fields specific to the content part.
 22
 23    For Kosong users, you typically do not need to subclass this directly. Instead, use the provided
 24    subclasses like `TextPart`, `ThinkPart`, `ImageURLPart`, etc. Unless you are implementing custom
 25    `ChatProvider`s that supports new content part types.
 26    """
 27
 28    __content_part_registry: ClassVar[dict[str, type["ContentPart"]]] = {}
 29
 30    type: str
 31    ...  # to be added by subclasses
 32
 33    def __init_subclass__(cls, **kwargs: Any) -> None:
 34        super().__init_subclass__(**kwargs)
 35
 36        invalid_subclass_error_msg = (
 37            f"ContentPart subclass {cls.__name__} must have a `type` field of type `str`"
 38        )
 39
 40        type_value = getattr(cls, "type", None)
 41        if type_value is None or not isinstance(type_value, str):
 42            raise ValueError(invalid_subclass_error_msg)
 43
 44        cls.__content_part_registry[type_value] = cls
 45
 46    @classmethod
 47    def __get_pydantic_core_schema__(
 48        cls, source_type: Any, handler: GetCoreSchemaHandler
 49    ) -> core_schema.CoreSchema:
 50        # If we're dealing with the base ContentPart class, use custom validation
 51        if cls.__name__ == "ContentPart":
 52
 53            def validate_content_part(value: Any) -> Any:
 54                # if it's already an instance of a ContentPart subclass, return it
 55                if hasattr(value, "__class__") and issubclass(value.__class__, cls):
 56                    return value
 57
 58                # if it's a dict with a type field, dispatch to the appropriate subclass
 59                if isinstance(value, dict) and "type" in value:
 60                    type_value: Any | None = cast(dict[str, Any], value).get("type")
 61                    if not isinstance(type_value, str):
 62                        raise ValueError(f"Cannot validate {value} as ContentPart")
 63                    target_class = cls.__content_part_registry[type_value]
 64                    return target_class.model_validate(value)
 65
 66                raise ValueError(f"Cannot validate {value} as ContentPart")
 67
 68            return core_schema.no_info_plain_validator_function(validate_content_part)
 69
 70        # for subclasses, use the default schema
 71        return handler(source_type)
 72
 73
 74class TextPart(ContentPart):
 75    """
 76    >>> TextPart(text="Hello, world!").model_dump()
 77    {'type': 'text', 'text': 'Hello, world!'}
 78    """
 79
 80    type: str = "text"
 81    text: str
 82
 83    @override
 84    def merge_in_place(self, other: Any) -> bool:
 85        if not isinstance(other, TextPart):
 86            return False
 87        self.text += other.text
 88        return True
 89
 90
 91class ThinkPart(ContentPart):
 92    """
 93    >>> ThinkPart(think="I think I need to think about this.").model_dump()
 94    {'type': 'think', 'think': 'I think I need to think about this.', 'encrypted': None}
 95    """
 96
 97    type: str = "think"
 98    think: str
 99    encrypted: str | None = None
100    """Encrypted thinking content, or signature."""
101
102    @override
103    def merge_in_place(self, other: Any) -> bool:
104        if not isinstance(other, ThinkPart):
105            return False
106        if self.encrypted:
107            return False
108        self.think += other.think
109        if other.encrypted:
110            self.encrypted = other.encrypted
111        return True
112
113
114class ImageURLPart(ContentPart):
115    """
116    >>> ImageURLPart(
117    ...     image_url=ImageURLPart.ImageURL(url="https://example.com/image.png")
118    ... ).model_dump()
119    {'type': 'image_url', 'image_url': {'url': 'https://example.com/image.png', 'id': None}}
120    """
121
122    class ImageURL(BaseModel):
123        """Image URL payload."""
124
125        url: str
126        """The URL of the image, can be data URI scheme like `data:image/png;base64,...`."""
127        id: str | None = None
128        """The ID of the image, to allow LLMs to distinguish different images."""
129
130    type: str = "image_url"
131    image_url: ImageURL
132
133
134class AudioURLPart(ContentPart):
135    """
136    >>> AudioURLPart(
137    ...     audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")
138    ... ).model_dump()
139    {'type': 'audio_url', 'audio_url': {'url': 'https://example.com/audio.mp3', 'id': None}}
140    """
141
142    class AudioURL(BaseModel):
143        """Audio URL payload."""
144
145        url: str
146        """The URL of the audio, can be data URI scheme like `data:audio/aac;base64,...`."""
147        id: str | None = None
148        """The ID of the audio, to allow LLMs to distinguish different audios."""
149
150    type: str = "audio_url"
151    audio_url: AudioURL
152
153
154class VideoURLPart(ContentPart):
155    """
156    >>> VideoURLPart(
157    ...     video_url=VideoURLPart.VideoURL(url="https://example.com/video.mp4")
158    ... ).model_dump()
159    {'type': 'video_url', 'video_url': {'url': 'https://example.com/video.mp4', 'id': None}}
160    """
161
162    class VideoURL(BaseModel):
163        """Video URL payload."""
164
165        url: str
166        """The URL of the video, can be data URI scheme like `data:video/mp4;base64,...`."""
167        id: str | None = None
168        """The ID of the video, to allow LLMs to distinguish different videos."""
169
170    type: str = "video_url"
171    video_url: VideoURL
172
173
174class ToolCall(BaseModel, MergeableMixin):
175    """
176    A tool call requested by the assistant.
177
178    >>> ToolCall(
179    ...     id="123",
180    ...     function=ToolCall.FunctionBody(name="function", arguments="{}"),
181    ... ).model_dump(exclude_none=True)
182    {'type': 'function', 'id': '123', 'function': {'name': 'function', 'arguments': '{}'}}
183    """
184
185    class FunctionBody(BaseModel):
186        """Tool call function body."""
187
188        name: str
189        """The name of the tool to be called."""
190        arguments: str | None
191        """Arguments of the tool call in JSON string format."""
192
193    type: Literal["function"] = "function"
194
195    id: str
196    """The ID of the tool call."""
197    function: FunctionBody
198    """The function body of the tool call."""
199    extras: dict[str, JsonType] | None = None
200    """Extra information about the tool call."""
201
202    @override
203    def merge_in_place(self, other: Any) -> bool:
204        if not isinstance(other, ToolCallPart):
205            return False
206        if self.function.arguments is None:
207            self.function.arguments = other.arguments_part
208        else:
209            self.function.arguments += other.arguments_part or ""
210        return True
211
212
213class ToolCallPart(BaseModel, MergeableMixin):
214    """A part of the tool call."""
215
216    arguments_part: str | None = None
217    """A part of the arguments of the tool call."""
218
219    @override
220    def merge_in_place(self, other: Any) -> bool:
221        if not isinstance(other, ToolCallPart):
222            return False
223        if self.arguments_part is None:
224            self.arguments_part = other.arguments_part
225        else:
226            self.arguments_part += other.arguments_part or ""
227        return True
228
229
230type Role = Literal[
231    # for OpenAI API, this should be converted to `developer`
232    # OpenAI & Kimi support system messages in the middle of the conversation.
233    # Anthropic only support system messages at the beginning https://docs.claude.com/en/api/messages#body-messages
234    # In this case, we map `system` message to a `user` message wrapped in `<system></system>` tags.
235    "system",
236    "user",
237    "assistant",
238    "tool",
239]
240"""The role of a message sender."""
241
242
243class Message(BaseModel):
244    """A message in a conversation."""
245
246    role: Role
247    """The role of the message sender."""
248
249    name: str | None = None
250
251    content: list[ContentPart]
252    """
253    The content of the message.
254    Empty list `[]` will be interpreted as no content.
255    """
256
257    tool_calls: list[ToolCall] | None = None
258    """Tool calls requested by the assistant in this message."""
259
260    tool_call_id: str | None = None
261    """The ID of the tool call if this message is a tool response."""
262
263    partial: bool | None = None
264
265    @field_serializer("content")
266    def _serialize_content(self, content: list[ContentPart]) -> str | list[dict[str, Any]] | None:
267        if len(content) == 1 and isinstance(content[0], TextPart):
268            return content[0].text
269        return [part.model_dump() for part in content]
270
271    @field_validator("content", mode="before")
272    @classmethod
273    def _coerce_none_content(cls, value: Any) -> Any:
274        if value is None:
275            return []
276        if isinstance(value, str):
277            return [TextPart(text=value)]
278        return value
279
280    def __init__(
281        self,
282        *,
283        role: Role,
284        content: list[ContentPart] | ContentPart | str,
285        tool_calls: list[ToolCall] | None = None,
286        tool_call_id: str | None = None,
287        **data: Any,
288    ) -> None:
289        if isinstance(content, str):
290            content = [TextPart(text=content)]
291        elif isinstance(content, ContentPart):
292            content = [content]
293        super().__init__(
294            role=role,
295            content=content,
296            tool_calls=tool_calls,
297            tool_call_id=tool_call_id,
298            **data,
299        )
300
301    def extract_text(self, sep: str = "") -> str:
302        """Extract and concatenate all text parts in the message content."""
303        return sep.join(part.text for part in self.content if isinstance(part, TextPart))
class MergeableMixin:
11class MergeableMixin:
12    def merge_in_place(self, other: Any) -> bool:
13        """Merge the other part into the current part. Return True if the merge is successful."""
14        return False
def merge_in_place(self, other: Any) -> bool:
12    def merge_in_place(self, other: Any) -> bool:
13        """Merge the other part into the current part. Return True if the merge is successful."""
14        return False

Merge the other part into the current part. Return True if the merge is successful.

class ContentPart(pydantic.main.BaseModel, abc.ABC, MergeableMixin):
17class ContentPart(BaseModel, ABC, MergeableMixin):
18    """
19    A part of a message content.
20
21    This is the abstract base class for all supported content parts. Subclasses must define a `type`
22    field of type `str` and optional other fields specific to the content part.
23
24    For Kosong users, you typically do not need to subclass this directly. Instead, use the provided
25    subclasses like `TextPart`, `ThinkPart`, `ImageURLPart`, etc. Unless you are implementing custom
26    `ChatProvider`s that supports new content part types.
27    """
28
29    __content_part_registry: ClassVar[dict[str, type["ContentPart"]]] = {}
30
31    type: str
32    ...  # to be added by subclasses
33
34    def __init_subclass__(cls, **kwargs: Any) -> None:
35        super().__init_subclass__(**kwargs)
36
37        invalid_subclass_error_msg = (
38            f"ContentPart subclass {cls.__name__} must have a `type` field of type `str`"
39        )
40
41        type_value = getattr(cls, "type", None)
42        if type_value is None or not isinstance(type_value, str):
43            raise ValueError(invalid_subclass_error_msg)
44
45        cls.__content_part_registry[type_value] = cls
46
47    @classmethod
48    def __get_pydantic_core_schema__(
49        cls, source_type: Any, handler: GetCoreSchemaHandler
50    ) -> core_schema.CoreSchema:
51        # If we're dealing with the base ContentPart class, use custom validation
52        if cls.__name__ == "ContentPart":
53
54            def validate_content_part(value: Any) -> Any:
55                # if it's already an instance of a ContentPart subclass, return it
56                if hasattr(value, "__class__") and issubclass(value.__class__, cls):
57                    return value
58
59                # if it's a dict with a type field, dispatch to the appropriate subclass
60                if isinstance(value, dict) and "type" in value:
61                    type_value: Any | None = cast(dict[str, Any], value).get("type")
62                    if not isinstance(type_value, str):
63                        raise ValueError(f"Cannot validate {value} as ContentPart")
64                    target_class = cls.__content_part_registry[type_value]
65                    return target_class.model_validate(value)
66
67                raise ValueError(f"Cannot validate {value} as ContentPart")
68
69            return core_schema.no_info_plain_validator_function(validate_content_part)
70
71        # for subclasses, use the default schema
72        return handler(source_type)

A part of a message content.

This is the abstract base class for all supported content parts. Subclasses must define a type field of type str and optional other fields specific to the content part.

For Kosong users, you typically do not need to subclass this directly. Instead, use the provided subclasses like TextPart, ThinkPart, ImageURLPart, etc. Unless you are implementing custom ChatProviders that supports new content part types.

type: str = PydanticUndefined
Inherited Members
MergeableMixin
merge_in_place
class TextPart(ContentPart):
75class TextPart(ContentPart):
76    """
77    >>> TextPart(text="Hello, world!").model_dump()
78    {'type': 'text', 'text': 'Hello, world!'}
79    """
80
81    type: str = "text"
82    text: str
83
84    @override
85    def merge_in_place(self, other: Any) -> bool:
86        if not isinstance(other, TextPart):
87            return False
88        self.text += other.text
89        return True
>>> TextPart(text="Hello, world!").model_dump()
{'type': 'text', 'text': 'Hello, world!'}
type: str = 'text'
text: str = PydanticUndefined
@override
def merge_in_place(self, other: Any) -> bool:
84    @override
85    def merge_in_place(self, other: Any) -> bool:
86        if not isinstance(other, TextPart):
87            return False
88        self.text += other.text
89        return True

Merge the other part into the current part. Return True if the merge is successful.

class ThinkPart(ContentPart):
 92class ThinkPart(ContentPart):
 93    """
 94    >>> ThinkPart(think="I think I need to think about this.").model_dump()
 95    {'type': 'think', 'think': 'I think I need to think about this.', 'encrypted': None}
 96    """
 97
 98    type: str = "think"
 99    think: str
100    encrypted: str | None = None
101    """Encrypted thinking content, or signature."""
102
103    @override
104    def merge_in_place(self, other: Any) -> bool:
105        if not isinstance(other, ThinkPart):
106            return False
107        if self.encrypted:
108            return False
109        self.think += other.think
110        if other.encrypted:
111            self.encrypted = other.encrypted
112        return True
>>> ThinkPart(think="I think I need to think about this.").model_dump()
{'type': 'think', 'think': 'I think I need to think about this.', 'encrypted': None}
type: str = 'think'
think: str = PydanticUndefined
encrypted: str | None = None

Encrypted thinking content, or signature.

@override
def merge_in_place(self, other: Any) -> bool:
103    @override
104    def merge_in_place(self, other: Any) -> bool:
105        if not isinstance(other, ThinkPart):
106            return False
107        if self.encrypted:
108            return False
109        self.think += other.think
110        if other.encrypted:
111            self.encrypted = other.encrypted
112        return True

Merge the other part into the current part. Return True if the merge is successful.

class ImageURLPart(ContentPart):
115class ImageURLPart(ContentPart):
116    """
117    >>> ImageURLPart(
118    ...     image_url=ImageURLPart.ImageURL(url="https://example.com/image.png")
119    ... ).model_dump()
120    {'type': 'image_url', 'image_url': {'url': 'https://example.com/image.png', 'id': None}}
121    """
122
123    class ImageURL(BaseModel):
124        """Image URL payload."""
125
126        url: str
127        """The URL of the image, can be data URI scheme like `data:image/png;base64,...`."""
128        id: str | None = None
129        """The ID of the image, to allow LLMs to distinguish different images."""
130
131    type: str = "image_url"
132    image_url: ImageURL
>>> ImageURLPart(
...     image_url=ImageURLPart.ImageURL(url="https://example.com/image.png")
... ).model_dump()
{'type': 'image_url', 'image_url': {'url': 'https://example.com/image.png', 'id': None}}
type: str = 'image_url'
image_url: ImageURLPart.ImageURL = PydanticUndefined
Inherited Members
MergeableMixin
merge_in_place
class ImageURLPart.ImageURL(pydantic.main.BaseModel):
123    class ImageURL(BaseModel):
124        """Image URL payload."""
125
126        url: str
127        """The URL of the image, can be data URI scheme like `data:image/png;base64,...`."""
128        id: str | None = None
129        """The ID of the image, to allow LLMs to distinguish different images."""

Image URL payload.

url: str = PydanticUndefined

The URL of the image, can be data URI scheme like data:image/png;base64,....

id: str | None = None

The ID of the image, to allow LLMs to distinguish different images.

class AudioURLPart(ContentPart):
135class AudioURLPart(ContentPart):
136    """
137    >>> AudioURLPart(
138    ...     audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")
139    ... ).model_dump()
140    {'type': 'audio_url', 'audio_url': {'url': 'https://example.com/audio.mp3', 'id': None}}
141    """
142
143    class AudioURL(BaseModel):
144        """Audio URL payload."""
145
146        url: str
147        """The URL of the audio, can be data URI scheme like `data:audio/aac;base64,...`."""
148        id: str | None = None
149        """The ID of the audio, to allow LLMs to distinguish different audios."""
150
151    type: str = "audio_url"
152    audio_url: AudioURL
>>> AudioURLPart(
...     audio_url=AudioURLPart.AudioURL(url="https://example.com/audio.mp3")
... ).model_dump()
{'type': 'audio_url', 'audio_url': {'url': 'https://example.com/audio.mp3', 'id': None}}
type: str = 'audio_url'
audio_url: AudioURLPart.AudioURL = PydanticUndefined
Inherited Members
MergeableMixin
merge_in_place
class AudioURLPart.AudioURL(pydantic.main.BaseModel):
143    class AudioURL(BaseModel):
144        """Audio URL payload."""
145
146        url: str
147        """The URL of the audio, can be data URI scheme like `data:audio/aac;base64,...`."""
148        id: str | None = None
149        """The ID of the audio, to allow LLMs to distinguish different audios."""

Audio URL payload.

url: str = PydanticUndefined

The URL of the audio, can be data URI scheme like data:audio/aac;base64,....

id: str | None = None

The ID of the audio, to allow LLMs to distinguish different audios.

class VideoURLPart(ContentPart):
155class VideoURLPart(ContentPart):
156    """
157    >>> VideoURLPart(
158    ...     video_url=VideoURLPart.VideoURL(url="https://example.com/video.mp4")
159    ... ).model_dump()
160    {'type': 'video_url', 'video_url': {'url': 'https://example.com/video.mp4', 'id': None}}
161    """
162
163    class VideoURL(BaseModel):
164        """Video URL payload."""
165
166        url: str
167        """The URL of the video, can be data URI scheme like `data:video/mp4;base64,...`."""
168        id: str | None = None
169        """The ID of the video, to allow LLMs to distinguish different videos."""
170
171    type: str = "video_url"
172    video_url: VideoURL
>>> VideoURLPart(
...     video_url=VideoURLPart.VideoURL(url="https://example.com/video.mp4")
... ).model_dump()
{'type': 'video_url', 'video_url': {'url': 'https://example.com/video.mp4', 'id': None}}
type: str = 'video_url'
video_url: VideoURLPart.VideoURL = PydanticUndefined
Inherited Members
MergeableMixin
merge_in_place
class VideoURLPart.VideoURL(pydantic.main.BaseModel):
163    class VideoURL(BaseModel):
164        """Video URL payload."""
165
166        url: str
167        """The URL of the video, can be data URI scheme like `data:video/mp4;base64,...`."""
168        id: str | None = None
169        """The ID of the video, to allow LLMs to distinguish different videos."""

Video URL payload.

url: str = PydanticUndefined

The URL of the video, can be data URI scheme like data:video/mp4;base64,....

id: str | None = None

The ID of the video, to allow LLMs to distinguish different videos.

class ToolCall(pydantic.main.BaseModel, MergeableMixin):
175class ToolCall(BaseModel, MergeableMixin):
176    """
177    A tool call requested by the assistant.
178
179    >>> ToolCall(
180    ...     id="123",
181    ...     function=ToolCall.FunctionBody(name="function", arguments="{}"),
182    ... ).model_dump(exclude_none=True)
183    {'type': 'function', 'id': '123', 'function': {'name': 'function', 'arguments': '{}'}}
184    """
185
186    class FunctionBody(BaseModel):
187        """Tool call function body."""
188
189        name: str
190        """The name of the tool to be called."""
191        arguments: str | None
192        """Arguments of the tool call in JSON string format."""
193
194    type: Literal["function"] = "function"
195
196    id: str
197    """The ID of the tool call."""
198    function: FunctionBody
199    """The function body of the tool call."""
200    extras: dict[str, JsonType] | None = None
201    """Extra information about the tool call."""
202
203    @override
204    def merge_in_place(self, other: Any) -> bool:
205        if not isinstance(other, ToolCallPart):
206            return False
207        if self.function.arguments is None:
208            self.function.arguments = other.arguments_part
209        else:
210            self.function.arguments += other.arguments_part or ""
211        return True

A tool call requested by the assistant.

>>> ToolCall(
...     id="123",
...     function=ToolCall.FunctionBody(name="function", arguments="{}"),
... ).model_dump(exclude_none=True)
{'type': 'function', 'id': '123', 'function': {'name': 'function', 'arguments': '{}'}}
type: Literal['function'] = 'function'
id: str = PydanticUndefined

The ID of the tool call.

function: ToolCall.FunctionBody = PydanticUndefined

The function body of the tool call.

extras: dict[str, JsonType] | None = None

Extra information about the tool call.

@override
def merge_in_place(self, other: Any) -> bool:
203    @override
204    def merge_in_place(self, other: Any) -> bool:
205        if not isinstance(other, ToolCallPart):
206            return False
207        if self.function.arguments is None:
208            self.function.arguments = other.arguments_part
209        else:
210            self.function.arguments += other.arguments_part or ""
211        return True

Merge the other part into the current part. Return True if the merge is successful.

class ToolCall.FunctionBody(pydantic.main.BaseModel):
186    class FunctionBody(BaseModel):
187        """Tool call function body."""
188
189        name: str
190        """The name of the tool to be called."""
191        arguments: str | None
192        """Arguments of the tool call in JSON string format."""

Tool call function body.

name: str = PydanticUndefined

The name of the tool to be called.

arguments: str | None = PydanticUndefined

Arguments of the tool call in JSON string format.

class ToolCallPart(pydantic.main.BaseModel, MergeableMixin):
214class ToolCallPart(BaseModel, MergeableMixin):
215    """A part of the tool call."""
216
217    arguments_part: str | None = None
218    """A part of the arguments of the tool call."""
219
220    @override
221    def merge_in_place(self, other: Any) -> bool:
222        if not isinstance(other, ToolCallPart):
223            return False
224        if self.arguments_part is None:
225            self.arguments_part = other.arguments_part
226        else:
227            self.arguments_part += other.arguments_part or ""
228        return True

A part of the tool call.

arguments_part: str | None = None

A part of the arguments of the tool call.

@override
def merge_in_place(self, other: Any) -> bool:
220    @override
221    def merge_in_place(self, other: Any) -> bool:
222        if not isinstance(other, ToolCallPart):
223            return False
224        if self.arguments_part is None:
225            self.arguments_part = other.arguments_part
226        else:
227            self.arguments_part += other.arguments_part or ""
228        return True

Merge the other part into the current part. Return True if the merge is successful.

type Role = Literal['system', 'user', 'assistant', 'tool']

The role of a message sender.

class Message(pydantic.main.BaseModel):
244class Message(BaseModel):
245    """A message in a conversation."""
246
247    role: Role
248    """The role of the message sender."""
249
250    name: str | None = None
251
252    content: list[ContentPart]
253    """
254    The content of the message.
255    Empty list `[]` will be interpreted as no content.
256    """
257
258    tool_calls: list[ToolCall] | None = None
259    """Tool calls requested by the assistant in this message."""
260
261    tool_call_id: str | None = None
262    """The ID of the tool call if this message is a tool response."""
263
264    partial: bool | None = None
265
266    @field_serializer("content")
267    def _serialize_content(self, content: list[ContentPart]) -> str | list[dict[str, Any]] | None:
268        if len(content) == 1 and isinstance(content[0], TextPart):
269            return content[0].text
270        return [part.model_dump() for part in content]
271
272    @field_validator("content", mode="before")
273    @classmethod
274    def _coerce_none_content(cls, value: Any) -> Any:
275        if value is None:
276            return []
277        if isinstance(value, str):
278            return [TextPart(text=value)]
279        return value
280
281    def __init__(
282        self,
283        *,
284        role: Role,
285        content: list[ContentPart] | ContentPart | str,
286        tool_calls: list[ToolCall] | None = None,
287        tool_call_id: str | None = None,
288        **data: Any,
289    ) -> None:
290        if isinstance(content, str):
291            content = [TextPart(text=content)]
292        elif isinstance(content, ContentPart):
293            content = [content]
294        super().__init__(
295            role=role,
296            content=content,
297            tool_calls=tool_calls,
298            tool_call_id=tool_call_id,
299            **data,
300        )
301
302    def extract_text(self, sep: str = "") -> str:
303        """Extract and concatenate all text parts in the message content."""
304        return sep.join(part.text for part in self.content if isinstance(part, TextPart))

A message in a conversation.

role: Role = PydanticUndefined

The role of the message sender.

name: str | None = None
content: list[ContentPart] = PydanticUndefined

The content of the message. Empty list [] will be interpreted as no content.

tool_calls: list[ToolCall] | None = None

Tool calls requested by the assistant in this message.

tool_call_id: str | None = None

The ID of the tool call if this message is a tool response.

partial: bool | None = None
def extract_text(self, sep: str = '') -> str:
302    def extract_text(self, sep: str = "") -> str:
303        """Extract and concatenate all text parts in the message content."""
304        return sep.join(part.text for part in self.content if isinstance(part, TextPart))

Extract and concatenate all text parts in the message content.