Skip to content

Commit 838a7fd

Browse files
Python: [BREAKING] Types API Review improvements (#3647)
* Replace Role and FinishReason classes with NewType + Literal - Remove EnumLike metaclass from _types.py - Replace Role class with NewType('Role', str) + RoleLiteral - Replace FinishReason class with NewType('FinishReason', str) + FinishReasonLiteral - Update all usages across codebase to use string literals - Remove .value access patterns (direct string comparison now works) - Add backward compatibility for legacy dict serialization format - Update tests to reflect new string-based types Addresses #3591, #3615 * Simplify ChatResponse and AgentResponse type hints (#3592) - Remove overloads from ChatResponse.__init__ - Remove text parameter from ChatResponse.__init__ - Remove | dict[str, Any] from finish_reason and usage_details params - Remove **kwargs from AgentResponse.__init__ - Both now accept ChatMessage | Sequence[ChatMessage] | None for messages - Update docstrings and examples to reflect changes - Fix tests that were using removed kwargs - Fix Role type hint usage in ag-ui utils * Remove text parameter from ChatResponseUpdate and AgentResponseUpdate (#3597) - Remove text parameter from ChatResponseUpdate.__init__ - Remove text parameter from AgentResponseUpdate.__init__ - Remove **kwargs from both update classes - Simplify contents parameter type to Sequence[Content] | None - Update all usages to use contents=[Content.from_text(...)] pattern - Fix imports in test files - Update docstrings and examples * Rename from_chat_response_updates to from_updates (#3593) - ChatResponse.from_chat_response_updates → ChatResponse.from_updates - ChatResponse.from_chat_response_generator → ChatResponse.from_update_generator - AgentResponse.from_agent_run_response_updates → AgentResponse.from_updates * Remove try_parse_value method from ChatResponse and AgentResponse (#3595) - Remove try_parse_value method from ChatResponse - Remove try_parse_value method from AgentResponse - Remove try_parse_value calls from from_updates and from_update_generator methods - Update samples to use try/except with response.value instead - Update tests to use response.value pattern - Users should now use response.value with try/except for safe parsing * Add agent_id to AgentResponse and clarify author_name documentation (#3596) - Add agent_id parameter to AgentResponse class - Document that author_name is on ChatMessage objects, not responses - Update ChatResponse docstring with author_name note - Update AgentResponse docstring with author_name note * Simplify ChatMessage.__init__ signature (#3618) - Make contents a positional argument accepting Sequence[Content | str] - Auto-convert strings in contents to TextContent - Remove overloads, keep text kwarg for backward compatibility with serialization - Update _parse_content_list to handle string items - Update all usages across codebase to use new format: ChatMessage("role", ["text"]) * Allow Content as input on run and get_response - Update prepare_messages and normalize_messages to accept Content - Update type signatures in _agents.py and _clients.py - Add tests for Content input handling * Fix ChatMessage usage across packages and samples Update all remaining ChatMessage(role=..., text=...) to use new ChatMessage('role', ['text']) signature. * Fix Role string usage and response format parsing - Fix redis provider: remove .value access on string literals - Fix durabletask ensure_response_format: set _response_format before accessing .value * Fix ollama .value and ai_model_id issues, handle None in content list - Fix ollama _chat_client: remove .value on string literals - Fix ollama _chat_client: rename ai_model_id to model_id - Fix _parse_content_list: skip None values gracefully * Fix A2AAgent type signature to include Content * Fix Role/FinishReason NewType dict annotations and improve test coverage to 95% * Fix mypy errors for Role/FinishReason NewType usage * Fix Role.TOOL and Role.ASSISTANT usage in _orchestrator_helpers.py * Fix Role NewType usage in durabletask _models.py
1 parent ef79862 commit 838a7fd

File tree

341 files changed

+3767
-3229
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

341 files changed

+3767
-3229
lines changed

python/CODING_STANDARD.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ Prefer attributes over inheritance when parameters are mostly the same:
5555
# ✅ Preferred - using attributes
5656
from agent_framework import ChatMessage
5757

58-
user_msg = ChatMessage(role="user", content="Hello, world!")
59-
asst_msg = ChatMessage(role="assistant", content="Hello, world!")
58+
user_msg = ChatMessage("user", ["Hello, world!"])
59+
asst_msg = ChatMessage("assistant", ["Hello, world!"])
6060

6161
# ❌ Not preferred - unnecessary inheritance
6262
from agent_framework import UserMessage, AssistantMessage

python/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ async def main():
113113
client = OpenAIChatClient()
114114

115115
messages = [
116-
ChatMessage(role="system", text="You are a helpful assistant."),
117-
ChatMessage(role="user", text="Write a haiku about Agent Framework.")
116+
ChatMessage("system", ["You are a helpful assistant."]),
117+
ChatMessage("user", ["Write a haiku about Agent Framework."])
118118
]
119119

120120
response = await client.get_response(messages)

python/packages/a2a/agent_framework_a2a/_agent.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
BaseAgent,
3333
ChatMessage,
3434
Content,
35-
Role,
3635
normalize_messages,
3736
prepend_agent_framework_to_user_agent,
3837
)
@@ -187,7 +186,7 @@ async def __aexit__(
187186

188187
async def run(
189188
self,
190-
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
189+
messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None,
191190
*,
192191
thread: AgentThread | None = None,
193192
**kwargs: Any,
@@ -210,11 +209,11 @@ async def run(
210209
"""
211210
# Collect all updates and use framework to consolidate updates into response
212211
updates = [update async for update in self.run_stream(messages, thread=thread, **kwargs)]
213-
return AgentResponse.from_agent_run_response_updates(updates)
212+
return AgentResponse.from_updates(updates)
214213

215214
async def run_stream(
216215
self,
217-
messages: str | ChatMessage | Sequence[str | ChatMessage] | None = None,
216+
messages: str | Content | ChatMessage | Sequence[str | Content | ChatMessage] | None = None,
218217
*,
219218
thread: AgentThread | None = None,
220219
**kwargs: Any,
@@ -245,7 +244,7 @@ async def run_stream(
245244
contents = self._parse_contents_from_a2a(item.parts)
246245
yield AgentResponseUpdate(
247246
contents=contents,
248-
role=Role.ASSISTANT if item.role == A2ARole.agent else Role.USER,
247+
role="assistant" if item.role == A2ARole.agent else "user",
249248
response_id=str(getattr(item, "message_id", uuid.uuid4())),
250249
raw_representation=item,
251250
)
@@ -269,7 +268,7 @@ async def run_stream(
269268
# Empty task
270269
yield AgentResponseUpdate(
271270
contents=[],
272-
role=Role.ASSISTANT,
271+
role="assistant",
273272
response_id=task.id,
274273
raw_representation=task,
275274
)
@@ -421,7 +420,7 @@ def _parse_messages_from_task(self, task: Task) -> list[ChatMessage]:
421420
contents = self._parse_contents_from_a2a(history_item.parts)
422421
messages.append(
423422
ChatMessage(
424-
role=Role.ASSISTANT if history_item.role == A2ARole.agent else Role.USER,
423+
role="assistant" if history_item.role == A2ARole.agent else "user",
425424
contents=contents,
426425
raw_representation=history_item,
427426
)
@@ -433,7 +432,7 @@ def _parse_message_from_artifact(self, artifact: Artifact) -> ChatMessage:
433432
"""Parse A2A Artifact into ChatMessage using part contents."""
434433
contents = self._parse_contents_from_a2a(artifact.parts)
435434
return ChatMessage(
436-
role=Role.ASSISTANT,
435+
role="assistant",
437436
contents=contents,
438437
raw_representation=artifact,
439438
)

python/packages/a2a/tests/test_a2a_agent.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
AgentResponseUpdate,
2626
ChatMessage,
2727
Content,
28-
Role,
2928
)
3029
from agent_framework.a2a import A2AAgent
3130
from pytest import fixture, raises
@@ -129,7 +128,7 @@ async def test_run_with_message_response(a2a_agent: A2AAgent, mock_a2a_client: M
129128

130129
assert isinstance(response, AgentResponse)
131130
assert len(response.messages) == 1
132-
assert response.messages[0].role == Role.ASSISTANT
131+
assert response.messages[0].role == "assistant"
133132
assert response.messages[0].text == "Hello from agent!"
134133
assert response.response_id == "msg-123"
135134
assert mock_a2a_client.call_count == 1
@@ -144,7 +143,7 @@ async def test_run_with_task_response_single_artifact(a2a_agent: A2AAgent, mock_
144143

145144
assert isinstance(response, AgentResponse)
146145
assert len(response.messages) == 1
147-
assert response.messages[0].role == Role.ASSISTANT
146+
assert response.messages[0].role == "assistant"
148147
assert response.messages[0].text == "Generated report content"
149148
assert response.response_id == "task-456"
150149
assert mock_a2a_client.call_count == 1
@@ -170,7 +169,7 @@ async def test_run_with_task_response_multiple_artifacts(a2a_agent: A2AAgent, mo
170169

171170
# All should be assistant messages
172171
for message in response.messages:
173-
assert message.role == Role.ASSISTANT
172+
assert message.role == "assistant"
174173

175174
assert response.response_id == "task-789"
176175

@@ -233,7 +232,7 @@ def test_parse_messages_from_task_with_artifacts(a2a_agent: A2AAgent) -> None:
233232
assert len(result) == 2
234233
assert result[0].text == "Content 1"
235234
assert result[1].text == "Content 2"
236-
assert all(msg.role == Role.ASSISTANT for msg in result)
235+
assert all(msg.role == "assistant" for msg in result)
237236

238237

239238
def test_parse_message_from_artifact(a2a_agent: A2AAgent) -> None:
@@ -252,7 +251,7 @@ def test_parse_message_from_artifact(a2a_agent: A2AAgent) -> None:
252251
result = a2a_agent._parse_message_from_artifact(artifact)
253252

254253
assert isinstance(result, ChatMessage)
255-
assert result.role == Role.ASSISTANT
254+
assert result.role == "assistant"
256255
assert result.text == "Artifact content"
257256
assert result.raw_representation == artifact
258257

@@ -296,7 +295,7 @@ def test_prepare_message_for_a2a_with_error_content(a2a_agent: A2AAgent) -> None
296295

297296
# Create ChatMessage with ErrorContent
298297
error_content = Content.from_error(message="Test error message")
299-
message = ChatMessage(role=Role.USER, contents=[error_content])
298+
message = ChatMessage("user", [error_content])
300299

301300
# Convert to A2A message
302301
a2a_message = a2a_agent._prepare_message_for_a2a(message)
@@ -311,7 +310,7 @@ def test_prepare_message_for_a2a_with_uri_content(a2a_agent: A2AAgent) -> None:
311310

312311
# Create ChatMessage with UriContent
313312
uri_content = Content.from_uri(uri="http://example.com/file.pdf", media_type="application/pdf")
314-
message = ChatMessage(role=Role.USER, contents=[uri_content])
313+
message = ChatMessage("user", [uri_content])
315314

316315
# Convert to A2A message
317316
a2a_message = a2a_agent._prepare_message_for_a2a(message)
@@ -327,7 +326,7 @@ def test_prepare_message_for_a2a_with_data_content(a2a_agent: A2AAgent) -> None:
327326

328327
# Create ChatMessage with DataContent (base64 data URI)
329328
data_content = Content.from_uri(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain")
330-
message = ChatMessage(role=Role.USER, contents=[data_content])
329+
message = ChatMessage("user", [data_content])
331330

332331
# Convert to A2A message
333332
a2a_message = a2a_agent._prepare_message_for_a2a(message)
@@ -341,7 +340,7 @@ def test_prepare_message_for_a2a_with_data_content(a2a_agent: A2AAgent) -> None:
341340
def test_prepare_message_for_a2a_empty_contents_raises_error(a2a_agent: A2AAgent) -> None:
342341
"""Test _prepare_message_for_a2a with empty contents raises ValueError."""
343342
# Create ChatMessage with no contents
344-
message = ChatMessage(role=Role.USER, contents=[])
343+
message = ChatMessage("user", [])
345344

346345
# Should raise ValueError for empty contents
347346
with raises(ValueError, match="ChatMessage.contents is empty"):
@@ -360,7 +359,7 @@ async def test_run_stream_with_message_response(a2a_agent: A2AAgent, mock_a2a_cl
360359
# Verify streaming response
361360
assert len(updates) == 1
362361
assert isinstance(updates[0], AgentResponseUpdate)
363-
assert updates[0].role == Role.ASSISTANT
362+
assert updates[0].role == "assistant"
364363
assert len(updates[0].contents) == 1
365364

366365
content = updates[0].contents[0]
@@ -408,7 +407,7 @@ def test_prepare_message_for_a2a_with_multiple_contents() -> None:
408407

409408
# Create message with multiple content types
410409
message = ChatMessage(
411-
role=Role.USER,
410+
role="user",
412411
contents=[
413412
Content.from_text(text="Here's the analysis:"),
414413
Content.from_data(data=b"binary data", media_type="application/octet-stream"),
@@ -465,7 +464,7 @@ def test_prepare_message_for_a2a_with_hosted_file() -> None:
465464

466465
# Create message with hosted file content
467466
message = ChatMessage(
468-
role=Role.USER,
467+
role="user",
469468
contents=[Content.from_hosted_file(file_id="hosted://storage/document.pdf")],
470469
)
471470

python/packages/ag-ui/agent_framework_ag_ui/_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ async def _inner_get_response(
334334
Returns:
335335
ChatResponse object
336336
"""
337-
return await ChatResponse.from_chat_response_generator(
337+
return await ChatResponse.from_update_generator(
338338
self._inner_get_streaming_response(
339339
messages=messages,
340340
options=options,

python/packages/ag-ui/agent_framework_ag_ui/_event_converters.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
from agent_framework import (
88
ChatResponseUpdate,
99
Content,
10-
FinishReason,
11-
Role,
1210
)
1311

1412

@@ -86,7 +84,7 @@ def _handle_run_started(self, event: dict[str, Any]) -> ChatResponseUpdate:
8684
self.run_id = event.get("runId")
8785

8886
return ChatResponseUpdate(
89-
role=Role.ASSISTANT,
87+
role="assistant",
9088
contents=[],
9189
additional_properties={
9290
"thread_id": self.thread_id,
@@ -98,7 +96,7 @@ def _handle_text_message_start(self, event: dict[str, Any]) -> ChatResponseUpdat
9896
"""Handle TEXT_MESSAGE_START event."""
9997
self.current_message_id = event.get("messageId")
10098
return ChatResponseUpdate(
101-
role=Role.ASSISTANT,
99+
role="assistant",
102100
message_id=self.current_message_id,
103101
contents=[],
104102
)
@@ -112,7 +110,7 @@ def _handle_text_message_content(self, event: dict[str, Any]) -> ChatResponseUpd
112110
self.current_message_id = message_id
113111

114112
return ChatResponseUpdate(
115-
role=Role.ASSISTANT,
113+
role="assistant",
116114
message_id=self.current_message_id,
117115
contents=[Content.from_text(text=delta)],
118116
)
@@ -128,7 +126,7 @@ def _handle_tool_call_start(self, event: dict[str, Any]) -> ChatResponseUpdate:
128126
self.accumulated_tool_args = ""
129127

130128
return ChatResponseUpdate(
131-
role=Role.ASSISTANT,
129+
role="assistant",
132130
contents=[
133131
Content.from_function_call(
134132
call_id=self.current_tool_call_id or "",
@@ -144,7 +142,7 @@ def _handle_tool_call_args(self, event: dict[str, Any]) -> ChatResponseUpdate:
144142
self.accumulated_tool_args += delta
145143

146144
return ChatResponseUpdate(
147-
role=Role.ASSISTANT,
145+
role="assistant",
148146
contents=[
149147
Content.from_function_call(
150148
call_id=self.current_tool_call_id or "",
@@ -165,7 +163,7 @@ def _handle_tool_call_result(self, event: dict[str, Any]) -> ChatResponseUpdate:
165163
result = event.get("result") if event.get("result") is not None else event.get("content")
166164

167165
return ChatResponseUpdate(
168-
role=Role.TOOL,
166+
role="tool",
169167
contents=[
170168
Content.from_function_result(
171169
call_id=tool_call_id,
@@ -177,8 +175,8 @@ def _handle_tool_call_result(self, event: dict[str, Any]) -> ChatResponseUpdate:
177175
def _handle_run_finished(self, event: dict[str, Any]) -> ChatResponseUpdate:
178176
"""Handle RUN_FINISHED event."""
179177
return ChatResponseUpdate(
180-
role=Role.ASSISTANT,
181-
finish_reason=FinishReason.STOP,
178+
role="assistant",
179+
finish_reason="stop",
182180
contents=[],
183181
additional_properties={
184182
"thread_id": self.thread_id,
@@ -191,8 +189,8 @@ def _handle_run_error(self, event: dict[str, Any]) -> ChatResponseUpdate:
191189
error_message = event.get("message", "Unknown error")
192190

193191
return ChatResponseUpdate(
194-
role=Role.ASSISTANT,
195-
finish_reason=FinishReason.CONTENT_FILTER,
192+
role="assistant",
193+
finish_reason="content_filter",
196194
contents=[
197195
Content.from_error(
198196
message=error_message,

0 commit comments

Comments
 (0)