Skip to content

Commit 62049aa

Browse files
committed
support multiple runs
1 parent b28931a commit 62049aa

File tree

10 files changed

+682
-13
lines changed

10 files changed

+682
-13
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ cd python-sdk
4646
poetry install
4747

4848
# Run tests
49-
poetry run pytest
49+
python -m unittest discover tests
5050

5151
# Build distribution
5252
poetry build

docs/concepts/events.mdx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,10 @@ for an incoming message, such as creating a new message bubble with a loading
194194
indicator. The `role` property identifies whether the message is coming from the
195195
assistant or potentially another participant in the conversation.
196196

197-
| Property | Description |
198-
| ----------- | ---------------------------------------------- |
199-
| `messageId` | Unique identifier for the message |
200-
| `role` | Role of the message sender (e.g., "assistant") |
197+
| Property | Description |
198+
| ----------- | --------------------------------------------------------------------------------- |
199+
| `messageId` | Unique identifier for the message |
200+
| `role` | Role of the message sender ("developer", "system", "assistant", "user", "tool") |
201201

202202
### TextMessageContent
203203

@@ -231,6 +231,22 @@ automatic scrolling to ensure the full message is visible.
231231
| ----------- | -------------------------------------- |
232232
| `messageId` | Matches the ID from `TextMessageStart` |
233233

234+
### TextMessageChunk
235+
236+
A self-contained text message event that combines start, content, and end.
237+
238+
The `TextMessageChunk` event provides a convenient way to send complete text messages
239+
in a single event instead of the three-event sequence (start, content, end). This is
240+
particularly useful for simple messages or when the entire content is available at once.
241+
The event includes both the message metadata and content, making it more efficient for
242+
non-streaming scenarios.
243+
244+
| Property | Description |
245+
| ----------- | ------------------------------------------------------------------------------------- |
246+
| `messageId` | Optional unique identifier for the message |
247+
| `role` | Optional role of the sender ("developer", "system", "assistant", "user", "tool") |
248+
| `delta` | Optional text content of the message |
249+
234250
## Tool Call Events
235251

236252
These events represent the lifecycle of tool calls made by agents. Tool calls

python-sdk/ag_ui/core/events.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77

88
from pydantic import Field
99

10-
from .types import ConfiguredBaseModel, Message, State
10+
from .types import ConfiguredBaseModel, Message, State, Role
11+
12+
# Text messages can have any role except "tool"
13+
TextMessageRole = Literal["developer", "system", "assistant", "user"]
1114

1215

1316
class EventType(str, Enum):
@@ -55,7 +58,7 @@ class TextMessageStartEvent(BaseEvent):
5558
"""
5659
type: Literal[EventType.TEXT_MESSAGE_START] = EventType.TEXT_MESSAGE_START # pyright: ignore[reportIncompatibleVariableOverride]
5760
message_id: str
58-
role: Literal["assistant"] = "assistant"
61+
role: TextMessageRole = "assistant"
5962

6063

6164
class TextMessageContentEvent(BaseEvent):
@@ -80,7 +83,7 @@ class TextMessageChunkEvent(BaseEvent):
8083
"""
8184
type: Literal[EventType.TEXT_MESSAGE_CHUNK] = EventType.TEXT_MESSAGE_CHUNK # pyright: ignore[reportIncompatibleVariableOverride]
8285
message_id: Optional[str] = None
83-
role: Optional[Literal["assistant"]] = None
86+
role: Optional[TextMessageRole] = None
8487
delta: Optional[str] = None
8588

