Skip to content

Support structured and manual JSON output_type modes in addition to tool calls #1628

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

DouweM
Copy link
Contributor

@DouweM DouweM commented May 2, 2025

Right now, this only implements response_format=json_schema structured output for OpenAI chat completions in non-streaming mode.

This test illustrates the behavior, including the fact that OpenAI supports tool calls alongside structured output (as noted in #582 (comment)):

async def test_openai_structured_output(allow_model_requests: None, openai_api_key: str):
    m = OpenAIModel('gpt-4o', provider=OpenAIProvider(api_key=openai_api_key))

    class CityLocation(BaseModel):
        city: str
        country: str

    agent = Agent(m, output_type=StructuredOutput(type_=CityLocation))

    @agent.tool_plain
    async def get_user_country() -> str:
        return 'Mexico'

    result = await agent.run('What is the largest city in the user country?')
    assert result.output == snapshot(CityLocation(city='Mexico City', country='Mexico'))

    assert result.all_messages() == snapshot(
        [
            ModelRequest(
                parts=[
                    UserPromptPart(
                        content='What is the largest city in the user country?',
                        timestamp=IsDatetime(),
                    )
                ]
            ),
            ModelResponse(
                parts=[ToolCallPart(tool_name='get_user_country', args='{}', tool_call_id=IsStr())],
                model_name='gpt-4o-2024-08-06',
                timestamp=IsDatetime(),
            ),
            ModelRequest(
                parts=[
                    ToolReturnPart(
                        tool_name='get_user_country',
                        content='Mexico',
                        tool_call_id=IsStr(),
                        timestamp=IsDatetime(),
                    )
                ]
            ),
            ModelResponse(
                parts=[StructuredOutputPart(content='{"city":"Mexico City","country":"Mexico"}')],
                model_name='gpt-4o-2024-08-06',
                timestamp=IsDatetime(),
            ),
        ]
    )

Copy link

github-actions bot commented May 2, 2025

Docs Preview

commit: 7974df0
Preview URL: https://30f14b49-pydantic-ai-previews.pydantic.workers.dev

Comment on lines 428 to 431
elif structured_outputs:
# No events are emitted during the handling of structured outputs, so we don't need to yield anything
self._next_node = await self._handle_structured_outputs(ctx, structured_outputs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this code is assuming that you don't have both tool calls and structured outputs, but I would think that this is possible. Obviously you aren't going to have output tool calls, but you could have non-output tool calls right? My intuition is that we should rework it so that we process any tool calls and then handle the structured outputs.

If it's impossible to use tool calls while also using structured output then maybe this is fine but I don't see why that should be the case in principle. (But maybe it's the case in all existing APIs? Or maybe it isn't?)

try:
result_data = output_schema.validate(structured_output)
result_data = await _validate_output(result_data, ctx, None)
except _output.ToolRetryError as e:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunate exception name now

Copy link
Contributor

@dmontagu dmontagu May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use ModelRetry directly here? Maybe that's hard/annoying if things are set up to convert those into ToolRetryError (because it was assumed to be handled in a tool). I don't remember the details..

# Conflicts:
#	tests/models/test_openai.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Structured outputs as an alternative to Tool Calling Support for returning response directly from tool
2 participants