Repository: pydantic/pydantic-ai
File: tests/test_agent.py (Python)

Code before:
        await agent.run('Trigger filter')


async def test_central_content_filter_with_partial_content():
    """
    Test that the agent graph returns partial content (does not raise exception)
    even if finish_reason='content_filter', provided parts are not empty.
    """

    async def filtered_response(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
        return ModelResponse(
            parts=[TextPart('Partially generated content...')], model_name='test-model', finish_reason='content_filter'
        )

    model = FunctionModel(function=filtered_response, model_name='test-model')
    agent = Agent(model)

    # Should NOT raise ContentFilterError
    result = await agent.run('Trigger filter')
    assert result.output == 'Partially generated content...'


async def test_content_filter_serialization_fallback():
    """
    Test fallback to str() when ModelResponse cannot be serialized to JSON.
    Covers the except block in _agent_graph.py.
    """

    class UnserializableObj:
        def __repr__(self):
            return '<Unserializable>'

    # Create a response that will fail JSON serialization
    response = ModelResponse(
        parts=[],
        model_name='test-model',
        finish_reason='content_filter',
        # Inject unserializable object into provider_details to break dumping
        provider_details={'bad_data': UnserializableObj()},
    )

    async def bad_response_model(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
        return response

    model = FunctionModel(function=bad_response_model, model_name='test-model')
    agent = Agent(model)

    with pytest.raises(ContentFilterError) as exc_info:
        await agent.run('trigger')

    # Verify it fell back to str() representation

Review comment (suggestion):
I don't think we need the except block or this test; `ModelResponse` is guaranteed to be serializable, if it isn't, I'd rather see the error so we fix it ASAP, instead of hiding it.

Code after:
def test_override_with_dynamic_prompts():
    """Test override interacting with dynamic prompts."""
    agent = Agent('test')

    dynamic_value = 'DYNAMIC'

    @agent.system_prompt
    def dynamic_sys() -> str:
        return dynamic_value

    @agent.instructions
    def dynamic_instr() -> str:
        return 'DYNAMIC_INSTR'

    with capture_run_messages() as base_messages:
        agent.run_sync('Hi', model=TestModel(custom_output_text='baseline'))

    base_req = base_messages[0]
    assert isinstance(base_req, ModelRequest)
    assert base_req.instructions == 'DYNAMIC_INSTR'

    # Override should take precedence over dynamic instructions but leave system prompts intact
    with agent.override(instructions='OVERRIDE_INSTR'):
        with capture_run_messages() as messages:
            agent.run_sync('Hi', model=TestModel(custom_output_text='ok'))

    req = messages[0]
    assert isinstance(req, ModelRequest)
    assert req.instructions == 'OVERRIDE_INSTR'
    sys_texts = [p.content for p in req.parts if isinstance(p, SystemPromptPart)]
    # The dynamic system prompt should still be present since overrides target instructions only
    assert dynamic_value in sys_texts


def test_continue_conversation_that_ended_in_output_tool_call(allow_model_requests: None):
    def llm(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
        if any(isinstance(p, ToolReturnPart) and p.tool_name == 'roll_dice' for p in messages[-1].parts):
            return ModelResponse(
                parts=[
                    ToolCallPart(
                        tool_name='final_result',
                        args={'dice_roll': 4},
                        tool_call_id='pyd_ai_tool_call_id__final_result',
                    )
                ]
            )
        return ModelResponse(
            parts=[ToolCallPart(tool_name='roll_dice', args={}, tool_call_id='pyd_ai_tool_call_id__roll_dice')]
        )

    class Result(BaseModel):