Skip to content

Commit 2f7bb82

Browse files
Type ModelRequest.parts and ModelResponse.parts as Sequence (#2798)
1 parent 339a6b0 commit 2f7bb82

File tree

4 files changed

+33
-7
lines changed

4 files changed

+33
-7
lines changed

pydantic_ai_slim/pydantic_ai/_a2a.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ def _response_parts_from_a2a(self, parts: list[Part]) -> list[ModelResponsePart]
272272
assert_never(part)
273273
return model_parts
274274

275-
def _response_parts_to_a2a(self, parts: list[ModelResponsePart]) -> list[Part]:
275+
def _response_parts_to_a2a(self, parts: Sequence[ModelResponsePart]) -> list[Part]:
276276
"""Convert pydantic-ai ModelResponsePart objects to A2A Part objects.
277277
278278
This handles the conversion from pydantic-ai's internal response parts to

pydantic_ai_slim/pydantic_ai/_agent_graph.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,16 +301,21 @@ async def _reevaluate_dynamic_prompts(
301301
if self.system_prompt_dynamic_functions:
302302
for msg in messages:
303303
if isinstance(msg, _messages.ModelRequest):
304-
for i, part in enumerate(msg.parts):
304+
reevaluated_message_parts: list[_messages.ModelRequestPart] = []
305+
for part in msg.parts:
305306
if isinstance(part, _messages.SystemPromptPart) and part.dynamic_ref:
306307
# Look up the runner by its ref
307308
if runner := self.system_prompt_dynamic_functions.get( # pragma: lax no cover
308309
part.dynamic_ref
309310
):
310311
updated_part_content = await runner.run(run_context)
311-
msg.parts[i] = _messages.SystemPromptPart(
312-
updated_part_content, dynamic_ref=part.dynamic_ref
313-
)
312+
part = _messages.SystemPromptPart(updated_part_content, dynamic_ref=part.dynamic_ref)
313+
314+
reevaluated_message_parts.append(part)
315+
316+
# Replace message parts with reevaluated ones to prevent mutating parts list
317+
if reevaluated_message_parts != msg.parts:
318+
msg.parts = reevaluated_message_parts
314319

315320
async def _sys_parts(self, run_context: RunContext[DepsT]) -> list[_messages.ModelRequestPart]:
316321
"""Build the initial messages for the conversation."""

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@ def otel_message_parts(self, settings: InstrumentationSettings) -> list[_otel_me
833833
class ModelRequest:
834834
"""A request generated by Pydantic AI and sent to a model, e.g. a message from the Pydantic AI app to the model."""
835835

836-
parts: list[ModelRequestPart]
836+
parts: Sequence[ModelRequestPart]
837837
"""The parts of the user message."""
838838

839839
_: KW_ONLY
@@ -988,7 +988,7 @@ class BuiltinToolCallPart(BaseToolCallPart):
988988
class ModelResponse:
989989
"""A response from a model, e.g. a message from the model to the Pydantic AI app."""
990990

991-
parts: list[ModelResponsePart]
991+
parts: Sequence[ModelResponsePart]
992992
"""The parts of the model message."""
993993

994994
_: KW_ONLY

tests/test_agent.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2993,6 +2993,27 @@ async def func():
29932993
)
29942994

29952995

2996+
def test_dynamic_system_prompt_no_changes():
2997+
"""Test coverage for _reevaluate_dynamic_prompts branch where no parts are changed
2998+
and the messages loop continues after replacement of parts.
2999+
"""
3000+
agent = Agent('test')
3001+
3002+
@agent.system_prompt(dynamic=True)
3003+
async def dynamic_func() -> str:
3004+
return 'Dynamic'
3005+
3006+
result1 = agent.run_sync('Hello')
3007+
3008+
# Create ModelRequest with non-dynamic SystemPromptPart (no dynamic_ref)
3009+
manual_request = ModelRequest(parts=[SystemPromptPart(content='Static'), UserPromptPart(content='Manual')])
3010+
3011+
# Mix dynamic and non-dynamic messages to trigger branch coverage
3012+
result2 = agent.run_sync('Second call', message_history=result1.all_messages() + [manual_request])
3013+
3014+
assert result2.output == 'success (no tool calls)'
3015+
3016+
29963017
def test_capture_run_messages_tool_agent() -> None:
29973018
agent_outer = Agent('test')
29983019
agent_inner = Agent(TestModel(custom_output_text='inner agent result'))

0 commit comments

Comments
 (0)