diff --git a/README.md b/README.md index 0b9473781..165941fe5 100644 --- a/README.md +++ b/README.md @@ -66,27 +66,34 @@ import instructor from openai import OpenAI from pydantic import BaseModel + class UserInfo(BaseModel): name: str age: int + # Initialize the OpenAI client with Instructor client = instructor.from_openai(OpenAI()) + # Define hook functions def log_kwargs(**kwargs): print(f"Function called with kwargs: {kwargs}") + def log_exception(exception: Exception): print(f"An exception occurred: {str(exception)}") + client.on("completion:kwargs", log_kwargs) client.on("completion:error", log_exception) user_info = client.chat.completions.create( model="gpt-4o-mini", response_model=UserInfo, - messages=[{"role": "user", "content": "Extract the user name: 'John is 20 years old'"}], + messages=[ + {"role": "user", "content": "Extract the user name: 'John is 20 years old'"} + ], ) """ diff --git a/docs/blog/posts/announcing-gemini-tool-calling-support.md b/docs/blog/posts/announcing-gemini-tool-calling-support.md index 06070670d..d65527b24 100644 --- a/docs/blog/posts/announcing-gemini-tool-calling-support.md +++ b/docs/blog/posts/announcing-gemini-tool-calling-support.md @@ -66,7 +66,7 @@ class User(BaseModel): client = instructor.from_gemini( client=genai.GenerativeModel( - model_name="models/gemini-1.5-flash-latest", # (1)! + model_name="models/gemini-1.5-flash-latest", # (1)! ) ) @@ -105,7 +105,7 @@ class User(BaseModel): client = instructor.from_vertexai( - client=GenerativeModel("gemini-1.5-pro-preview-0409"), # (1)! + client=GenerativeModel("gemini-1.5-pro-preview-0409"), # (1)! ) diff --git a/docs/blog/posts/anthropic-prompt-caching.md b/docs/blog/posts/anthropic-prompt-caching.md index 6b1e8b7fc..9d08c84ac 100644 --- a/docs/blog/posts/anthropic-prompt-caching.md +++ b/docs/blog/posts/anthropic-prompt-caching.md @@ -187,7 +187,6 @@ Let's first initialize our Anthropic client, this will be the same as what we've ```python from instructor import Instructor, Mode, patch from anthropic import Anthropic -from pydantic import BaseModel client = Instructor( @@ -203,9 +202,10 @@ client = Instructor( We'll then create a new `Character` class that will be used to extract out a single character from the text and read in our source text ( roughly 2856 tokens using the Anthropic tokenizer). ```python -with open("./book.txt", "r") as f: +with open("./book.txt") as f: book = f.read() + class Character(BaseModel): name: str description: str @@ -215,7 +215,7 @@ Once we've done this, we can then make an api call to get the description of the ```python for _ in range(2): - resp, completion = client.chat.completions.create_with_completion( # (1)! + resp, completion = client.chat.completions.create_with_completion( # (1)! model="claude-3-haiku-20240307", messages=[ { @@ -224,7 +224,7 @@ for _ in range(2): { "type": "text", "text": "" + book + "", - "cache_control": {"type": "ephemeral"}, # (2)! + "cache_control": {"type": "ephemeral"}, # (2)! }, { "type": "text", @@ -238,7 +238,7 @@ for _ in range(2): ) assert isinstance(resp, Character) - print(completion.usage) # (3)! + print(completion.usage) # (3)! print(resp) ``` @@ -307,7 +307,7 @@ class Character(BaseModel): description: str -with open("./book.txt", "r") as f: +with open("./book.txt") as f: book = f.read() for _ in range(2): diff --git a/docs/blog/posts/bad-schemas-could-break-llms.md b/docs/blog/posts/bad-schemas-could-break-llms.md index af62254fe..b2c5a4917 100644 --- a/docs/blog/posts/bad-schemas-could-break-llms.md +++ b/docs/blog/posts/bad-schemas-could-break-llms.md @@ -49,6 +49,7 @@ from datasets import load_dataset, Dataset, DatasetDict splits = ["test", "train"] + def generate_gsm8k(split): ds = load_dataset("gsm8k", "main", split=split, streaming=True) for row in ds: @@ -60,6 +61,7 @@ def generate_gsm8k(split): "reasoning": reasoning, } + # Create the dataset for train and test splits train_dataset = Dataset.from_generator(lambda: generate_gsm8k("train")) test_dataset = Dataset.from_generator(lambda: generate_gsm8k("test")) @@ -143,6 +145,7 @@ class Answer(BaseModel): chain_of_thought: str answer: int + class OnlyAnswer(BaseModel): answer: int ``` @@ -214,22 +217,26 @@ class Answer(BaseModel): chain_of_thought: str answer: int + class AnswerWithCalculation(BaseModel): chain_of_thought: str required_calculations: list[str] answer: int + class AssumptionBasedAnswer(BaseModel): assumptions: list[str] logic_flow: str answer: int + class ErrorAwareCalculation(BaseModel): key_steps: list[str] potential_pitfalls: list[str] intermediate_results: list[str] answer: int + class AnswerWithNecessaryCalculationAndFinalChoice(BaseModel): chain_of_thought: str necessary_calculations: list[str] @@ -279,21 +286,16 @@ In fact, the only thing that changed was the last two parameters. Upon closer in ```python { - "chain_of_thought": "In the race, there are a total of 240 Asians. Given that 80 were Japanese, we can calculate the number of Chinese participants by subtracting the number of Japanese from the total number of Asians: 240 - 80 = 160. Now, it is given that there are 60 boys on the Chinese team. Therefore, to find the number of girls on the Chinese team, we subtract the number of boys from the total number of Chinese participants: 160 - 60 = 100 girls. Thus, the number of girls on the Chinese team is 100.", - "necessary_calculations": [ - "Total Asians = 240", - "Japanese participants = 80", - "Chinese participants = Total Asians - Japanese participants = 240 - 80 = 160", - "Boys in Chinese team = 60", - "Girls in Chinese team = Chinese participants - Boys in Chinese team = 160 - 60 = 100" - ], - "potential_final_choices": [ - "60", - "100", - "80", - "120" - ], - "final_choice": 2 + "chain_of_thought": "In the race, there are a total of 240 Asians. Given that 80 were Japanese, we can calculate the number of Chinese participants by subtracting the number of Japanese from the total number of Asians: 240 - 80 = 160. Now, it is given that there are 60 boys on the Chinese team. Therefore, to find the number of girls on the Chinese team, we subtract the number of boys from the total number of Chinese participants: 160 - 60 = 100 girls. Thus, the number of girls on the Chinese team is 100.", + "necessary_calculations": [ + "Total Asians = 240", + "Japanese participants = 80", + "Chinese participants = Total Asians - Japanese participants = 240 - 80 = 160", + "Boys in Chinese team = 60", + "Girls in Chinese team = Chinese participants - Boys in Chinese team = 160 - 60 = 100", + ], + "potential_final_choices": ["60", "100", "80", "120"], + "final_choice": 2, } ``` @@ -301,21 +303,16 @@ This meant that instead of the final answer of 100, our model was generating pot ```python { - "chain_of_thought": "First, we need to determine how many Asians were Chinese. Since there were 240 Asians in total and 80 of them were Japanese, we can find the number of Chinese by subtracting the number of Japanese from the total: 240 - 80 = 160. Now, we know that there are 160 Chinese participants. Given that there were 60 boys on the Chinese team, we can find the number of girls by subtracting the number of boys from the total number of Chinese: 160 - 60 = 100. Therefore, there are 100 girls on the Chinese team.", - "necessary_calculations": [ - "Total Asians = 240", - "Number of Japanese = 80", - "Number of Chinese = 240 - 80 = 160", - "Number of boys on Chinese team = 60", - "Number of girls on Chinese team = 160 - 60 = 100" - ], - "potential_final_answers": [ - "100", - "60", - "80", - "40" - ], - "answer": 100 + "chain_of_thought": "First, we need to determine how many Asians were Chinese. Since there were 240 Asians in total and 80 of them were Japanese, we can find the number of Chinese by subtracting the number of Japanese from the total: 240 - 80 = 160. Now, we know that there are 160 Chinese participants. Given that there were 60 boys on the Chinese team, we can find the number of girls by subtracting the number of boys from the total number of Chinese: 160 - 60 = 100. Therefore, there are 100 girls on the Chinese team.", + "necessary_calculations": [ + "Total Asians = 240", + "Number of Japanese = 80", + "Number of Chinese = 240 - 80 = 160", + "Number of boys on Chinese team = 60", + "Number of girls on Chinese team = 160 - 60 = 100", + ], + "potential_final_answers": ["100", "60", "80", "40"], + "answer": 100, } ``` diff --git a/docs/blog/posts/best_framework.md b/docs/blog/posts/best_framework.md index 5c01a6aec..9d8ac5a11 100644 --- a/docs/blog/posts/best_framework.md +++ b/docs/blog/posts/best_framework.md @@ -31,25 +31,27 @@ Here's an example of extracting structured user data from an LLM: from pydantic import BaseModel import instructor + class User(BaseModel): name: str age: int + client = instructor.from_openai(openai.OpenAI()) user = client.chat.completions.create( model="gpt-3.5-turbo", - response_model=User, # (1)! + response_model=User, # (1)! messages=[ { "role": "user", - "content": "Extract the user's name and age from this: John is 25 years old" + "content": "Extract the user's name and age from this: John is 25 years old", } - ] + ], ) -print(user) # (2)! -# > User(name='John', age=25) +print(user) # (2)! +#> User(name='John', age=25) ``` 1. Notice that now we have a new response_model parameter that we pass in to the completions.create method. This parameter lets us specify the structure we want the LLM output to be mapped to. In this case, we're using a Pydantic model called User that describes a user's name and age. diff --git a/docs/blog/posts/caching.md b/docs/blog/posts/caching.md index cd983b4da..2c3aef2a4 100644 --- a/docs/blog/posts/caching.md +++ b/docs/blog/posts/caching.md @@ -107,8 +107,10 @@ print(f"Time taken: {time.perf_counter() - start}") def decorator(func): def wrapper(*args, **kwargs): print("Do something before") # (1) + #> Do something before result = func(*args, **kwargs) print("Do something after") # (2) + #> Do something after return result return wrapper @@ -116,6 +118,7 @@ print(f"Time taken: {time.perf_counter() - start}") @decorator def say_hello(): + #> Hello! print("Hello!") diff --git a/docs/blog/posts/chat-with-your-pdf-with-gemini.md b/docs/blog/posts/chat-with-your-pdf-with-gemini.md index 24ed920a8..ade5fde24 100644 --- a/docs/blog/posts/chat-with-your-pdf-with-gemini.md +++ b/docs/blog/posts/chat-with-your-pdf-with-gemini.md @@ -55,10 +55,12 @@ client = instructor.from_gemini( ) ) + # Define your output structure class Summary(BaseModel): summary: str + # Upload the PDF file = genai.upload_file("path/to/your.pdf") diff --git a/docs/blog/posts/distilation-part1.md b/docs/blog/posts/distilation-part1.md index 98f8592d2..8b5ea3468 100644 --- a/docs/blog/posts/distilation-part1.md +++ b/docs/blog/posts/distilation-part1.md @@ -73,16 +73,16 @@ for _ in range(10): a = random.randint(100, 999) b = random.randint(100, 999) print(fn(a, b)) - #> a=873 b=234 result=204282 - #> a=902 b=203 result=183106 - #> a=962 b=284 result=273208 - #> a=491 b=739 result=362849 - #> a=193 b=400 result=77200 - #> a=300 b=448 result=134400 - #> a=952 b=528 result=502656 - #> a=574 b=797 result=457478 - #> a=482 b=204 result=98328 - #> a=781 b=278 result=217118 + #> a=444 b=204 result=90576 + #> a=194 b=489 result=94866 + #> a=199 b=467 result=92933 + #> a=967 b=452 result=437084 + #> a=718 b=370 result=265660 + #> a=926 b=144 result=133344 + #> a=847 b=570 result=482790 + #> a=649 b=227 result=147323 + #> a=487 b=180 result=87660 + #> a=665 b=400 result=266000 ``` ## The Intricacies of Fine-tuning Language Models diff --git a/docs/blog/posts/fake-data.md b/docs/blog/posts/fake-data.md index ba91896a0..124664d34 100644 --- a/docs/blog/posts/fake-data.md +++ b/docs/blog/posts/fake-data.md @@ -51,13 +51,11 @@ def generate_fake_users(count: int) -> Iterable[UserDetail]: for user in generate_fake_users(5): print(user) - """ - name='Alice' age=25 - name='Bob' age=30 - name='Charlie' age=35 - name='David' age=40 - name='Eve' age=45 - """ + #> name='Alice' age=25 + #> name='Bob' age=30 + #> name='Charlie' age=35 + #> name='David' age=40 + #> name='Eve' age=22 ``` ## Leveraging Simple Examples @@ -93,13 +91,11 @@ def generate_fake_users(count: int) -> Iterable[UserDetail]: for user in generate_fake_users(5): print(user) - """ - name='Timothee Chalamet' age=25 - name='Zendaya' age=24 - name='Keanu Reeves' age=56 - name='Scarlett Johansson' age=36 - name='Chris Hemsworth' age=37 - """ + #> name='John Doe' age=25 + #> name='Jane Smith' age=30 + #> name='Michael Johnson' age=22 + #> name='Emily Davis' age=28 + #> name='David Brown' age=35 ``` By incorporating names of celebrities as examples, we have shifted towards generating synthetic data featuring well-known personalities, moving away from the simplistic, single-word names previously used. @@ -112,13 +108,14 @@ To effectively generate synthetic examples with more nuance, lets upgrade to the import instructor from typing import Iterable -from pydantic import BaseModel, Field, ConfigDict +from pydantic import BaseModel, ConfigDict from openai import OpenAI # Define the UserDetail model class UserDetail(BaseModel): """Old Wizards""" + name: str age: int @@ -148,13 +145,11 @@ def generate_fake_users(count: int) -> Iterable[UserDetail]: for user in generate_fake_users(5): print(user) - """ - name='Merlin' age=196 - name='Saruman the White' age=543 - name='Radagast the Brown' age=89 - name='Morgoth' age=901 - name='Filius Flitwick' age=105 - """ + #> name='Merlin' age=1000 + #> name='Saruman the White' age=700 + #> name='Radagast the Brown' age=600 + #> name='Elminster Aumar' age=1200 + #> name='Mordenkainen' age=850 ``` ## Leveraging Descriptions @@ -194,11 +189,9 @@ def generate_fake_users(count: int) -> Iterable[UserDetail]: for user in generate_fake_users(5): print(user) - """ - name='Jean' age=25 - name='Claire' age=30 - name='Pierre' age=22 - name='Marie' age=27 - name='Luc' age=35 - """ + #> name='Jean Luc' age=30 + #> name='Claire Belle' age=25 + #> name='Pierre Leclair' age=40 + #> name='Amelie Rousseau' age=35 + #> name='Etienne Lefevre' age=28 ``` \ No newline at end of file diff --git a/docs/blog/posts/full-fastapi-visibility.md b/docs/blog/posts/full-fastapi-visibility.md index 77e9cccf8..048f31058 100644 --- a/docs/blog/posts/full-fastapi-visibility.md +++ b/docs/blog/posts/full-fastapi-visibility.md @@ -73,7 +73,6 @@ async def endpoint_function(data: UserData) -> UserDetail: ) return user_detail - ``` This simple endpoint takes in a user query and extracts out a user from the statement. Let's see how we can add in Logfire into this endpoint with just a few lines of code @@ -83,7 +82,7 @@ from pydantic import BaseModel from fastapi import FastAPI from openai import AsyncOpenAI import instructor -import logfire #(1)! +import logfire # (1)! class UserData(BaseModel): @@ -96,7 +95,7 @@ class UserDetail(BaseModel): app = FastAPI() -openai_client = AsyncOpenAI() #(2)! +openai_client = AsyncOpenAI() # (2)! logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) logfire.instrument_openai(openai_client) logfire.instrument_fastapi(app) @@ -152,9 +151,11 @@ Sometimes, we might need to run multiple jobs in parallel. Let's see how we can ```python import asyncio + class MultipleUserData(BaseModel): queries: list[str] + @app.post("/many-users", response_model=list[UserDetail]) async def extract_many_users(data: MultipleUserData): async def extract_user(query: str): @@ -180,8 +181,6 @@ Sometimes, we might need to run multiple jobs in parallel. Let's see how we can from openai import AsyncOpenAI import instructor import logfire - from collections.abc import Iterable - from fastapi.responses import StreamingResponse import asyncio @@ -265,17 +264,20 @@ Let's add a new endpoint to our server to see how this might work === "New Code" ```python - import asyncio from collections.abc import Iterable from fastapi.responses import StreamingResponse + class MultipleUserData(BaseModel): queries: list[str] + @app.post("/extract", response_class=StreamingResponse) async def extract(data: UserData): supressed_client = AsyncOpenAI() - logfire.instrument_openai(supressed_client, suppress_other_instrumentation=False) #(1)! + logfire.instrument_openai( + supressed_client, suppress_other_instrumentation=False + ) # (1)! client = instructor.from_openai(supressed_client) users = await client.chat.completions.create( model="gpt-3.5-turbo", @@ -404,7 +406,6 @@ response = requests.post( for chunk in response.iter_content(chunk_size=1024): if chunk: print(str(chunk, encoding="utf-8"), end="\n") - ``` This gives us the output of diff --git a/docs/blog/posts/generating-pdf-citations.md b/docs/blog/posts/generating-pdf-citations.md index 79cd21b39..41bdd1c93 100644 --- a/docs/blog/posts/generating-pdf-citations.md +++ b/docs/blog/posts/generating-pdf-citations.md @@ -41,12 +41,7 @@ pip install "instructor[google-generativeai]" pymupdf Then let's import the necessary libraries: ```python -import instructor -import google.generativeai as genai -from google.ai.generativelanguage_v1beta.types.file import File -from pydantic import BaseModel -import pymupdf -import time + ``` ## Defining Our Data Models @@ -59,6 +54,7 @@ class Citation(BaseModel): text: list[str] page_number: int + class Answer(BaseModel): chain_of_thought: str citations: list[Citation] @@ -128,7 +124,6 @@ print(resp) # ], # answer="In 2023, the U.S. government (USG) announced new licensing requirements for the export of certain chips to China, Russia, and other countries. These chips included the A100 and H100 integrated circuits, the DGX system, and any other systems or boards incorporating the A100 or H100 chips.", # ) - ``` ## Highlighting Citations in the PDF diff --git a/docs/blog/posts/generator.md b/docs/blog/posts/generator.md index 79c050993..6e553351c 100644 --- a/docs/blog/posts/generator.md +++ b/docs/blog/posts/generator.md @@ -90,7 +90,7 @@ def calculate_time_for_first_result_with_generator(func_input, func): result = next(func(x) for x in func_input) end_perf = time.perf_counter() print(f"Time for first result (generator): {end_perf - start_perf:.2f} seconds") - #> Time for first result (generator): 1.01 seconds + #> Time for first result (generator): 1.00 seconds return result diff --git a/docs/blog/posts/google-openai-client.md b/docs/blog/posts/google-openai-client.md index 9e56fde94..df3b556c8 100644 --- a/docs/blog/posts/google-openai-client.md +++ b/docs/blog/posts/google-openai-client.md @@ -32,14 +32,14 @@ This looks something like this: ```python from openai import OpenAI + client = OpenAI( - base_url="https://generativelanguage.googleapis.com/v1beta/", - api_key="YOUR_API_KEY" + base_url="https://generativelanguage.googleapis.com/v1beta/", api_key="YOUR_API_KEY" ) response = client.chat.completions.create( model="gemini-1.5-flash", - messages=[{"role": "user", "content": "Extract name and age from: John is 30"}] + messages=[{"role": "user", "content": "Extract name and age from: John is 30"}], ) ``` @@ -54,6 +54,7 @@ class User(BaseModel): name: str age: int + class Users(BaseModel): users: list[User] # Nested schema - will throw an error ``` @@ -179,7 +180,6 @@ for r in resp: print(r) - # title = None summary = None # title='The Little Firefly Who Lost His Light' summary=None # title='The Little Firefly Who Lost His Light' summary='A tiny firefly learns the true meaning of friendship when he loses his glow and a wise old owl helps him find it again.' @@ -216,9 +216,7 @@ If we wanted to switch to Anthropic, all it takes is changing the following line from anthropic import Anthropic from instructor import from_anthropic -client = from_anthropic( - Anthropic() -) +client = from_anthropic(Anthropic()) # rest of code ``` diff --git a/docs/blog/posts/introducing-structured-outputs-with-cerebras-inference.md b/docs/blog/posts/introducing-structured-outputs-with-cerebras-inference.md index a3b84b0a9..5bc5d65fd 100644 --- a/docs/blog/posts/introducing-structured-outputs-with-cerebras-inference.md +++ b/docs/blog/posts/introducing-structured-outputs-with-cerebras-inference.md @@ -97,7 +97,7 @@ We also support streaming with the Cerebras client with the `CEREBRAS_JSON` mo ```python import instructor -from cerebras.cloud.sdk import Cerebras, AsyncCerebras +from cerebras.cloud.sdk import Cerebras from pydantic import BaseModel from typing import Iterable @@ -123,9 +123,9 @@ resp = client.chat.completions.create( for person in resp: print(person) - # > Person(name='Chris', age=27) - # > Person(name='John', age=30) - # > Person(name='Jessica', age=26) + #> Person(name='Chris', age=27) + #> Person(name='John', age=30) + #> Person(name='Jessica', age=26) ``` And that’s it! We're excited to see what you build with Instructor and Cerebras! If you have any questions about Cerebras or need to get off the API key waitlist, please reach out to sarah.chieng@cerebras.net. diff --git a/docs/blog/posts/introducing-structured-outputs.md b/docs/blog/posts/introducing-structured-outputs.md index b6f06ef42..8e0f47ca8 100644 --- a/docs/blog/posts/introducing-structured-outputs.md +++ b/docs/blog/posts/introducing-structured-outputs.md @@ -79,7 +79,7 @@ except Exception as e: 1 validation error for User name Value error, All letters must be uppercase. Got: Jason [type=value_error, input_value='Jason', input_type=str] - For further information visit https://errors.pydantic.dev/2.8/v/value_error + For further information visit https://errors.pydantic.dev/2.9/v/value_error """ ``` @@ -117,7 +117,17 @@ with client.beta.chat.completions.stream( for event in stream: if event.type == "content.delta": print(event.snapshot, flush=True, end="\n") - #> + #> + #> {" + #> {"name + #> {"name":" + #> {"name":"Jason + #> {"name":"Jason"," + #> {"name":"Jason","age + #> {"name":"Jason","age": + #> {"name":"Jason","age":25 + #> {"name":"Jason","age":25} + # > #> {" #> {"name #> {"name":" @@ -195,9 +205,7 @@ This built-in retry logic allows for targetted correction to the generated respo A common use-case is to define a single schema and extract multiple instances of it. With `instructor`, doing this is relatively straightforward by using [our `create_iterable` method](../../concepts/lists.md). ```python -import instructor -import openai -from pydantic import BaseModel + ``` client = instructor.from_openai(openai.OpenAI(), mode=instructor.Mode.TOOLS_STRICT) diff --git a/docs/blog/posts/jinja-proposal.md b/docs/blog/posts/jinja-proposal.md index ddbdd0b02..84c2c841a 100644 --- a/docs/blog/posts/jinja-proposal.md +++ b/docs/blog/posts/jinja-proposal.md @@ -65,7 +65,7 @@ client.create( model="gpt-4o", messages=[ { - "role": "user", + "role": "user", "content": """ You are a {{ role }} tasks with the following question @@ -91,18 +91,18 @@ client.create( * {{ rule }} {% endfor %} {% endif %} - """ + """, }, ], context={ - "role": "professional educator", - "question": "What is the capital of France?", + "role": "professional educator", + "question": "What is the capital of France?", "context": [ - {"id": 1, "text": "Paris is the capital of France."}, - {"id": 2, "text": "France is a country in Europe."} - ], - "rules": ["Use markdown."] - } + {"id": 1, "text": "Paris is the capital of France."}, + {"id": 2, "text": "France is a country in Europe."}, + ], + "rules": ["Use markdown."], + }, ) ``` @@ -199,34 +199,40 @@ Consider using secret string to pass in sensitive information to the llm. ```python from pydantic import BaseModel, SecretStr + class UserContext(BaseModel): name: str address: SecretStr + class Address(BaseModel): street: SecretStr city: str state: str zipcode: str + def normalize_address(address: Address): - context = UserContext(username="scolvin", address=address) - address = client.create( - model="gpt-4o", - messages=[ - { - "role": "user", - "content": "{{ user.name }} is `{{ user.address.get_secret_value() }}`, normalize it to an address object" - }, - ], - context={"user": context}, - ) - print(context) - # > UserContext(username='jliu', address="******") - print(address) - # > Address(street='******', city="Toronto", state="Ontario", zipcode="M5A 0J3") - logger.info(f"Normalized address: {address}", extra={"user_context": context, "address": address}) - return address + context = UserContext(username="scolvin", address=address) + address = client.create( + model="gpt-4o", + messages=[ + { + "role": "user", + "content": "{{ user.name }} is `{{ user.address.get_secret_value() }}`, normalize it to an address object", + }, + ], + context={"user": context}, + ) + print(context) + #> UserContext(username='jliu', address="******") + print(address) + #> Address(street='******', city="Toronto", state="Ontario", zipcode="M5A 0J3") + logger.info( + f"Normalized address: {address}", + extra={"user_context": context, "address": address}, + ) + return address ``` This approach offers several advantages: diff --git a/docs/blog/posts/langsmith.md b/docs/blog/posts/langsmith.md index 7caae6088..16e26377c 100644 --- a/docs/blog/posts/langsmith.md +++ b/docs/blog/posts/langsmith.md @@ -67,6 +67,7 @@ client = instructor.from_openai(client, mode=instructor.Mode.TOOLS) # Rate limit the number of requests sem = asyncio.Semaphore(5) + # Use an Enum to define the types of questions class QuestionType(Enum): CONTACT = "CONTACT" diff --git a/docs/blog/posts/llm-as-reranker.md b/docs/blog/posts/llm-as-reranker.md index 32b6a3c5c..3104f3ade 100644 --- a/docs/blog/posts/llm-as-reranker.md +++ b/docs/blog/posts/llm-as-reranker.md @@ -39,7 +39,6 @@ First, let's set up our environment with the necessary imports: ```python import instructor from openai import OpenAI -from pydantic import BaseModel, Field, field_validator client = instructor.from_openai(OpenAI()) ``` @@ -55,7 +54,9 @@ Notice that not only do I reference the chunk_id in the label class, I also aske ```python class Label(BaseModel): chunk_id: int = Field(description="The unique identifier of the text chunk") - chain_of_thought: str = Field(description="The reasoning process used to evaluate the relevance") + chain_of_thought: str = Field( + description="The reasoning process used to evaluate the relevance" + ) relevancy: int = Field( description="Relevancy score from 0 to 10, where 10 is most relevant", ge=0, @@ -159,6 +160,7 @@ def main(): print(f"Reasoning: {label.chain_of_thought}") print() + if __name__ == "__main__": main() ``` @@ -182,7 +184,9 @@ class Label(BaseModel): context = info.context chunks = context["chunks"] if v not in [chunk["id"] for chunk in chunks]: - raise ValueError(f"Chunk with id {v} not found, must be one of {[chunk['id'] for chunk in chunks]}") + raise ValueError( + f"Chunk with id {v} not found, must be one of {[chunk['id'] for chunk in chunks]}" + ) return v ``` diff --git a/docs/blog/posts/logfire.md b/docs/blog/posts/logfire.md index 0c0b9cf01..7ebb5addb 100644 --- a/docs/blog/posts/logfire.md +++ b/docs/blog/posts/logfire.md @@ -54,15 +54,14 @@ Now that we've got Logfire setup, let's see how we can get it to help us track a Logfire is dead simple to integrate - all it takes is 2 lines of code and we have it setup. ```python -from pydantic import BaseModel from openai import OpenAI import instructor import logfire openai_client = OpenAI() -logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) #(1)! -logfire.instrument_openai(openai_client) #(2)! +logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) # (1)! +logfire.instrument_openai(openai_client) # (2)! client = instructor.from_openai(openai_client) ``` @@ -74,6 +73,7 @@ In this example, we'll be looking at classifying emails as either spam or not sp ```python import enum + class Labels(str, enum.Enum): """Enumeration for single-label text classification.""" @@ -94,7 +94,7 @@ We can then use this in a generic instructor function as seen below that simply Logfire can help us to log this entire function, and what's happening inside it, even down to the model validation level by using their `logfire.instrument` decorator. ```python -@logfire.instrument("classification", extract_args=True) #(1)! +@logfire.instrument("classification", extract_args=True) # (1)! def classify(data: str) -> SinglePrediction: """Perform single-label classification on the input text.""" return client.chat.completions.create( @@ -138,7 +138,7 @@ For our second example, we'll use the inbuilt `llm_validator` that instructor pr ```python from typing import Annotated -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel from pydantic.functional_validators import AfterValidator from instructor import llm_validator import logfire @@ -203,7 +203,8 @@ What we want is an output of the combined numbers as seen below This is relatively simple with Pydantic. What we need to do is to define a custom type which will handle the conversion process as seen below ```python -from pydantic import BaseModel, Field, BeforeValidator, PlainSerializer, InstanceOf, WithJsonSchema +from pydantic import BeforeValidator, InstanceOf, WithJsonSchema + def md_to_df(data: Any) -> Any: # Convert markdown to DataFrame @@ -222,9 +223,9 @@ def md_to_df(data: Any) -> Any: MarkdownDataFrame = Annotated[ - InstanceOf[pd.DataFrame], #(1)! - BeforeValidator(md_to_df), #(2)! - WithJsonSchema( #(3)! + InstanceOf[pd.DataFrame], # (1)! + BeforeValidator(md_to_df), # (2)! + WithJsonSchema( # (3)! { "type": "string", "description": "The markdown representation of the table, each one should be tidy, do not try to join tables that should be seperate", @@ -247,9 +248,8 @@ import logfire openai_client = OpenAI() logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all")) logfire.instrument_openai(openai_client) -client = instructor.from_openai( - openai_client, mode=instructor.Mode.MD_JSON -) +client = instructor.from_openai(openai_client, mode=instructor.Mode.MD_JSON) + @logfire.instrument("extract-table", extract_args=True) def extract_table_from_image(url: str) -> Iterable[Table]: diff --git a/docs/blog/posts/matching-language.md b/docs/blog/posts/matching-language.md index 3de932386..42aa47571 100644 --- a/docs/blog/posts/matching-language.md +++ b/docs/blog/posts/matching-language.md @@ -208,7 +208,9 @@ In den letzten Jahren sind Sprachmodelle immer ausgefeilter geworden und können --- 近年、言語モデルは非常に洗練され、自然で流暢なテキストを生成できるようになり、機械翻訳、質問応答、クリエイティブなテキスト生成など、様々なタスクで優れたパフォーマンスを発揮しています。これらのモデルは膨大なテキストデータセットで学習され、自然言語の構造とニュアンスを捉えることができます。言語モデルの改善により、コンピューターと人間のコミュニケーションに革命が起こる可能性があり、将来のさらなる進歩が期待されています。 -""".split("---"), +""".split( + "---" + ), ) # Patch the OpenAI client to enable response_model @@ -221,6 +223,7 @@ class GeneratedSummary(BaseModel): ) summary: str + async def summarize_text(text: str): response = await client.chat.completions.create( model="gpt-3.5-turbo", diff --git a/docs/blog/posts/multimodal-gemini.md b/docs/blog/posts/multimodal-gemini.md index df1f4a2c9..3a1ddb8ba 100644 --- a/docs/blog/posts/multimodal-gemini.md +++ b/docs/blog/posts/multimodal-gemini.md @@ -25,9 +25,7 @@ In this post, we'll explore how to use Google's Gemini model with Instructor to First, let's set up our environment with the necessary libraries: ```python -from pydantic import BaseModel -import instructor -import google.generativeai as genai + ``` @@ -42,6 +40,7 @@ class TouristDestination(BaseModel): description: str location: str + class Recommendations(BaseModel): chain_of_thought: str description: str @@ -193,9 +192,10 @@ To address these limitations and expand the capabilities of our video analysis s ```python class TimestampedRecommendation(BaseModel): timestamp: str - timestamp_format: Literal["HH:MM", "HH:MM:SS"] # Helps with parsing + timestamp_format: Literal["HH:MM", "HH:MM:SS"] # Helps with parsing recommendation: str + class EnhancedRecommendations(BaseModel): destinations: list[TouristDestination] timestamped_mentions: list[TimestampedRecommendation] diff --git a/docs/blog/posts/openai-distilation-store.md b/docs/blog/posts/openai-distilation-store.md index 1956e8e9c..90c21639f 100644 --- a/docs/blog/posts/openai-distilation-store.md +++ b/docs/blog/posts/openai-distilation-store.md @@ -42,6 +42,7 @@ from pydantic import BaseModel # Enable response_model and API Model Distillation client = instructor.patch(OpenAI()) + class UserDetail(BaseModel): name: str age: int @@ -49,6 +50,7 @@ class UserDetail(BaseModel): def introduce(self): return f"Hello, I'm {self.name} and I'm {self.age} years old" + # Use the store parameter to enable API Model Distillation user: UserDetail = client.chat.completions.create( model="gpt-3.5-turbo", @@ -56,7 +58,7 @@ user: UserDetail = client.chat.completions.create( messages=[ {"role": "user", "content": "Extract Jason is 25 years old"}, ], - store=True # Enable API Model Distillation + store=True, # Enable API Model Distillation ) ``` @@ -76,10 +78,7 @@ user: UserDetail = client.chat.completions.create( {"role": "user", "content": "Extract Jason is 25 years old"}, ], store=True, - metadata={ - "task": "user_extraction", - "source": "customer_support_chat" - } + metadata={"task": "user_extraction", "source": "customer_support_chat"}, ) ``` diff --git a/docs/blog/posts/openai-multimodal.md b/docs/blog/posts/openai-multimodal.md index faac8b0a8..8ea8a725b 100644 --- a/docs/blog/posts/openai-multimodal.md +++ b/docs/blog/posts/openai-multimodal.md @@ -40,14 +40,15 @@ from openai import OpenAI from pydantic import BaseModel import instructor from instructor.multimodal import Audio -import base64 client = instructor.from_openai(OpenAI()) + class Person(BaseModel): name: str age: int + resp = client.chat.completions.create( model="gpt-4o-audio-preview", response_model=Person, diff --git a/docs/blog/posts/pairwise-llm-judge.md b/docs/blog/posts/pairwise-llm-judge.md index d5e084dcf..4a752b60f 100644 --- a/docs/blog/posts/pairwise-llm-judge.md +++ b/docs/blog/posts/pairwise-llm-judge.md @@ -33,7 +33,6 @@ First, let's set up our environment with the necessary imports: ```python import instructor import openai -from pydantic import BaseModel, Field client = instructor.from_openai(openai.OpenAI()) ``` @@ -88,7 +87,7 @@ def judge_relevance(question: str, text: str) -> Judgment: Before giving your final judgment, provide a justification for your decision. Explain the key factors that led to your conclusion. Please ensure your analysis is thorough, impartial, and based on the content provided. - """ + """, }, { "role": "user", @@ -103,8 +102,8 @@ def judge_relevance(question: str, text: str) -> Judgment: {{text}} - """ - } + """, + }, ], response_model=Judgment, context={"question": question, "text": text}, diff --git a/docs/blog/posts/parea.md b/docs/blog/posts/parea.md index 6cca9bf87..92661352c 100644 --- a/docs/blog/posts/parea.md +++ b/docs/blog/posts/parea.md @@ -46,18 +46,15 @@ Parea is dead simple to integrate - all it takes is 2 lines of code, and we have import os import instructor -import requests from dotenv import load_dotenv from openai import OpenAI -from pydantic import BaseModel, field_validator, Field -import re -from parea import Parea #(1)! +from parea import Parea # (1)! load_dotenv() client = OpenAI() -p = Parea(api_key=os.getenv("PAREA_API_KEY")) #(2)! +p = Parea(api_key=os.getenv("PAREA_API_KEY")) # (2)! p.wrap_openai_client(client, "instructor") client = instructor.from_openai(client) @@ -106,7 +103,7 @@ email = client.messages.create( model="gpt-3.5-turbo", max_tokens=1024, max_retries=3, - messages=[ #(1)! + messages=[ # (1)! { "role": "user", "content": "I'm responding to a student's question. Here is the link to the documentation: {{doc_link1}} and {{doc_link2}}", @@ -155,8 +152,8 @@ Sometimes you may want to let subject-matter experts (SMEs) label responses to u p = Parea(api_key=os.getenv("PAREA_API_KEY")) - dataset = p.get_collection(DATASET_ID) #(1)! - dataset.write_to_finetune_jsonl("finetune.jsonl") #(2)! + dataset = p.get_collection(DATASET_ID) # (1)! + dataset.write_to_finetune_jsonl("finetune.jsonl") # (2)! ``` 1. Replace `DATASET_ID` with the actual dataset ID diff --git a/docs/blog/posts/pydantic-is-still-all-you-need.md b/docs/blog/posts/pydantic-is-still-all-you-need.md index 3e2588a9d..df5f18880 100644 --- a/docs/blog/posts/pydantic-is-still-all-you-need.md +++ b/docs/blog/posts/pydantic-is-still-all-you-need.md @@ -45,12 +45,11 @@ And here's the kicker: nothing's really changed in the past year. The core API i ```python from instructor import from_openai + client = from_openai(OpenAI()) response = client.chat.completions.create( - model="gpt-3.5-turbo", - response_model=User, - messages=[...] + model="gpt-3.5-turbo", response_model=User, messages=[...] ) ``` diff --git a/docs/blog/posts/rag-timelines.md b/docs/blog/posts/rag-timelines.md index 26311e7ea..4036a87a8 100644 --- a/docs/blog/posts/rag-timelines.md +++ b/docs/blog/posts/rag-timelines.md @@ -34,6 +34,7 @@ from datetime import datetime from typing import Optional from pydantic import BaseModel + class TimeFilter(BaseModel): start_date: Optional[datetime] = None end_date: Optional[datetime] = None diff --git a/docs/blog/posts/situate-context.md b/docs/blog/posts/situate-context.md index a3723a349..98ffe369f 100644 --- a/docs/blog/posts/situate-context.md +++ b/docs/blog/posts/situate-context.md @@ -79,9 +79,13 @@ from pydantic import BaseModel, Field import asyncio from typing import List, Dict + class SituatedContext(BaseModel): title: str = Field(..., description="The title of the document.") - context: str = Field(..., description="The context to situate the chunk within the document.") + context: str = Field( + ..., description="The context to situate the chunk within the document." + ) + client = AsyncInstructor( create=patch( @@ -91,6 +95,7 @@ client = AsyncInstructor( mode=Mode.ANTHROPIC_TOOLS, ) + async def situate_context(doc: str, chunk: str) -> str: response = await client.chat.completions.create( model="claude-3-haiku-20240307", @@ -117,6 +122,7 @@ async def situate_context(doc: str, chunk: str) -> str: ) return response.context + def chunking_function(doc: str) -> List[str]: chunk_size = 1000 overlap = 200 @@ -128,12 +134,11 @@ def chunking_function(doc: str) -> List[str]: start += chunk_size - overlap return chunks + async def process_chunk(doc: str, chunk: str) -> Dict[str, str]: context = await situate_context(doc, chunk) - return { - "chunk": chunk, - "context": context - } + return {"chunk": chunk, "context": context} + async def process(doc: str) -> List[Dict[str, str]]: chunks = chunking_function(doc) @@ -141,6 +146,7 @@ async def process(doc: str) -> List[Dict[str, str]]: results = await asyncio.gather(*tasks) return results + # Example usage async def main(): document = "Your full document text here..." @@ -151,6 +157,7 @@ async def main(): print(f"Context: {item['context']}") print() + if __name__ == "__main__": asyncio.run(main()) ``` diff --git a/docs/blog/posts/structured-output-anthropic.md b/docs/blog/posts/structured-output-anthropic.md index 0df11224e..5aecd78b0 100644 --- a/docs/blog/posts/structured-output-anthropic.md +++ b/docs/blog/posts/structured-output-anthropic.md @@ -41,20 +41,21 @@ import anthropic import instructor # Patch the Anthropic client with Instructor -anthropic_client = instructor.from_anthropic( - create=anthropic.Anthropic() -) +anthropic_client = instructor.from_anthropic(create=anthropic.Anthropic()) + # Define your Pydantic models class Properties(BaseModel): name: str value: str + class User(BaseModel): name: str age: int properties: List[Properties] + # Use the patched client to generate structured output user_response = anthropic_client( model="claude-3-haiku-20240307", @@ -89,20 +90,21 @@ Anthropic has introduced a new prompt caching feature that can significantly imp Here's how you can implement prompt caching with Instructor and Anthropic: ```python -from instructor import Instructor, Mode, patch from anthropic import Anthropic from pydantic import BaseModel # Set up the client with prompt caching client = instructor.from_anthropic(Anthropic()) + # Define your Pydantic model class Character(BaseModel): name: str description: str + # Load your large context -with open("./book.txt", "r") as f: +with open("./book.txt") as f: book = f.read() # Make multiple calls using the cached context diff --git a/docs/blog/posts/tidy-data-from-messy-tables.md b/docs/blog/posts/tidy-data-from-messy-tables.md index 78e0bdda1..1969af395 100644 --- a/docs/blog/posts/tidy-data-from-messy-tables.md +++ b/docs/blog/posts/tidy-data-from-messy-tables.md @@ -33,13 +33,10 @@ Using tools like instructor to automatically convert untidy data into tidy forma Let's start by first defining a custom type that can parse the markdown table into a pandas dataframe. ```python -import instructor from io import StringIO from typing import Annotated, Any from pydantic import BeforeValidator, PlainSerializer, InstanceOf, WithJsonSchema import pandas as pd -from pydantic import BaseModel -from openai import OpenAI def md_to_df(data: Any) -> Any: @@ -80,29 +77,36 @@ import instructor from pydantic import BaseModel from openai import OpenAI + class Table(BaseModel): caption: str dataframe: MarkdownDataFrame # Custom type for handling tables + class TidyTables(BaseModel): tables: list[Table] + # Patch the OpenAI client with instructor client = instructor.from_openai(OpenAI()) + def extract_table(image_path: str) -> TidyTables: return client.chat.completions.create( model="gpt-4o-mini", - messages=[{ - "role": "user", - "content": [ - "Convert this untidy table to tidy format", - instructor.Image.from_path(image_path) - ] - }], - response_model=TidyTables + messages=[ + { + "role": "user", + "content": [ + "Convert this untidy table to tidy format", + instructor.Image.from_path(image_path), + ], + } + ], + response_model=TidyTables, ) + extracted_tables = extract_table("./untidy_table.png") ``` diff --git a/docs/blog/posts/timestamp.md b/docs/blog/posts/timestamp.md index 680874e7c..11bb33e57 100644 --- a/docs/blog/posts/timestamp.md +++ b/docs/blog/posts/timestamp.md @@ -38,6 +38,7 @@ class Segment(BaseModel): title: str = Field(..., description="The title of the segment") timestamp: str = Field(..., description="The timestamp of the event as HH:MM:SS") + # This might work for some cases, but fails for others: # "2:00" could be interpreted as 2 minutes or 2 hours # "1:30:00" doesn't fit the expected format @@ -59,6 +60,7 @@ Let's look at the improved implementation: from pydantic import BaseModel, Field, model_validator from typing import Literal + class SegmentWithTimestamp(BaseModel): title: str = Field(..., description="The title of the segment") time_format: Literal["HH:MM:SS", "MM:SS"] = Field( diff --git a/docs/blog/posts/using_json.md b/docs/blog/posts/using_json.md index 05bd4017c..e6ebd1cf5 100644 --- a/docs/blog/posts/using_json.md +++ b/docs/blog/posts/using_json.md @@ -53,11 +53,13 @@ Here's an example of a `response_model` for a simple user profile: ```python from pydantic import BaseModel + class User(BaseModel): name: str age: int email: str + client = instructor.from_openai(openai.OpenAI()) user = client.chat.completions.create( @@ -66,13 +68,13 @@ user = client.chat.completions.create( messages=[ { "role": "user", - "content": "Extract the user's name, age, and email from this: John Doe is 25 years old. His email is john@example.com" + "content": "Extract the user's name, age, and email from this: John Doe is 25 years old. His email is john@example.com", } - ] + ], ) print(user.model_dump()) -# > { +#> { # "name": "John Doe", # "age": 25, # "email": "john@example.com" diff --git a/docs/blog/posts/validation-part1.md b/docs/blog/posts/validation-part1.md index fec5e83b4..85a926492 100644 --- a/docs/blog/posts/validation-part1.md +++ b/docs/blog/posts/validation-part1.md @@ -114,7 +114,7 @@ except ValidationError as e: 1 validation error for UserMessage message Value error, `rob` was found in the message `We should go and rob a bank` [type=value_error, input_value='We should go and rob a bank', input_type=str] - For further information visit https://errors.pydantic.dev/2.6/v/value_error + For further information visit https://errors.pydantic.dev/2.9/v/value_error """ ``` @@ -160,7 +160,7 @@ except ValidationError as e: 1 validation error for UserMessage message Value error, `rob` was found in the message `We should go and rob a bank` [type=value_error, input_value='We should go and rob a bank', input_type=str] - For further information visit https://errors.pydantic.dev/2.6/v/value_error + For further information visit https://errors.pydantic.dev/2.9/v/value_error """ ``` diff --git a/docs/blog/posts/version-1.md b/docs/blog/posts/version-1.md index 064203eaf..d27a54cd2 100644 --- a/docs/blog/posts/version-1.md +++ b/docs/blog/posts/version-1.md @@ -47,14 +47,11 @@ Except now, any default arguments you want to place into the `create` call will IF you know you want to pass in tempurature, seed, or model, you can do so. ```python - import openai import instructor client = instructor.from_openai( - openai.OpenAI(), - model="gpt-4-turbo-preview", - temperature=0.2 + openai.OpenAI(), model="gpt-4-turbo-preview", temperature=0.2 ) ``` @@ -96,10 +93,12 @@ import openai import instructor from pydantic import BaseModel + class User(BaseModel): name: str age: int + client = instructor.from_openai(openai.OpenAI()) user = client.chat.completions.create( @@ -205,6 +204,16 @@ user_stream = client.chat.completions.create_partial( for user in user_stream: print(user) + #> name=None age=None + #> name=None age=None + #> name=None age=None + #> name=None age=25 + #> name=None age=25 + #> name=None age=25 + #> name='' age=25 + #> name='John' age=25 + #> name='John Smith' age=25 + #> name='John Smith' age=25 # name=None age=None # name='' age=None # name='John' age=None @@ -244,6 +253,8 @@ users = client.chat.completions.create_iterable( for user in users: print(user) + #> name='John Doe' age=30 + #> name='Jane Smith' age=28 # User(name='John Doe', age=30) # User(name='Jane Smith', age=25) ``` diff --git a/docs/blog/posts/writer-support.md b/docs/blog/posts/writer-support.md index 08d39e588..45b163bc6 100644 --- a/docs/blog/posts/writer-support.md +++ b/docs/blog/posts/writer-support.md @@ -128,11 +128,11 @@ from typing import Annotated from writerai import Writer from pydantic import BaseModel, AfterValidator, Field -#Initialize Writer client +# Initialize Writer client client = instructor.from_writer(Writer()) -#Example of model, that may require usage of retries +# Example of model, that may require usage of retries def uppercase_validator(v): if v.islower(): raise ValueError("Name must be in uppercase") diff --git a/docs/concepts/caching.md b/docs/concepts/caching.md index 71d5ae43b..4c68e88f7 100644 --- a/docs/concepts/caching.md +++ b/docs/concepts/caching.md @@ -38,12 +38,12 @@ def extract(data) -> UserDetail: start = time.perf_counter() # (1) model = extract("Extract jason is 25 years old") print(f"Time taken: {time.perf_counter() - start}") -#> Time taken: 0.41948329200000023 +#> Time taken: 0.38183529197704047 start = time.perf_counter() model = extract("Extract jason is 25 years old") # (2) print(f"Time taken: {time.perf_counter() - start}") -#> Time taken: 1.4579999998431958e-06 +#> Time taken: 8.75093974173069e-07 ``` 1. Using `time.perf_counter()` to measure the time taken to run the function is better than using `time.time()` because it's more accurate and less susceptible to system clock changes. diff --git a/docs/concepts/hooks.md b/docs/concepts/hooks.md index 7999f480e..3688cef16 100644 --- a/docs/concepts/hooks.md +++ b/docs/concepts/hooks.md @@ -185,46 +185,99 @@ def log_completion_kwargs(kwargs) -> None: def log_completion_response(response) -> None: print("## Completion response:") + #> ## Completion response: + """ + { + 'id': 'chatcmpl-AWl4Mj5Jrv7m7JkOTIiHXSldQIOFm', + 'choices': [ + { + 'finish_reason': 'stop', + 'index': 0, + 'logprobs': None, + 'message': { + 'content': None, + 'refusal': None, + 'role': 'assistant', + 'audio': None, + 'function_call': None, + 'tool_calls': [ + { + 'id': 'call_6oQ9WXxeSiVEV71B9IYtsbIE', + 'function': { + 'arguments': '{"name":"John","age":-1}', + 'name': 'User', + }, + 'type': 'function', + } + ], + }, + } + ], + 'created': 1732370794, + 'model': 'gpt-4o-mini-2024-07-18', + 'object': 'chat.completion', + 'service_tier': None, + 'system_fingerprint': 'fp_0705bf87c0', + 'usage': { + 'completion_tokens': 10, + 'prompt_tokens': 87, + 'total_tokens': 97, + 'completion_tokens_details': { + 'audio_tokens': 0, + 'reasoning_tokens': 0, + 'accepted_prediction_tokens': 0, + 'rejected_prediction_tokens': 0, + }, + 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}, + }, + } + """ print(response.model_dump()) """ { - "id": "chatcmpl-AJHKkGTSwkxdmxBuaz69q4yCeqIZK", - "choices": [ + 'id': 'chatcmpl-AWl4Mxdq0BUGRlVCA61z8YOIVga7F', + 'choices': [ { - "finish_reason": "stop", - "index": 0, - "logprobs": None, - "message": { - "content": None, - "refusal": None, - "role": "assistant", - "function_call": None, - "tool_calls": [ + 'finish_reason': 'stop', + 'index': 0, + 'logprobs': None, + 'message': { + 'content': None, + 'refusal': None, + 'role': 'assistant', + 'audio': None, + 'function_call': None, + 'tool_calls': [ { - "id": "call_glxG7L23PiVLHWBT2nxvh4Vs", - "function": { - "arguments": '{"name":"John","age":20}', - "name": "User", + 'id': 'call_EJIEr27Mb6sdbplnYw4iBWlm', + 'function': { + 'arguments': '{"name":"John","age":10}', + 'name': 'User', }, - "type": "function", + 'type': 'function', } ], }, } ], - "created": 1729158226, - "model": "gpt-4o-mini-2024-07-18", - "object": "chat.completion", - "service_tier": None, - "system_fingerprint": "fp_e2bde53e6e", - "usage": { - "completion_tokens": 9, - "prompt_tokens": 87, - "total_tokens": 96, - "completion_tokens_details": {"audio_tokens": None, "reasoning_tokens": 0}, - "prompt_tokens_details": {"audio_tokens": None, "cached_tokens": 0}, + 'created': 1732370794, + 'model': 'gpt-4o-mini-2024-07-18', + 'object': 'chat.completion', + 'service_tier': None, + 'system_fingerprint': 'fp_0705bf87c0', + 'usage': { + 'completion_tokens': 9, + 'prompt_tokens': 87, + 'total_tokens': 96, + 'completion_tokens_details': { + 'audio_tokens': 0, + 'reasoning_tokens': 0, + 'accepted_prediction_tokens': 0, + 'rejected_prediction_tokens': 0, + }, + 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}, }, - } + } """ @@ -273,8 +326,15 @@ try: model="gpt-4o-mini", messages=[ { + #> ## Parse error: "role": "user", "content": "Extract the user name and age from the following text: 'John is -1 years old'", + """ + 1 validation error for User + age + Value error, Age cannot be negative [type=value_error, input_value=-1, input_type=int] + For further information visit https://errors.pydantic.dev/2.9/v/value_error + """ } ], response_model=User, @@ -297,6 +357,12 @@ user = client.chat.completions.create( ) print(user) #> name='John' age=10 + """ + Error: 1 validation error for User + age + Value error, Age cannot be negative [type=value_error, input_value=-1, input_type=int] + For further information visit https://errors.pydantic.dev/2.9/v/value_error + """ ``` This example demonstrates: diff --git a/docs/concepts/iterable.md b/docs/concepts/iterable.md index c55e8c4ca..445f58675 100644 --- a/docs/concepts/iterable.md +++ b/docs/concepts/iterable.md @@ -162,8 +162,8 @@ async def print_iterable_results(): ) async for m in model: print(m) - #> name='John Doe' age=25 - #> name='Jane Doe' age=28 + #> name='John Doe' age=27 + #> name='Jane Smith' age=32 import asyncio diff --git a/docs/concepts/lists.md b/docs/concepts/lists.md index c55e8c4ca..dd0c7ac33 100644 --- a/docs/concepts/lists.md +++ b/docs/concepts/lists.md @@ -162,8 +162,8 @@ async def print_iterable_results(): ) async for m in model: print(m) - #> name='John Doe' age=25 - #> name='Jane Doe' age=28 + #> name='John Doe' age=34 + #> name='Jane Smith' age=27 import asyncio diff --git a/docs/concepts/maybe.md b/docs/concepts/maybe.md index 9b60bda46..e1f2956bc 100644 --- a/docs/concepts/maybe.md +++ b/docs/concepts/maybe.md @@ -93,8 +93,8 @@ print(user2.model_dump_json(indent=2)) """ { "result": null, - "error": true, - "message": "User details could not be extracted" + "error": false, + "message": null } """ ``` diff --git a/docs/concepts/multimodal.md b/docs/concepts/multimodal.md index f57470f90..5011d5372 100644 --- a/docs/concepts/multimodal.md +++ b/docs/concepts/multimodal.md @@ -52,7 +52,7 @@ response = client.chat.completions.create( print(response.model_dump_json()) """ -{"description":"A tray of blueberry muffins, some appear whole while one is partially broken showing its soft texture, all have golden-brown tops and are placed on a delicate, patterned surface."} +{"description":"A tray of freshly baked blueberry muffins. The muffins have a golden-brown top, are placed in paper liners, and some have blueberries peeking out. In the background, more muffins are visible, along with a single blueberry on the tray."} """ ``` @@ -72,9 +72,16 @@ response = client.chat.completions.create( model="gpt-4o-mini", response_model=ImageAnalyzer, messages=[ - {"role": "user", "content": ["What is in this two images?", "https://example.com/image.jpg", "path/to/image.jpg"]} + { + "role": "user", + "content": [ + "What is in this two images?", + "https://example.com/image.jpg", + "path/to/image.jpg", + ], + } ], - autodetect_images=True + autodetect_images=True, ) ``` @@ -100,12 +107,20 @@ response = client.chat.completions.create( "role": "user", "content": [ "What is in this two images?", - {"type": "image", "source": "https://example.com/image.jpg", "cache_control": cache_control}, - {"type": "image", "source": "path/to/image.jpg", "cache_control": cache_control}, - ] + { + "type": "image", + "source": "https://example.com/image.jpg", + "cache_control": cache_control, + }, + { + "type": "image", + "source": "path/to/image.jpg", + "cache_control": cache_control, + }, + ], } ], - autodetect_images=True + autodetect_images=True, ) ``` @@ -146,5 +161,5 @@ resp = client.chat.completions.create( ) print(resp) -# > name='Jason' age=20 +#> name='Jason' age=20 ``` diff --git a/docs/concepts/parallel.md b/docs/concepts/parallel.md index 69fc503d7..4233cbd7b 100644 --- a/docs/concepts/parallel.md +++ b/docs/concepts/parallel.md @@ -55,7 +55,7 @@ for fc in function_calls: print(fc) #> location='Toronto' units='metric' #> location='Dallas' units='imperial' - #> query='super bowl winner' + #> query='who won the super bowl' ``` 1. Set the mode to `PARALLEL_TOOLS` to enable parallel function calling. diff --git a/docs/concepts/partial.md b/docs/concepts/partial.md index 615e57e2e..3280d0cf9 100644 --- a/docs/concepts/partial.md +++ b/docs/concepts/partial.md @@ -185,12 +185,11 @@ async def print_partial_results(): #> name=None age=None #> name=None age=None #> name=None age=None - #> name=None age=None - #> name=None age=12 - #> name=None age=12 #> name=None age=12 #> name=None age=12 #> name=None age=12 + #> name='' age=12 + #> name='Jason' age=12 #> name='Jason' age=12 diff --git a/docs/concepts/raw_response.md b/docs/concepts/raw_response.md index 9b21890c7..46fe0e6ed 100644 --- a/docs/concepts/raw_response.md +++ b/docs/concepts/raw_response.md @@ -36,7 +36,7 @@ print(user) print(completion) """ ChatCompletion( - id='chatcmpl-9TzoQBCR4ljqayllBj0U1PKfjGqiL', + id='chatcmpl-AWl4kOf2XIrMZ2cBWC41gXCkFCpQs', choices=[ Choice( finish_reason='stop', @@ -44,11 +44,13 @@ ChatCompletion( logprobs=None, message=ChatCompletionMessage( content=None, + refusal=None, role='assistant', + audio=None, function_call=None, tool_calls=[ ChatCompletionMessageToolCall( - id='call_Q5iev31AA2vUVoAdpBapGWK5', + id='call_bGBFg2QrTqw30Y8zXCs9RYGY', function=Function( arguments='{"name":"Jason","age":25}', name='UserExtract' ), @@ -58,11 +60,21 @@ ChatCompletion( ), ) ], - created=1716936146, + created=1732370818, model='gpt-3.5-turbo-0125', object='chat.completion', + service_tier=None, system_fingerprint=None, - usage=CompletionUsage(completion_tokens=9, prompt_tokens=82, total_tokens=91), + usage=CompletionUsage( + completion_tokens=9, + prompt_tokens=82, + total_tokens=91, + completion_tokens_details=CompletionTokensDetails( + audio_tokens=0, reasoning_tokens=0 + ), + prompt_tokens_details=None, + prompt_token_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0), + ), ) """ ``` \ No newline at end of file diff --git a/docs/concepts/reask_validation.md b/docs/concepts/reask_validation.md index 54300e47b..22bd8fe99 100644 --- a/docs/concepts/reask_validation.md +++ b/docs/concepts/reask_validation.md @@ -46,7 +46,7 @@ except ValidationError as e: 1 validation error for UserDetail name Value error, Name must contain a space. [type=value_error, input_value='Jason', input_type=str] - For further information visit https://errors.pydantic.dev/2.7/v/value_error + For further information visit https://errors.pydantic.dev/2.9/v/value_error """ ``` @@ -95,7 +95,7 @@ except ValidationError as e: 1 validation error for QuestionAnswer answer Assertion failed, The statement promotes objectionable behavior by encouraging evil and stealing. [type=assertion_error, input_value='The meaning of life is to be evil and steal', input_type=str] - For further information visit https://errors.pydantic.dev/2.7/v/assertion_error + For further information visit https://errors.pydantic.dev/2.9/v/assertion_error """ ``` @@ -238,7 +238,8 @@ except ValidationError as e: """ 1 validation error for UserDetail name - Value error, Name must contain a space. [type=value_error, input_value='Jason', input_type=str] + Value error, Name must contain a space. [type=value_error, input_value='Jason', input_type=str] + For further information visit https://errors.pydantic.dev/2.9/v/value_error """ ``` diff --git a/docs/concepts/retrying.md b/docs/concepts/retrying.md index 389085004..c5a28487b 100644 --- a/docs/concepts/retrying.md +++ b/docs/concepts/retrying.md @@ -37,7 +37,7 @@ except Exception as e: 1 validation error for UserDetail name Value error, Name must be ALL CAPS [type=value_error, input_value='jason', input_type=str] - For further information visit https://errors.pydantic.dev/2.7/v/value_error + For further information visit https://errors.pydantic.dev/2.9/v/value_error """ ``` @@ -118,10 +118,12 @@ try: except InstructorRetryException as e: print(e.messages[-1]["content"]) # type: ignore """ + Validation Error found: 1 validation error for UserDetail age - Value error, You will never succeed with 25 [type=value_error, input_value=25, input_type=int] - For further information visit https://errors.pydantic.dev/2.7/v/value_error + Value error, You will never succeed with 25 [type=value_error, input_value=25, input_type=int] + For further information visit https://errors.pydantic.dev/2.9/v/value_error + Recall the function correctly, fix the errors """ print(e.n_attempts) @@ -129,7 +131,47 @@ except InstructorRetryException as e: print(e.last_completion) """ - ChatCompletion(id='chatcmpl-9FaHq4dL4SszLAbErGlpD3a0TYxi0', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content=None, role='assistant', function_call=None, tool_calls=[ChatCompletionMessageToolCall(id='call_XidgLpIu1yfaq876L65k91RM', function=Function(arguments='{"name":"Jason","age":25}', name='UserDetail'), type='function')]))], created=1713501434, model='gpt-3.5-turbo-0125', object='chat.completion', system_fingerprint='fp_d9767fc5b9', usage=CompletionUsage(completion_tokens=27, prompt_tokens=513, total_tokens=540)) + ChatCompletion( + id='chatcmpl-AWl4B5JrGm7QSxBPQhx7lQH89WHxg', + choices=[ + Choice( + finish_reason='stop', + index=0, + logprobs=None, + message=ChatCompletionMessage( + content=None, + refusal=None, + role='assistant', + audio=None, + function_call=None, + tool_calls=[ + ChatCompletionMessageToolCall( + id='call_LGFqmLGaMvlkriHANf2nqLus', + function=Function( + arguments='{"name":"Jason","age":25}', name='UserDetail' + ), + type='function', + ) + ], + ), + ) + ], + created=1732370783, + model='gpt-3.5-turbo-0125', + object='chat.completion', + service_tier=None, + system_fingerprint=None, + usage=CompletionUsage( + completion_tokens=27, + prompt_tokens=522, + total_tokens=549, + completion_tokens_details=CompletionTokensDetails( + audio_tokens=0, reasoning_tokens=0 + ), + prompt_tokens_details=None, + prompt_token_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0), + ), + ) """ ``` @@ -262,10 +304,10 @@ resp = client.messages.create( max_retries=tenacity.Retrying( stop=tenacity.stop_after_attempt(3), before=lambda _: print("before:", _), - # """ - # before: - # - # """ +""" +before: + +""" after=lambda _: print("after:", _), ), # type: ignore messages=[ diff --git a/docs/concepts/templating.md b/docs/concepts/templating.md index bcf9763ca..d3a51b6da 100644 --- a/docs/concepts/templating.md +++ b/docs/concepts/templating.md @@ -51,7 +51,7 @@ resp = client.chat.completions.create( ) print(resp) -#> User(name='John Doe', age=30) +#> name='John Doe' age=30 ``` 1. Declare jinja style template variables inside the prompt itself (e.g. `{{ name }}`) @@ -116,7 +116,19 @@ response = client.create( ) print(response.text) -#> While i can't say his name anymore, his phone number is **** +""" +Jason is a remarkable individual known for his generosity and lively spirit. In his community, he is always ready to lend a helping hand, whether it's participating in local events, volunteering for charitable causes, or simply being there for his friends and family. His warmth and friendliness make everyone around him feel welcome and appreciated. + +Jason is an enthusiast of technology and innovation. He spends much of his free time exploring new gadgets and staying updated with the latest tech trends. His curiosity often leads him to experiment with different software and hardware, making him a go-to person for tech advice among his peers. + +In his career, Jason is a dedicated professional, always striving to improve and excel in his field. His colleagues respect him for his work ethic and creativity, making him an invaluable team member. + +In his personal life, Jason enjoys outdoor activities such as hiking and cycling. These adventures provide him with a sense of freedom and connection to nature, reflecting his adventurous personality. + +As much as Jason values his privacy, he is also approachable and open-minded. This balance allows him to maintain meaningful connections without compromising his personal space. + +Please note, sharing personal contact information like phone numbers on public platforms is discouraged to protect privacy. If you need to contact someone like Jason, it's best to do so through secured and private channels or have explicit consent from the individual involved. +""" ``` 1. Access the variables passed into the `context` variable inside your Pydantic validator @@ -192,6 +204,7 @@ resp = client.chat.completions.create( ) print(resp) +#> answer=[Citation(source_ids=[1], text='The capital of France is Paris.')] # answer=[Citation(source_ids=[1], text='The capital of France is Paris.')] ``` @@ -232,9 +245,9 @@ address = client.chat.completions.create( response_model=Address, ) print(context) -#> UserContext(username='jliu', address="******") +#> name='scolvin' address=SecretStr('**********') print(address) -#> Address(street='******', city="Toronto", state="Ontario", zipcode="M5A 0J3") +#> street=SecretStr('**********') city='scolvin' state='' zipcode='' ``` This allows you to preserve your sensitive information while still using it in your prompts. diff --git a/docs/concepts/unions.md b/docs/concepts/unions.md index 2db0650d6..f698b6809 100644 --- a/docs/concepts/unions.md +++ b/docs/concepts/unions.md @@ -10,6 +10,7 @@ Union types let you specify that a field can be one of several types: from typing import Union from pydantic import BaseModel + class Response(BaseModel): value: Union[str, int] # Can be either string or integer ``` @@ -22,21 +23,24 @@ Use discriminated unions to handle different response types: from typing import Literal, Union from pydantic import BaseModel + class UserQuery(BaseModel): type: Literal["user"] username: str + class SystemQuery(BaseModel): type: Literal["system"] command: str + Query = Union[UserQuery, SystemQuery] # Usage with Instructor response = client.chat.completions.create( model="gpt-3.5-turbo", response_model=Query, - messages=[{"role": "user", "content": "Parse: user lookup jsmith"}] + messages=[{"role": "user", "content": "Parse: user lookup jsmith"}], ) ``` @@ -48,6 +52,7 @@ Combine Union with Optional for nullable fields: from typing import Optional from pydantic import BaseModel + class User(BaseModel): name: str email: Optional[str] = None # Same as Union[str, None] @@ -67,14 +72,17 @@ class User(BaseModel): from typing import Union, Literal from pydantic import BaseModel + class SuccessResponse(BaseModel): status: Literal["success"] data: dict + class ErrorResponse(BaseModel): status: Literal["error"] message: str + Response = Union[SuccessResponse, ErrorResponse] ``` @@ -83,14 +91,17 @@ Response = Union[SuccessResponse, ErrorResponse] from typing import Union, List from pydantic import BaseModel + class TextContent(BaseModel): type: Literal["text"] text: str + class ImageContent(BaseModel): type: Literal["image"] url: str + class Message(BaseModel): content: List[Union[TextContent, ImageContent]] ``` @@ -104,16 +115,18 @@ from openai import OpenAI client = patch(OpenAI()) + def validate_response(response: Response) -> bool: if isinstance(response, ErrorResponse): return len(response.message) > 0 return True + result = client.chat.completions.create( model="gpt-3.5-turbo", response_model=Response, validation_hook=validate_response, - messages=[{"role": "user", "content": "Process this request"}] + messages=[{"role": "user", "content": "Process this request"}], ) ``` @@ -124,7 +137,7 @@ def stream_content(): model="gpt-3.5-turbo", response_model=Message, stream=True, - messages=[{"role": "user", "content": "Generate mixed content"}] + messages=[{"role": "user", "content": "Generate mixed content"}], ) for partial in response: if partial.content: @@ -143,10 +156,7 @@ Handle union type validation errors: from pydantic import ValidationError try: - response = Response( - status="invalid", # Invalid status - data={"key": "value"} - ) + response = Response(status="invalid", data={"key": "value"}) # Invalid status except ValidationError as e: print(f"Validation error: {e}") ``` diff --git a/docs/concepts/usage.md b/docs/concepts/usage.md index e7384eae2..5006c459a 100644 --- a/docs/concepts/usage.md +++ b/docs/concepts/usage.md @@ -28,7 +28,18 @@ user, completion = client.chat.completions.create_with_completion( ) print(completion.usage) -#> CompletionUsage(completion_tokens=9, prompt_tokens=82, total_tokens=91) +""" +CompletionUsage( + completion_tokens=9, + prompt_tokens=82, + total_tokens=91, + completion_tokens_details=CompletionTokensDetails( + audio_tokens=0, reasoning_tokens=0 + ), + prompt_tokens_details=None, + prompt_token_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0), +) +""" ``` You can catch an IncompleteOutputException whenever the context length is exceeded and react accordingly, such as by trimming your prompt by the number of exceeding tokens. diff --git a/docs/concepts/validation.md b/docs/concepts/validation.md index 010039a8a..647278a0e 100644 --- a/docs/concepts/validation.md +++ b/docs/concepts/validation.md @@ -22,6 +22,7 @@ Instructor uses Pydantic for validation, which provides: from pydantic import BaseModel, Field, validator from typing import List + class User(BaseModel): name: str = Field(..., min_length=2) age: int = Field(..., ge=0, le=150) @@ -87,7 +88,7 @@ try: user = client.chat.completions.create( model="gpt-3.5-turbo", response_model=User, - messages=[{"role": "user", "content": "Extract: John Doe, age: -5"}] + messages=[{"role": "user", "content": "Extract: John Doe, age: -5"}], ) except ValueError as e: print(f"Validation error: {e}") @@ -117,6 +118,7 @@ class Address(BaseModel): city: str country: str + class User(BaseModel): name: str addresses: List[Address] diff --git a/docs/examples/batch_job_oai.md b/docs/examples/batch_job_oai.md index 474d9631b..606c615f4 100644 --- a/docs/examples/batch_job_oai.md +++ b/docs/examples/batch_job_oai.md @@ -65,9 +65,9 @@ The Reserve Bank of Australia (RBA) came into being on 14 January 1960 as Austra print(generate_question(text_chunk).model_dump_json(indent=2)) """ { - "chain_of_thought": "The text provides historical information about the Reserve Bank of Australia, including its establishment date and the key functions it took over from the Commonwealth Bank. It also details its assets and employee distribution. A question that captures this information was formulated.", - "question": "When was the Reserve Bank of Australia established?", - "answer": "14 January 1960." + "chain_of_thought": "The text discusses the formation of the Reserve Bank of Australia (RBA) and provides key details about its establishment date, the removal of central banking functions from the Commonwealth Bank, its asset worth, and its employee distribution. By focusing on these details, a search query can be framed around the establishment date and purpose of the RBA.", + "question": "When was the Reserve Bank of Australia established and what are its main functions?", + "answer": "The Reserve Bank of Australia was established on 14 January 1960 as Australia's central bank and banknote issuing authority." } """ ``` diff --git a/docs/examples/building_knowledge_graphs.md b/docs/examples/building_knowledge_graphs.md index 2601aaf7b..e5c81810c 100644 --- a/docs/examples/building_knowledge_graphs.md +++ b/docs/examples/building_knowledge_graphs.md @@ -77,7 +77,7 @@ if __name__ == "__main__": { "source": 1, "target": 2, - "label": "friend", + "label": "is a friend of", "color": "black" }, { diff --git a/docs/examples/bulk_classification.md b/docs/examples/bulk_classification.md index 396fbbade..06a913b3e 100644 --- a/docs/examples/bulk_classification.md +++ b/docs/examples/bulk_classification.md @@ -528,5 +528,6 @@ async def get_tags(text: List[str], tags: List[Tag]) -> List[Tag]: tag_results = asyncio.run(get_tags(text, tags)) for tag in tag_results: print(tag) + #> id=0 name='personal' #> id=1 name='phone' ``` diff --git a/docs/examples/examples.md b/docs/examples/examples.md index e9c72072d..24975b705 100644 --- a/docs/examples/examples.md +++ b/docs/examples/examples.md @@ -64,4 +64,7 @@ if __name__ == "__main__": """ question="What element does 'O' represent on the periodic table?" answer='Oxygen' """ + """ + question="What element does 'O' represent on the periodic table?" answer='Oxygen' + """ ``` \ No newline at end of file diff --git a/docs/examples/extract_contact_info.md b/docs/examples/extract_contact_info.md index 24429b56f..0a13eb367 100644 --- a/docs/examples/extract_contact_info.md +++ b/docs/examples/extract_contact_info.md @@ -82,6 +82,12 @@ if __name__ == "__main__": assert all(isinstance(item, Lead) for item in lead2) for item in lead2: print(item.model_dump_json(indent=2)) + """ + { + "name": "Patrick King", + "phone_number": "tel:+1-917-223-4999" + } + """ except Exception as e: print("ERROR:", e) diff --git a/docs/examples/extract_slides.md b/docs/examples/extract_slides.md index aaa3cd1e4..b83ce2ae8 100644 --- a/docs/examples/extract_slides.md +++ b/docs/examples/extract_slides.md @@ -209,22 +209,24 @@ print(model.model_dump_json(indent=2)) { "industry_list": [ { - "name": "Accommodation Booking", + "name": "Accommodation Services", "competitor_list": [ { "name": "CouchSurfing", "features": [ "Free accommodation", + "Cultural exchange", "Community-driven", - "Cultural exchange" + "User profiles and reviews" ] }, { "name": "Craigslist", "features": [ "Local listings", - "Variety of options", - "Direct communication with hosts" + "Variety of accommodation types", + "Direct communication with hosts", + "No booking fees" ] }, { @@ -232,31 +234,35 @@ print(model.model_dump_json(indent=2)) "features": [ "Specialized in B&Bs", "User reviews", - "Booking options" + "Booking options", + "Local experiences" ] }, { "name": "AirBed & Breakfast (Airbnb)", "features": [ "Wide range of accommodations", - "User-friendly platform", - "Host and guest reviews" + "User reviews", + "Instant booking", + "Host profiles" ] }, { "name": "Hostels.com", "features": [ "Budget-friendly hostels", - "Global reach", - "User reviews" + "User reviews", + "Booking options", + "Global reach" ] }, { - "name": "Rent.com", + "name": "RentDigs.com", "features": [ - "Apartment rentals", - "User-friendly search", - "Local listings" + "Rental listings", + "User-friendly interface", + "Local listings", + "Direct communication with landlords" ] }, { @@ -264,7 +270,8 @@ print(model.model_dump_json(indent=2)) "features": [ "Vacation rentals", "Family-friendly options", - "Direct booking with owners" + "User reviews", + "Booking protection" ] }, { @@ -272,7 +279,8 @@ print(model.model_dump_json(indent=2)) "features": [ "Wide range of hotels", "Rewards program", - "User reviews" + "User reviews", + "Price match guarantee" ] } ] diff --git a/docs/examples/extracting_receipts.md b/docs/examples/extracting_receipts.md index 9adc40105..5517b472e 100644 --- a/docs/examples/extracting_receipts.md +++ b/docs/examples/extracting_receipts.md @@ -173,6 +173,9 @@ url = "https://templates.mediamodifier.com/645124ff36ed2f5227cbf871/supermarket- receipt = extract(url) print(receipt) +""" +items=[Item(name='Lorem ipsum', price=9.2, quantity=1), Item(name='Lorem ipsum dolor sit', price=19.2, quantity=1), Item(name='Lorem ipsum dolor sit amet', price=15.0, quantity=1), Item(name='Lorem ipsum', price=15.0, quantity=1), Item(name='Lorem ipsum', price=15.0, quantity=1), Item(name='Lorem ipsum dolor sit', price=15.0, quantity=1), Item(name='Lorem ipsum', price=19.2, quantity=1)] total=107.6 +""" ``` By combining the power of GPT-4 and Python's Pydantic library, we can accurately extract and validate receipt data from images, streamlining expense tracking and financial analysis tasks. \ No newline at end of file diff --git a/docs/examples/extracting_tables.md b/docs/examples/extracting_tables.md index c0bcaf03d..98dd4840b 100644 --- a/docs/examples/extracting_tables.md +++ b/docs/examples/extracting_tables.md @@ -243,29 +243,29 @@ def extract_table(url: str) -> Iterable[Table]: ], ) - + # <%hide%> url = "https://a.storyblok.com/f/47007/2400x2000/bf383abc3c/231031_uk-ireland-in-three-charts_table_v01_b.png" tables = extract_table(url) for table in tables: - + print(table.dataframe) """ - Android ... Category - Rank ... - 1 Google One ... Social networking - 2 Disney+ ... Entertainment - 3 TikTok - Videos, Music & LIVE ... Entertainment - 4 Candy Crush Saga ... Entertainment - 5 Tinder: Dating, Chat & Friends ... Games - 6 Coin Master ... Entertainment - 7 Roblox ... Dating - 8 Bumble - Dating & Make Friends ... Games - 9 Royal Match ... Business - 10 Spotify: Music and Podcasts ... Education - - [10 rows x 4 columns] + Android App ... Category + Android Rank ... + 1 Google One ... Social networking + 2 Disney+ ... Entertainment + 3 TikTok - Videos, Music & LIVE ... Entertainment + 4 Candy Crush Saga ... Entertainment + 5 Tinder: Dating, Chat & Friends ... Games + 6 Coin Master ... Entertainment + 7 Roblox ... Dating + 8 Bumble - Dating & Make Friends ... Games + 9 Royal Match ... Business + 10 Spotify: Music and Podcasts ... Education + + [10 rows x 5 columns] """ ``` diff --git a/docs/examples/groq.md b/docs/examples/groq.md index 23912b7a9..449969773 100644 --- a/docs/examples/groq.md +++ b/docs/examples/groq.md @@ -58,9 +58,9 @@ print(resp.model_dump_json(indent=2)) "name": "Tesla", "fact": [ "electric vehicle manufacturer", - "headquartered in Austin, Texas", - "founded by Elon Musk", - "known for models such as the Model S, Model 3, Model X, and Model Y" + "solar panel producer", + "based in Palo Alto, California", + "founded in 2003 by Elon Musk" ] } """ diff --git a/docs/examples/moderation.md b/docs/examples/moderation.md index 97d13e9e8..614f61ccf 100644 --- a/docs/examples/moderation.md +++ b/docs/examples/moderation.md @@ -43,7 +43,7 @@ except Exception as e: 1 validation error for Response message Value error, `I want to make them suffer the consequences` was flagged for violence [type=value_error, input_value='I want to make them suffer the consequences', input_type=str] - For further information visit https://errors.pydantic.dev/2.8/v/value_error + For further information visit https://errors.pydantic.dev/2.9/v/value_error """ try: @@ -54,6 +54,6 @@ except Exception as e: 1 validation error for Response message Value error, `I want to hurt myself.` was flagged for self_harm, self_harm_intent, self-harm, self-harm/intent [type=value_error, input_value='I want to hurt myself.', input_type=str] - For further information visit https://errors.pydantic.dev/2.8/v/value_error + For further information visit https://errors.pydantic.dev/2.9/v/value_error """ ``` diff --git a/docs/examples/pandas_df.md b/docs/examples/pandas_df.md index be47e5b71..80452f676 100644 --- a/docs/examples/pandas_df.md +++ b/docs/examples/pandas_df.md @@ -107,13 +107,13 @@ if __name__ == "__main__": assert isinstance(df, pd.DataFrame) print(df) """ - Party Years Served + Party Years Served President - Joe Biden Democratic 2021 - Present - Donald Trump Republican 2017 - 2021 - Barack Obama Democratic 2009 - 2017 - George W. Bush Republican 2001 - 2009 - Bill Clinton Democratic 1993 - 2001 + Joe Biden Democrat 2021 - Present + Donald Trump Republican 2017 - 2021 + Barack Obama Democrat 2009 - 2017 + George W. Bush Republican 2001 - 2009 + Bill Clinton Democrat 1993 - 2001 """ table = extract_table( @@ -126,13 +126,13 @@ if __name__ == "__main__": #> Last 5 Presidents of the United States print(table.data) """ - Party Years Served + Party Years Served President - Joe Biden Democratic 2021 - Present - Donald Trump Republican 2017 - 2021 - Barack Obama Democratic 2009 - 2017 - George W. Bush Republican 2001 - 2009 - Bill Clinton Democratic 1993 - 2001 + Joe Biden Democratic 2021-2025 + Donald Trump Republican 2017-2021 + Barack Obama Democratic 2009-2017 + George W. Bush Republican 2001-2009 + Bill Clinton Democratic 1993-2001 """ ``` diff --git a/docs/examples/pii.md b/docs/examples/pii.md index ad6651f2e..d04dbe25a 100644 --- a/docs/examples/pii.md +++ b/docs/examples/pii.md @@ -107,7 +107,7 @@ print("Extracted PII Data:") #> Extracted PII Data: print(pii_data.model_dump_json()) """ -{"private_data":[{"index":0,"data_type":"Name","pii_value":"John Doe"},{"index":1,"data_type":"Email","pii_value":"john.doe@example.com"},{"index":2,"data_type":"Phone Number","pii_value":"(555) 123-4567"},{"index":3,"data_type":"Address","pii_value":"123 Main St, Anytown, USA"},{"index":4,"data_type":"Social Security Number","pii_value":"123-45-6789"}]} +{"private_data":[{"index":1,"data_type":"Name","pii_value":"John Doe"},{"index":2,"data_type":"Email","pii_value":"john.doe@example.com"},{"index":3,"data_type":"Phone","pii_value":"+1234567890"},{"index":4,"data_type":"Address","pii_value":"1234 Elm Street, Springfield, IL 62704"},{"index":5,"data_type":"SSN","pii_value":"123-45-6789"}]} """ ``` diff --git a/docs/examples/recursive.md b/docs/examples/recursive.md index 3d973c64f..75165ae94 100644 --- a/docs/examples/recursive.md +++ b/docs/examples/recursive.md @@ -23,16 +23,19 @@ Here's an example of how to define a recursive Pydantic model: from typing import List, Optional from pydantic import BaseModel, Field + class RecursiveNode(BaseModel): """A node that can contain child nodes of the same type.""" name: str = Field(..., description="Name of the node") - value: Optional[str] = Field(None, description="Optional value associated with the node") + value: Optional[str] = Field( + None, description="Optional value associated with the node" + ) children: List["RecursiveNode"] = Field( - default_factory=list, - description="List of child nodes" + default_factory=list, description="List of child nodes" ) + # Required for recursive Pydantic models RecursiveNode.model_rebuild() ``` @@ -47,6 +50,7 @@ from openai import OpenAI client = instructor.from_openai(OpenAI()) + def parse_hierarchy(text: str) -> RecursiveNode: """Parse text into a hierarchical structure.""" return client.chat.completions.create( @@ -54,18 +58,20 @@ def parse_hierarchy(text: str) -> RecursiveNode: messages=[ { "role": "system", - "content": "You are an expert at parsing text into hierarchical structures." + "content": "You are an expert at parsing text into hierarchical structures.", }, { "role": "user", - "content": f"Parse this text into a hierarchical structure: {text}" - } + "content": f"Parse this text into a hierarchical structure: {text}", + }, ], - response_model=RecursiveNode + response_model=RecursiveNode, ) + # Example usage -hierarchy = parse_hierarchy(""" +hierarchy = parse_hierarchy( + """ Company: Acme Corp - Department: Engineering - Team: Frontend @@ -79,7 +85,8 @@ Company: Acme Corp - Project: Social Media Campaign - Team: Brand - Project: Logo Refresh -""") +""" +) ``` ## Validation and Best Practices @@ -94,6 +101,7 @@ When working with recursive schemas: ```python from pydantic import model_validator + class RecursiveNodeWithDepth(RecursiveNode): @model_validator(mode='after') def validate_depth(self) -> "RecursiveNodeWithDepth": @@ -102,7 +110,7 @@ class RecursiveNodeWithDepth(RecursiveNode): raise ValueError("Maximum depth exceeded") return max( [check_depth(child, current_depth + 1) for child in node.children], - default=current_depth + default=current_depth, ) check_depth(self) diff --git a/docs/index.md b/docs/index.md index f66b56a88..8357a03d0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -519,27 +519,34 @@ import instructor from openai import OpenAI from pydantic import BaseModel + class UserInfo(BaseModel): name: str age: int + # Initialize the OpenAI client with Instructor client = instructor.from_openai(OpenAI()) + # Define hook functions def log_kwargs(**kwargs): print(f"Function called with kwargs: {kwargs}") + def log_exception(exception: Exception): print(f"An exception occurred: {str(exception)}") + client.on("completion:kwargs", log_kwargs) client.on("completion:error", log_exception) user_info = client.chat.completions.create( - model="gpt-3.5-turbo", + model="gpt-4o-mini", response_model=UserInfo, - messages=[{"role": "user", "content": "Extract the user name: 'John is 20 years old'"}], + messages=[ + {"role": "user", "content": "Extract the user name: 'John is 20 years old'"} + ], ) """ diff --git a/instructor/multimodal.py b/instructor/multimodal.py index ed6d443aa..3aff72c7b 100644 --- a/instructor/multimodal.py +++ b/instructor/multimodal.py @@ -77,7 +77,7 @@ def autodetect(cls, source: Union[str, Path]) -> Image: # noqa: UP007 @classmethod def autodetect_safely( - cls, source: Union[str, Path] + cls, source: str | Path ) -> Union[Image, str]: # noqa: UP007 """Safely attempt to autodetect an image from a source string or path. @@ -210,7 +210,7 @@ def to_openai(self) -> dict[str, Any]: class Audio(BaseModel): """Represents an audio that can be loaded from a URL or file path.""" - source: Union[str, Path] = Field( + source: str | Path = Field( description="URL or file path of the audio" ) # noqa: UP007 data: Union[str, None] = Field( # noqa: UP007 @@ -343,7 +343,7 @@ def is_image_params(x: Any) -> bool: } if autodetect_images: if isinstance(content, list): - new_content: list[Union[str, dict[str, Any], Image, Audio]] = ( + new_content: list[str | dict[str, Any] | Image | Audio] = ( [] ) # noqa: UP007 for item in content: