-
Notifications
You must be signed in to change notification settings - Fork 16k
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
core[minor], openai[minor], langchain[patch]: output format on openai #17302
Conversation
baskaryan
commented
Feb 9, 2024
•
edited
Loading
edited
The latest updates on your projects. Learn more about Vercel for Git ↗︎ 1 Ignored Deployment
|
*, | ||
mode: Literal["tools", "json"] = "tools", | ||
enforce_schema: bool = True, | ||
return_single: bool = True, |
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.
What is return_single
?
output_schema: Union[Dict[str, Any], Type[_BM]], | ||
*, | ||
mode: Literal["tools", "json"] = "tools", | ||
enforce_schema: bool = True, |
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.
enforce_schema sounds like validation, but this is not a validation step rather model forced to do tool invocation.
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.
what's a better name? want something not too openai tools specific if we're going to add this method (or similar methods) to other models
|
||
def with_output_format( | ||
self, | ||
output_schema: Union[Dict[str, Any], Type[_BM]], |
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.
Do we have an opinion of what to do about multiple schemas?
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.
what do you mean?
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.
The underlying API supports multiple tools which could be represented as different schemas
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.
would you just want to use bind_tools in that case?
mode: Literal["tools", "json"] = "tools", | ||
enforce_schema: bool = True, | ||
return_single: bool = True, | ||
) -> Runnable[LanguageModelInput, _BM]: |
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.
Not sure that this should return pydantic model by default
To me at least there are 2 separate ideas: (i) schema specification, (ii) optional validation against the 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.
for schema specification, not hard to turn pydantic -> dict if you don't want pydantic output
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 it. Small q
_OutputSchema = TypeVar("_OutputSchema") | ||
_OutputFormat = TypeVar("_OutputFormat") |
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.
should we restrict these a bit? Right now could be anything right? Seems difficult to use with both.
Just an output base model as well and force it to be pydantic or general json type (list/dict/etc) doesn't seem too bad to me
"""""" | ||
if kwargs: | ||
raise ValueError(f"Received unsupported arguments {kwargs}") | ||
is_pydantic_schema = _is_pydantic_class(output_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.
I am still in favor of making a conceptual distinction between schema declaration vs. the parsing.
For example, I will usually want to declare my schema with pydantic, but I don't necessarily want the output to be in pydantic (e.g., i want to stream the structured json out)
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.
you can always dict_schema = convert_to_openai_tool(pydantic_schema)
before passing in schema? i just don't like adding a parameter until we know it's really needed. we can always add it later
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 that will work. We can add a validator='auto'" parameter later on, allowing one to set
validator=Noneor
validator=Type[BaseModel]` or something along those lines?
|
||
def with_output_format( | ||
self, | ||
output_schema: _OutputSchema, |
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 an escape hatch to say that nothing can be extracted?
tool_choice = True -> so tool call will always be forced
The edge case that we need to handle:
- Schema is a single instance rather than a collection
- Goal for example: identify the most qualified candidate based on the following list of candidates and their qualifications
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.
- mv PydanticOutputParser from langchain -> core
- update OpenAI tools parsers to support return_single
- cp updated OpenAI tools parsers from langchain -> openai
- introduce FormattedOutputMixin
- update ChatOpenAI to implement FormattedOutputMixin
_BM = TypeVar("_BM", bound=BaseModel) | ||
_OutputSchema = Union[Dict[str, Any], Type[_BM]] | ||
_FormattedOutput = Union[BaseMessage, Dict, _BM] |
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.
Would it be simpler if we had these be the types for any model that supports structured outputs, so each implementation doesn't have to worry about generics? Just whether or not it supports structured outputs?
method: Literal["function_calling", "json_mode"] = "function_calling", | ||
return_type: Literal["all"] = "all", | ||
**kwargs: Any, | ||
) -> Runnable[LanguageModelInput, _AllFormattedOutput]: | ||
... | ||
|
||
@overload | ||
def with_output_format( | ||
self, | ||
schema: _OutputSchema, | ||
*, | ||
method: Literal["function_calling", "json_mode"] = "function_calling", | ||
return_type: Literal["parsed"] = "parsed", |
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.
The different defaults on the overloads are a bit confusing. Shouldn't defaults be the same as the catch-all definition?
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.e. Shouldn't return_type default always be parsed
or not have a default? For the overload of return_type: Literal["all"]
I think the user has to have it manually defined, so maybe shouldn't have a default?
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.
the types can't overlap for overloads
Co-authored-by: Erick Friis <[email protected]>
_OutputSchema = TypeVar("_OutputSchema") | ||
|
||
|
||
class StructuredOutputMixin(Generic[_OutputSchema], ABC): |
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.
we are back to NOT putting this on base class? why 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.
will only be documented on classes that implement it
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.
so the downside of putting on base class (and defaulting to not implemented) is that it will show up in the documentation for all classes? that doesnt seem that bad...
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.
what's downside of mixin?
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.
It's not that it shows up in documentation, but that it's hard to discover which models actually implement the interface.
After we implement, this we'll need to figure out how users will discover that this is an option
from langchain_core.pydantic_v1 import BaseModel | ||
from langchain_core.runnables import Runnable | ||
|
||
_OutputSchema = TypeVar("_OutputSchema") |
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.
why do we need this? IMO this doesnt really add that much and makes it harder to user
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.
harder for user or contributor? don't think affects user experience?
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.
contributor + any user that tries to read 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.
in the places the mixin is implemented there won't be a typevar, so what's the hard part for someone reading code?
don't feel super strongly on this though if we think typevar is that bad
This fails with an opaque message due to missing doc-string from langchain_openai.chat_models import ChatOpenAI
model = ChatOpenAI()
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
model.with_structured_output(Person).invoke('hello my name is chester')
|
Fails silently -- we could add an instance check maybe? from typing import List
from langchain_openai.chat_models import ChatOpenAI
model = ChatOpenAI()
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
model.with_structured_output(List[Person]).invoke('hello my name is chester and i am 20 years old') |
@@ -22,6 +22,8 @@ class JsonOutputToolsParser(BaseGenerationOutputParser[Any]): | |||
""" | |||
return_id: bool = False | |||
"""Whether to return the tool call id.""" | |||
return_single: bool = False |
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.
Do we think that return_single
is generic enough to warrant introducing it into the parser? Is this the right place? How does this interact with streaming? Would unpacking at the caller code work?
Also if we do keep it here is there a better name? e.g., first_tool_only
?
The doc-string should probably explain why this is necessary?
) | ||
else: | ||
key_name = convert_to_openai_tool(schema)["function"]["name"] | ||
output_parser = JsonOutputKeyToolsParser( |
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.
why not use a runnable lambda here to unpack? is it to make a clearer trace?
…structured_output langchain-ai#17302) ```python class Foo(BaseModel): bar: str structured_llm = ChatOpenAI().with_structured_output(Foo) ``` --------- Co-authored-by: Erick Friis <[email protected]>