Response Model¶
Defining llm output schemas in Pydantic is done via pydantic.BaseModel
. To learn more about models in pydantic checkout their documentation.
After defining a pydantic model, we can use it as as the response_model
in your client create
calls to openai. The job of the response_model
is to define the schema and prompts for the language model and validate the response from the API and return a pydantic model instance.
Prompting¶
When defining a response model, we can use docstrings and field annotations to define the prompt that will be used to generate the response.
from pydantic import BaseModel, Field
class User(BaseModel):
"""
This is the prompt that will be used to generate the response.
Any instructions here will be passed to the language model.
"""
name: str = Field(description="The name of the user.")
age: int = Field(description="The age of the user.")
Here all docstrings, types, and field annotations will be used to generate the prompt. The prompt will be generated by the create
method of the client and will be used to generate the response.
Optional Values¶
If we use Optional
and default
they will be considered not required when sent to the language model
class User(BaseModel):
name: str = Field(description="The name of the user.")
age: int = Field(description="The age of the user.")
email: Optional[str] = Field(description="The email of the user.", default=None)
Dynamic model creation¶
There are some occasions where it is desirable to create a model using runtime information to specify the fields. For this Pydantic provides the create_model function to allow models to be created on the fly:
from pydantic import BaseModel, create_model
class FooModel(BaseModel):
foo: str
bar: int = 123
BarModel = create_model(
'BarModel',
apple=(str, 'russet'),
banana=(str, 'yellow'),
__base__=FooModel,
)
print(BarModel)
#> <class '__main__.BarModel'>
print(BarModel.model_fields.keys())
#> dict_keys(['foo', 'bar', 'apple', 'banana'])
When would I use this?
Consider a situation where the model is dynamically defined, based on some configuration or database. For example, we could have a database table that stores the properties of a model for some model name or id. We could then query the database for the properties of the model and use that to create the model.
We can then use this information to create the model.
types = {
'string': str,
'integer': int,
'boolean': bool,
'number': float,
'List[str]': List[str],
}
BarModel = create_model(
'User',
**{
property_name: (types[property_type], description)
for property_name, property_type, description in cursor.fetchall()
},
__base__=BaseModel,
)
This would be useful when different users have different descriptions for the same model. We can use the same model but have different prompts for each user.
Structural Pattern Matching¶
Pydantic supports structural pattern matching for models, as introduced by PEP 636 in Python 3.10.
from pydantic import BaseModel
class Pet(BaseModel):
name: str
species: str
a = Pet(name='Bones', species='dog')
match a:
# match `species` to 'dog', declare and initialize `dog_name`
case Pet(species='dog', name=dog_name):
print(f'{dog_name} is a dog')
#> Bones is a dog
# default case
case _:
print('No dog matched')
Adding Behavior¶
We can add methods to our pydantic models just as any plain python class. We might want to do this to add some custom logic to our models.
from pydantic import BaseModel
from typing import Literal
from openai import OpenAI
import instructor
client = instructor.patch(OpenAI())
class SearchQuery(BaseModel):
query: str
query_type: Literal["web", "image", "video"]
def execute(self):
# do some logic here
return results
query = client.chat.completions.create(
..., response_model=SearchQuery
)
results = query.execute()
Now we can call execute
on our model instance after extracting it from a language model. If you want to see more examples of this checkout our post on RAG is more than embeddings