Skip to content

Commit e5c11d3

Browse files
Python: cleanup and refactoring of chat clients (#2937)
* refactoring and unifying naming schemes of internal methods of chat clients * set tool_choice to auto * fix for mypy * added note on naming and fix #2951 * fix responses * fixes in azure ai agents client
1 parent a71f768 commit e5c11d3

File tree

26 files changed

+1128
-1068
lines changed

26 files changed

+1128
-1068
lines changed

python/DEV_SETUP.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ Example:
154154
chat_completion = OpenAIChatClient(env_file_path="openai.env")
155155
```
156156

157+
# Method naming inside connectors
158+
159+
When naming methods inside connectors, we have a loose preference for using the following conventions:
160+
- Use `_prepare_<object>_for_<purpose>` as a prefix for methods that prepare data for sending to the external service.
161+
- Use `_parse_<object>_from_<source>` as a prefix for methods that process data received from the external service.
162+
163+
This is not a strict rule, but a guideline to help maintain consistency across the codebase.
164+
157165
## Tests
158166

159167
All the tests are located in the `tests` folder of each package. There are tests that are marked with a `@skip_if_..._integration_tests_disabled` decorator, these are integration tests that require an external service to be running, like OpenAI or Azure OpenAI.

python/packages/a2a/agent_framework_a2a/_agent.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -237,14 +237,14 @@ async def run_stream(
237237
An agent response item.
238238
"""
239239
messages = self._normalize_messages(messages)
240-
a2a_message = self._chat_message_to_a2a_message(messages[-1])
240+
a2a_message = self._prepare_message_for_a2a(messages[-1])
241241

242242
response_stream = self.client.send_message(a2a_message)
243243

244244
async for item in response_stream:
245245
if isinstance(item, Message):
246246
# Process A2A Message
247-
contents = self._a2a_parts_to_contents(item.parts)
247+
contents = self._parse_contents_from_a2a(item.parts)
248248
yield AgentRunResponseUpdate(
249249
contents=contents,
250250
role=Role.ASSISTANT if item.role == A2ARole.agent else Role.USER,
@@ -255,7 +255,7 @@ async def run_stream(
255255
task, _update_event = item
256256
if isinstance(task, Task) and task.status.state in TERMINAL_TASK_STATES:
257257
# Convert Task artifacts to ChatMessages and yield as separate updates
258-
task_messages = self._task_to_chat_messages(task)
258+
task_messages = self._parse_messages_from_task(task)
259259
if task_messages:
260260
for message in task_messages:
261261
# Use the artifact's ID from raw_representation as message_id for unique identification
@@ -280,8 +280,8 @@ async def run_stream(
280280
msg = f"Only Message and Task responses are supported from A2A agents. Received: {type(item)}"
281281
raise NotImplementedError(msg)
282282

283-
def _chat_message_to_a2a_message(self, message: ChatMessage) -> A2AMessage:
284-
"""Convert a ChatMessage to an A2A Message.
283+
def _prepare_message_for_a2a(self, message: ChatMessage) -> A2AMessage:
284+
"""Prepare a ChatMessage for the A2A protocol.
285285
286286
Transforms Agent Framework ChatMessage objects into A2A protocol Messages by:
287287
- Converting all message contents to appropriate A2A Part types
@@ -361,8 +361,8 @@ def _chat_message_to_a2a_message(self, message: ChatMessage) -> A2AMessage:
361361
metadata=cast(dict[str, Any], message.additional_properties),
362362
)
363363

364-
def _a2a_parts_to_contents(self, parts: Sequence[A2APart]) -> list[Contents]:
365-
"""Convert A2A Parts to Agent Framework Contents.
364+
def _parse_contents_from_a2a(self, parts: Sequence[A2APart]) -> list[Contents]:
365+
"""Parse A2A Parts into Agent Framework Contents.
366366
367367
Transforms A2A protocol Parts into framework-native Content objects,
368368
handling text, file (URI/bytes), and data parts with metadata preservation.
@@ -410,17 +410,17 @@ def _a2a_parts_to_contents(self, parts: Sequence[A2APart]) -> list[Contents]:
410410
raise ValueError(f"Unknown Part kind: {inner_part.kind}")
411411
return contents
412412

413-
def _task_to_chat_messages(self, task: Task) -> list[ChatMessage]:
414-
"""Convert A2A Task artifacts to ChatMessages with ASSISTANT role."""
413+
def _parse_messages_from_task(self, task: Task) -> list[ChatMessage]:
414+
"""Parse A2A Task artifacts into ChatMessages with ASSISTANT role."""
415415
messages: list[ChatMessage] = []
416416

417417
if task.artifacts is not None:
418418
for artifact in task.artifacts:
419-
messages.append(self._artifact_to_chat_message(artifact))
419+
messages.append(self._parse_message_from_artifact(artifact))
420420
elif task.history is not None and len(task.history) > 0:
421421
# Include the last history item as the agent response
422422
history_item = task.history[-1]
423-
contents = self._a2a_parts_to_contents(history_item.parts)
423+
contents = self._parse_contents_from_a2a(history_item.parts)
424424
messages.append(
425425
ChatMessage(
426426
role=Role.ASSISTANT if history_item.role == A2ARole.agent else Role.USER,
@@ -431,9 +431,9 @@ def _task_to_chat_messages(self, task: Task) -> list[ChatMessage]:
431431

432432
return messages
433433

434-
def _artifact_to_chat_message(self, artifact: Artifact) -> ChatMessage:
435-
"""Convert A2A Artifact to ChatMessage using part contents."""
436-
contents = self._a2a_parts_to_contents(artifact.parts)
434+
def _parse_message_from_artifact(self, artifact: Artifact) -> ChatMessage:
435+
"""Parse A2A Artifact into ChatMessage using part contents."""
436+
contents = self._parse_contents_from_a2a(artifact.parts)
437437
return ChatMessage(
438438
role=Role.ASSISTANT,
439439
contents=contents,

python/packages/a2a/tests/test_a2a_agent.py

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,18 @@ async def test_run_with_unknown_response_type_raises_error(a2a_agent: A2AAgent,
197197
await a2a_agent.run("Test message")
198198

199199

200-
def test_task_to_chat_messages_empty_artifacts(a2a_agent: A2AAgent) -> None:
201-
"""Test _task_to_chat_messages with task containing no artifacts."""
200+
def test_parse_messages_from_task_empty_artifacts(a2a_agent: A2AAgent) -> None:
201+
"""Test _parse_messages_from_task with task containing no artifacts."""
202202
task = MagicMock()
203203
task.artifacts = None
204204

205-
result = a2a_agent._task_to_chat_messages(task)
205+
result = a2a_agent._parse_messages_from_task(task)
206206

207207
assert len(result) == 0
208208

209209

210-
def test_task_to_chat_messages_with_artifacts(a2a_agent: A2AAgent) -> None:
211-
"""Test _task_to_chat_messages with task containing artifacts."""
210+
def test_parse_messages_from_task_with_artifacts(a2a_agent: A2AAgent) -> None:
211+
"""Test _parse_messages_from_task with task containing artifacts."""
212212
task = MagicMock()
213213

214214
# Create mock artifacts
@@ -232,16 +232,16 @@ def test_task_to_chat_messages_with_artifacts(a2a_agent: A2AAgent) -> None:
232232

233233
task.artifacts = [artifact1, artifact2]
234234

235-
result = a2a_agent._task_to_chat_messages(task)
235+
result = a2a_agent._parse_messages_from_task(task)
236236

237237
assert len(result) == 2
238238
assert result[0].text == "Content 1"
239239
assert result[1].text == "Content 2"
240240
assert all(msg.role == Role.ASSISTANT for msg in result)
241241

242242

243-
def test_artifact_to_chat_message(a2a_agent: A2AAgent) -> None:
244-
"""Test _artifact_to_chat_message conversion."""
243+
def test_parse_message_from_artifact(a2a_agent: A2AAgent) -> None:
244+
"""Test _parse_message_from_artifact conversion."""
245245
artifact = MagicMock()
246246
artifact.artifact_id = "test-artifact"
247247

@@ -253,7 +253,7 @@ def test_artifact_to_chat_message(a2a_agent: A2AAgent) -> None:
253253

254254
artifact.parts = [text_part]
255255

256-
result = a2a_agent._artifact_to_chat_message(artifact)
256+
result = a2a_agent._parse_message_from_artifact(artifact)
257257

258258
assert isinstance(result, ChatMessage)
259259
assert result.role == Role.ASSISTANT
@@ -276,7 +276,7 @@ def test_get_uri_data_invalid_uri() -> None:
276276
_get_uri_data("not-a-valid-data-uri")
277277

278278

279-
def test_a2a_parts_to_contents_conversion(a2a_agent: A2AAgent) -> None:
279+
def test_parse_contents_from_a2a_conversion(a2a_agent: A2AAgent) -> None:
280280
"""Test A2A parts to contents conversion."""
281281

282282
agent = A2AAgent(name="Test Agent", client=MockA2AClient(), _http_client=None)
@@ -285,7 +285,7 @@ def test_a2a_parts_to_contents_conversion(a2a_agent: A2AAgent) -> None:
285285
parts = [Part(root=TextPart(text="First part")), Part(root=TextPart(text="Second part"))]
286286

287287
# Convert to contents
288-
contents = agent._a2a_parts_to_contents(parts)
288+
contents = agent._parse_contents_from_a2a(parts)
289289

290290
# Verify conversion
291291
assert len(contents) == 2
@@ -295,61 +295,61 @@ def test_a2a_parts_to_contents_conversion(a2a_agent: A2AAgent) -> None:
295295
assert contents[1].text == "Second part"
296296

297297

298-
def test_chat_message_to_a2a_message_with_error_content(a2a_agent: A2AAgent) -> None:
299-
"""Test _chat_message_to_a2a_message with ErrorContent."""
298+
def test_prepare_message_for_a2a_with_error_content(a2a_agent: A2AAgent) -> None:
299+
"""Test _prepare_message_for_a2a with ErrorContent."""
300300

301301
# Create ChatMessage with ErrorContent
302302
error_content = ErrorContent(message="Test error message")
303303
message = ChatMessage(role=Role.USER, contents=[error_content])
304304

305305
# Convert to A2A message
306-
a2a_message = a2a_agent._chat_message_to_a2a_message(message)
306+
a2a_message = a2a_agent._prepare_message_for_a2a(message)
307307

308308
# Verify conversion
309309
assert len(a2a_message.parts) == 1
310310
assert a2a_message.parts[0].root.text == "Test error message"
311311

312312

313-
def test_chat_message_to_a2a_message_with_uri_content(a2a_agent: A2AAgent) -> None:
314-
"""Test _chat_message_to_a2a_message with UriContent."""
313+
def test_prepare_message_for_a2a_with_uri_content(a2a_agent: A2AAgent) -> None:
314+
"""Test _prepare_message_for_a2a with UriContent."""
315315

316316
# Create ChatMessage with UriContent
317317
uri_content = UriContent(uri="http://example.com/file.pdf", media_type="application/pdf")
318318
message = ChatMessage(role=Role.USER, contents=[uri_content])
319319

320320
# Convert to A2A message
321-
a2a_message = a2a_agent._chat_message_to_a2a_message(message)
321+
a2a_message = a2a_agent._prepare_message_for_a2a(message)
322322

323323
# Verify conversion
324324
assert len(a2a_message.parts) == 1
325325
assert a2a_message.parts[0].root.file.uri == "http://example.com/file.pdf"
326326
assert a2a_message.parts[0].root.file.mime_type == "application/pdf"
327327

328328

329-
def test_chat_message_to_a2a_message_with_data_content(a2a_agent: A2AAgent) -> None:
330-
"""Test _chat_message_to_a2a_message with DataContent."""
329+
def test_prepare_message_for_a2a_with_data_content(a2a_agent: A2AAgent) -> None:
330+
"""Test _prepare_message_for_a2a with DataContent."""
331331

332332
# Create ChatMessage with DataContent (base64 data URI)
333333
data_content = DataContent(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain")
334334
message = ChatMessage(role=Role.USER, contents=[data_content])
335335

336336
# Convert to A2A message
337-
a2a_message = a2a_agent._chat_message_to_a2a_message(message)
337+
a2a_message = a2a_agent._prepare_message_for_a2a(message)
338338

339339
# Verify conversion
340340
assert len(a2a_message.parts) == 1
341341
assert a2a_message.parts[0].root.file.bytes == "SGVsbG8gV29ybGQ="
342342
assert a2a_message.parts[0].root.file.mime_type == "text/plain"
343343

344344

345-
def test_chat_message_to_a2a_message_empty_contents_raises_error(a2a_agent: A2AAgent) -> None:
346-
"""Test _chat_message_to_a2a_message with empty contents raises ValueError."""
345+
def test_prepare_message_for_a2a_empty_contents_raises_error(a2a_agent: A2AAgent) -> None:
346+
"""Test _prepare_message_for_a2a with empty contents raises ValueError."""
347347
# Create ChatMessage with no contents
348348
message = ChatMessage(role=Role.USER, contents=[])
349349

350350
# Should raise ValueError for empty contents
351351
with raises(ValueError, match="ChatMessage.contents is empty"):
352-
a2a_agent._chat_message_to_a2a_message(message)
352+
a2a_agent._prepare_message_for_a2a(message)
353353

354354

355355
async def test_run_stream_with_message_response(a2a_agent: A2AAgent, mock_a2a_client: MockA2AClient) -> None:
@@ -405,7 +405,7 @@ async def test_context_manager_no_cleanup_when_no_http_client() -> None:
405405
pass
406406

407407

408-
def test_chat_message_to_a2a_message_with_multiple_contents() -> None:
408+
def test_prepare_message_for_a2a_with_multiple_contents() -> None:
409409
"""Test conversion of ChatMessage with multiple contents."""
410410

411411
agent = A2AAgent(client=MagicMock(), _http_client=None)
@@ -421,7 +421,7 @@ def test_chat_message_to_a2a_message_with_multiple_contents() -> None:
421421
],
422422
)
423423

424-
result = agent._chat_message_to_a2a_message(message)
424+
result = agent._prepare_message_for_a2a(message)
425425

426426
# Should have converted all 4 contents to parts
427427
assert len(result.parts) == 4
@@ -433,15 +433,15 @@ def test_chat_message_to_a2a_message_with_multiple_contents() -> None:
433433
assert result.parts[3].root.kind == "text" # JSON text remains as text (no parsing)
434434

435435

436-
def test_a2a_parts_to_contents_with_data_part() -> None:
436+
def test_parse_contents_from_a2a_with_data_part() -> None:
437437
"""Test conversion of A2A DataPart."""
438438

439439
agent = A2AAgent(client=MagicMock(), _http_client=None)
440440

441441
# Create DataPart
442442
data_part = Part(root=DataPart(data={"key": "value", "number": 42}, metadata={"source": "test"}))
443443

444-
contents = agent._a2a_parts_to_contents([data_part])
444+
contents = agent._parse_contents_from_a2a([data_part])
445445

446446
assert len(contents) == 1
447447

@@ -450,7 +450,7 @@ def test_a2a_parts_to_contents_with_data_part() -> None:
450450
assert contents[0].additional_properties == {"source": "test"}
451451

452452

453-
def test_a2a_parts_to_contents_unknown_part_kind() -> None:
453+
def test_parse_contents_from_a2a_unknown_part_kind() -> None:
454454
"""Test error handling for unknown A2A part kind."""
455455
agent = A2AAgent(client=MagicMock(), _http_client=None)
456456

@@ -459,10 +459,10 @@ def test_a2a_parts_to_contents_unknown_part_kind() -> None:
459459
mock_part.root.kind = "unknown_kind"
460460

461461
with raises(ValueError, match="Unknown Part kind: unknown_kind"):
462-
agent._a2a_parts_to_contents([mock_part])
462+
agent._parse_contents_from_a2a([mock_part])
463463

464464

465-
def test_chat_message_to_a2a_message_with_hosted_file() -> None:
465+
def test_prepare_message_for_a2a_with_hosted_file() -> None:
466466
"""Test conversion of ChatMessage with HostedFileContent to A2A message."""
467467

468468
agent = A2AAgent(client=MagicMock(), _http_client=None)
@@ -473,7 +473,7 @@ def test_chat_message_to_a2a_message_with_hosted_file() -> None:
473473
contents=[HostedFileContent(file_id="hosted://storage/document.pdf")],
474474
)
475475

476-
result = agent._chat_message_to_a2a_message(message) # noqa: SLF001
476+
result = agent._prepare_message_for_a2a(message) # noqa: SLF001
477477

478478
# Verify the conversion
479479
assert len(result.parts) == 1
@@ -488,7 +488,7 @@ def test_chat_message_to_a2a_message_with_hosted_file() -> None:
488488
assert part.root.file.mime_type is None # HostedFileContent doesn't specify media_type
489489

490490

491-
def test_a2a_parts_to_contents_with_hosted_file_uri() -> None:
491+
def test_parse_contents_from_a2a_with_hosted_file_uri() -> None:
492492
"""Test conversion of A2A FilePart with hosted file URI back to UriContent."""
493493

494494
agent = A2AAgent(client=MagicMock(), _http_client=None)
@@ -503,7 +503,7 @@ def test_a2a_parts_to_contents_with_hosted_file_uri() -> None:
503503
)
504504
)
505505

506-
contents = agent._a2a_parts_to_contents([file_part]) # noqa: SLF001
506+
contents = agent._parse_contents_from_a2a([file_part]) # noqa: SLF001
507507

508508
assert len(contents) == 1
509509

0 commit comments

Comments
 (0)