diff --git a/pydantic_ai_slim/pydantic_ai/messages.py b/pydantic_ai_slim/pydantic_ai/messages.py index 30b2eef493..057ab5f936 100644 --- a/pydantic_ai_slim/pydantic_ai/messages.py +++ b/pydantic_ai_slim/pydantic_ai/messages.py @@ -1,6 +1,5 @@ from __future__ import annotations as _annotations -import json from dataclasses import dataclass, field from datetime import datetime from typing import Annotated, Any, Literal, Union @@ -74,6 +73,9 @@ def model_response_object(self) -> dict[str, Any]: return {'return_value': tool_return_ta.dump_python(self.content, mode='json')} +ErrorDetailsTa = _pydantic.LazyTypeAdapter(list[pydantic_core.ErrorDetails]) + + @dataclass class RetryPrompt: """A message back to a model asking it to try again. @@ -109,7 +111,8 @@ def model_response(self) -> str: if isinstance(self.content, str): description = self.content else: - description = f'{len(self.content)} validation errors: {json.dumps(self.content, indent=2)}' + json_errors = ErrorDetailsTa.dump_json(self.content, exclude={'__all__': {'ctx'}}, indent=2) + description = f'{len(self.content)} validation errors: {json_errors.decode()}' return f'{description}\n\nFix the errors and try again.' diff --git a/tests/test_agent.py b/tests/test_agent.py index b8c5f143a5..afdb994604 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -5,7 +5,7 @@ import httpx import pytest from inline_snapshot import snapshot -from pydantic import BaseModel +from pydantic import BaseModel, field_validator from pydantic_ai import Agent, ModelRetry, RunContext, UnexpectedModelBehavior, UserError from pydantic_ai.messages import ( @@ -107,6 +107,50 @@ def return_model(messages: list[Message], info: AgentInfo) -> ModelAnyResponse: assert result.all_messages_json().startswith(b'[{"content":"Hello"') +def test_result_pydantic_model_validation_error(set_event_loop: None): + def return_model(messages: list[Message], info: AgentInfo) -> ModelAnyResponse: + assert info.result_tools is not None + if len(messages) == 1: + args_json = '{"a": 1, "b": "foo"}' + else: + args_json = '{"a": 1, "b": "bar"}' + return ModelStructuredResponse(calls=[ToolCall.from_json(info.result_tools[0].name, args_json)]) + + class Bar(BaseModel): + a: int + b: str + + @field_validator('b') + def check_b(cls, v: str) -> str: + if v == 'foo': + raise ValueError('must not be foo') + return v + + agent = Agent(FunctionModel(return_model), result_type=Bar) + + result = agent.run_sync('Hello') + assert isinstance(result.data, Bar) + assert result.data.model_dump() == snapshot({'a': 1, 'b': 'bar'}) + message_roles = [m.role for m in result.all_messages()] + assert message_roles == snapshot(['user', 'model-structured-response', 'retry-prompt', 'model-structured-response']) + + retry_prompt = result.all_messages()[2] + assert isinstance(retry_prompt, RetryPrompt) + assert retry_prompt.model_response() == snapshot("""\ +1 validation errors: [ + { + "type": "value_error", + "loc": [ + "b" + ], + "msg": "Value error, must not be foo", + "input": "foo" + } +] + +Fix the errors and try again.""") + + def test_result_validator(set_event_loop: None): def return_model(messages: list[Message], info: AgentInfo) -> ModelAnyResponse: assert info.result_tools is not None