8689
class ThinkingTextMessageStartEvent(BaseEvent):
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Tests for text message events with different roles."""
2+
3+
import unittest
4+
from pydantic import ValidationError
5+
from ag_ui.core import (
6+
EventType,
7+
TextMessageStartEvent,
8+
TextMessageContentEvent,
9+
TextMessageEndEvent,
10+
TextMessageChunkEvent,
11+
Role,
12+
)
13+
14+
# Test all available roles for text messages (excluding "tool")
15+
TEXT_MESSAGE_ROLES = ["developer", "system", "assistant", "user"]
16+
17+
18+
class TestTextMessageRoles(unittest.TestCase):
19+
"""Test text message events with different roles."""
20+
21+
def test_text_message_start_with_all_roles(self) -> None:
22+
"""Test TextMessageStartEvent with different roles."""
23+
for role in TEXT_MESSAGE_ROLES:
24+
with self.subTest(role=role):
25+
event = TextMessageStartEvent(
26+
message_id="test-msg",
27+
role=role,
28+
)
29+
30+
self.assertEqual(event.type, EventType.TEXT_MESSAGE_START)
31+
self.assertEqual(event.message_id, "test-msg")
32+
self.assertEqual(event.role, role)
33+
34+
def test_text_message_chunk_with_all_roles(self) -> None:
35+
"""Test TextMessageChunkEvent with different roles."""
36+
for role in TEXT_MESSAGE_ROLES:
37+
with self.subTest(role=role):
38+
event = TextMessageChunkEvent(
39+
message_id="test-msg",
40+
role=role,
41+
delta=f"Hello from {role}",
42+
)
43+
44+
self.assertEqual(event.type, EventType.TEXT_MESSAGE_CHUNK)
45+
self.assertEqual(event.message_id, "test-msg")
46+
self.assertEqual(event.role, role)
47+
self.assertEqual(event.delta, f"Hello from {role}")
48+
49+
def test_text_message_chunk_without_role(self) -> None:
50+
"""Test TextMessageChunkEvent without role (should be optional)."""
51+
event = TextMessageChunkEvent(
52+
message_id="test-msg",
53+
delta="Hello without role",
54+
)
55+
56+
self.assertEqual(event.type, EventType.TEXT_MESSAGE_CHUNK)
57+
self.assertEqual(event.message_id, "test-msg")
58+
self.assertIsNone(event.role)
59+
self.assertEqual(event.delta, "Hello without role")
60+
61+
def test_multiple_messages_different_roles(self) -> None:
62+
"""Test creating multiple messages with different roles."""
63+
events = []
64+
65+
for role in TEXT_MESSAGE_ROLES:
66+
start_event = TextMessageStartEvent(
67+
message_id=f"msg-{role}",
68+
role=role,
69+
)
70+
content_event = TextMessageContentEvent(
71+
message_id=f"msg-{role}",
72+
delta=f"Message from {role}",
73+
)
74+
end_event = TextMessageEndEvent(
75+
message_id=f"msg-{role}",
76+
)
77+
78+
events.extend([start_event, content_event, end_event])
79+
80+
# Verify we have 3 events per role
81+
self.assertEqual(len(events), len(TEXT_MESSAGE_ROLES) * 3)
82+
83+
# Verify each start event has the correct role
84+
for i, role in enumerate(TEXT_MESSAGE_ROLES):
85+
start_event = events[i * 3]
86+
self.assertIsInstance(start_event, TextMessageStartEvent)
87+
self.assertEqual(start_event.role, role)
88+
self.assertEqual(start_event.message_id, f"msg-{role}")
89+
90+
def test_text_message_serialization(self) -> None:
91+
"""Test that text message events serialize correctly with roles."""
92+
for role in TEXT_MESSAGE_ROLES:
93+
with self.subTest(role=role):
94+
event = TextMessageStartEvent(
95+
message_id="test-msg",
96+
role=role,
97+
)
98+
99+
# Convert to dict and back
100+
event_dict = event.model_dump()
101+
self.assertEqual(event_dict["role"], role)
102+
self.assertEqual(event_dict["type"], EventType.TEXT_MESSAGE_START)
103+
self.assertEqual(event_dict["message_id"], "test-msg")
104+
105+
# Recreate from dict
106+
new_event = TextMessageStartEvent(**event_dict)
107+
self.assertEqual(new_event.role, role)
108+
self.assertEqual(new_event, event)
109+
110+
def test_invalid_role_rejected(self) -> None:
111+
"""Test that invalid roles are rejected."""
112+
# Test with completely invalid role
113+
with self.assertRaises(ValidationError):
114+
TextMessageStartEvent(
115+
message_id="test-msg",
116+
role="invalid_role", # type: ignore
117+
)
118+
119+
# Test that 'tool' role is not allowed for text messages
120+
with self.assertRaises(ValidationError):
121+
TextMessageStartEvent(
122+
message_id="test-msg",
123+
role="tool", # type: ignore
124+
)
125+
126+
# Test that 'tool' role is not allowed for chunks either
127+
with self.assertRaises(ValidationError):
128+
TextMessageChunkEvent(
129+
message_id="test-msg",
130+
role="tool", # type: ignore
131+
delta="Tool message",
132+
)
133+
134+
def test_text_message_start_default_role(self) -> None:
135+
"""Test that TextMessageStartEvent defaults to 'assistant' role."""
136+
event = TextMessageStartEvent(
137+
message_id="test-msg",
138+
)
139+
140+
self.assertEqual(event.type, EventType.TEXT_MESSAGE_START)
141+
self.assertEqual(event.message_id, "test-msg")
142+
self.assertEqual(event.role, "assistant") # Should default to assistant
143+
144+
145+
if __name__ == "__main__":
146+
unittest.main()

0 commit comments

Comments
 (0)