-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat: support structured outputs in OpenAIChatGenerator
#9754
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
base: main
Are you sure you want to change the base?
Conversation
Pull Request Test Coverage Report for Build 17578922709Warning: This coverage report may be inaccurate.This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.
Details
💛 - Coveralls |
releasenotes/notes/add-openai-structured-outputs-906be78a984a1079.yaml
Outdated
Show resolved
Hide resolved
releasenotes/notes/add-openai-structured-outputs-906be78a984a1079.yaml
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left some comments.
I also suggest merging main to see the actual coverage. My impression is that several new code paths are not covered by unit tests. I would like to have them covered, since this component is crucial.
if response_format: | ||
if is_streaming: | ||
raise ValueError( | ||
"OpenAI does not support `streaming_callback` with `response_format`, please choose one." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to understand better. It seems that OpenAI supports streaming + structured outputs.
If we are making this choice for simplicity reasons, I would be more specific: "The OpenAIChatGenerator does not ..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm interesting. I think because of the beta code example in documentation I misunderstood that its an unstable feature (not available in completions API). But you are right its supported. I’ll update the function to enable this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I did not notice that it was beta
- It might also be reasonable to skip it if it requires rewriting significantly the component. (Unfortunately, we have many other components depending on this implementation)
- If there are stable ways to do this, let's do it. Otherwise, let's create an issue to track this once that API is no longer in beta.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I looked into their stream function and here looks like the stable version also supports
response_format
. - I will test this locally and will update the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anakin87 Looked into this and here are some points:
- The
response_format
with streaming will be passed tochat.completions.create
. This endpoint allowsresponse_format
to be either a json schema or{ "type": "json_object" }
. But it cannot be apydantic.BaseModel
. - From the documentation, it seems like the beta version supports pydantic models with
stream
endpoint. Which I dont want to introduce for the reasons you mentioned above.
So for now, I believe we can support the first point and mention the limitation in docstrings. Anyways, the error is handled by OpenAI itself if the user passes a pydantic model with streaming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree... Let's do what you propose and mention in docstrings that Pydantic response_format
won't work with streaming.
releasenotes/notes/add-openai-structured-outputs-906be78a984a1079.yaml
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the progress (unit tests still missing)
(Since I'll be off, once the PR is ready, feel free to dismiss my review as stale and let David approve)
releasenotes/notes/add-openai-structured-outputs-906be78a984a1079.yaml
Outdated
Show resolved
Hide resolved
releasenotes/notes/add-openai-structured-outputs-906be78a984a1079.yaml
Outdated
Show resolved
Hide resolved
if "stream" in api_args.keys(): | ||
chat_completion = self.client.chat.completions.create(**api_args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this hard to understand. Something similar to this would work?
We could always return a dictionary with the same fields from _prepare_api_call
.
if api_args.get("response_format"):
# We cannot pass stream param to chat.completions.parse endpoint
api_args.pop("stream", None)
chat_completion = self.client.chat.completions.parse(**api_args)
else:
...
(I might be wrong, in any case I'd appreciate it if we can make the code more intuitive to follow.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm we allow passing response_format
with stream
param to create
endpoint, for streaming structured outputs so this wont work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I now understand, but it's hard to follow.
What I would recommend is to
- include in
api_args
an item calledendpoint
/method
containing "parse" or "create" (in_prepare_api_call
) - (add comments where this value is set to explain why we are doing that.)
- reuse the value in
run
if openai_endpoint == "create": | ||
chat_completion = await self.async_client.chat.completions.create(**api_args) | ||
elif openai_endpoint == "parse": | ||
chat_completion = await self.async_client.chat.completions.parse(**api_args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should add an extra else here that raises an exception in case an unexpected value is popped? It would avoid all the typing issues below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated the condition to use parse
if its passed as the endpoint, else always use create
which was the case before.
@@ -5,7 +5,9 @@ | |||
import os | |||
from typing import Any, Optional, Union | |||
|
|||
from openai.lib._pydantic import to_strict_json_schema |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a different way to import this function that doesn't go through a private file? I'm a little worried the import path is subject to break/change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, for now seems like this is the only way to import this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm I think we can do this a different way. We should be able to directly use the one from pydantic which looks like this parameters_schema = model.model_json_schema()
. This is from the _create_tool_parameters_schema
function in ComponentTool
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OpenAI expect a stricter JSON Schema than Pydantic’s default. For example, objects must set additionalProperties
and Optional
keys are handled differently. As a result, model_json_schema()
often isn’t accepted as-is.
Its also discussed here where another solution is offered but I prefer using the openai method over some unpopular library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nevertheless, I spotted a bug in to_dict
where schema wasn't stored properly. Fixing this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahh okay thanks for the info
Related Issues
Proposed Changes:
response_format
inOpenAIChatGenerator
andAzureOpenAIChatGenerator
.How did you test it?
response_format
using pydantic model and json schema.Notes for the reviewer
Checklist
fix:
,feat:
,build:
,chore:
,ci:
,docs:
,style:
,refactor:
,perf:
,test:
and added!
in case the PR includes breaking changes.