Mr. GPT, please read me my emails: Structured Outputs in ChatGPT
Structured output from an email: Left email text, right structured YAML. ChatGPT’s API now has structured outputs. In short, this means you can get a JSON that complies with a schema as the response. Structured responses went from a favor to a promise.
We used to ask ChatGPT to give a JSON response, and it would — most of the time — plus some chatter before and after. It was valid JSON, but the schema compliance was loose at best.
Now, we can provide the schema, and ChatGPT will fit the response to it.
Let’s dive in.
It would be cool if ChatGPT could read my emails and just let me know what’s important. It could also figure out if there are any TODOs or events in the email. This is particularly important for children’s school emails. Schools send soooo much email, and you don’t want to be the parent who is not “in the know.”
We would like a response like this:
from pydantic import BaseModel, Field
from typing import Optional
class Event(BaseModel):
'''
Calendar event
'''
summary: str
description: str
start_iso_date_time: str
end_iso_date_time: str
location: str
class Todo(BaseModel):
'''
TODO task.
'''
description: str
details: str
deadline_iso_datetime: Optional[str]
class EmailAnalysis(BaseModel):
'''
Result of an analysis.
'''
sender: str
events: list[Event] | None
todos: list[Todo] | None
priority: str
summary: str
Getting that structured response from text seemed unthinkable a few years ago. (I’ve been trying to get everyone to email me in JSON, but that hasn’t worked). Can we do it with ChatGPT?
We are going to try. We can use the Gmail API to read my unread emails:
def authenticate_gmail():
creds = None
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.json', 'w') as token:
token.write(creds.to_json())
return creds
def get_unread_emails():
# Call the Gmail API
#from:@example.com
creds = authenticate_gmail()
service = build('gmail', 'v1', credentials=creds)
q = "is:unread"
results = service.users().messages().list(userId='me', labelIds=['INBOX'], q=q).execute()
messages = results.get('messages', [])
if not messages:
print('No new messages.')
return
print(f'You have {len(messages)} unread messages.')
for msg in messages:
msg = service.users().messages().get(userId='me', id=msg['id']).execute()
print([x['value'] for x in msg['payload']['headers'] if x['name'] in ['From','Date']])
email_content = get_email_content(msg)
yield email_content, msg['id'], [x['value'] for x in msg['payload']['headers'] if x['name'].lower() == 'from'][0], msg
Now that we can read the emails, not we need some json.
def parse_email(prompt, response_format = EmailAnalysis, system):
try:
response = client.beta.chat.completions.parse(
messages=[
{
"role": "system",
"content": system
},
{
"role": "user",
"content": prompt
}
],
model="gpt-4o-mini",#gpt-4o-mini
#tools=tools
response_format=response_format
)
print(f"Tokens used: {response.usage.total_tokens}")
return response.choices[0].message.parsed
except Exception as e:
print(f"Error: {str(e)}")
Notice how we pass the response_format to the library call. We are passing the Pydantic class we want the oputput to conform to. Neat? Very.
If we glue it all into a main.py.
#!/usr/bin/env python3
from utils import print_yaml_highlighted
from gmail import get_unread_emails, mark_email_as_read
from gpt import review
import json
emails_tuples = []
prompt = f"""
Extract the information from the email:
%s
Ignore label generic promotional emails as no-important. Label personal emails as important.
"""
for email, msg_id, sender, gmail_message in get_unread_emails():
full_prompt = prompt % (email,)
py_response = review(full_prompt, system="You are a helpful assistant processing emails")
response = json.loads(py_response.json())
print_yaml_highlighted(response)
The output is outstaindg. For eample from this email:
Dear Felipe,
I hope this message finds you well. I wanted to remind you that this Friday, we are asking all students to bring flowers for their teacher as part of our appreciation activity. Please ensure your child brings their flowers to class that morning.
Additionally, we would like to remind you of the parent-teacher conference scheduled for Friday at 10:00 AM. This is an important opportunity to discuss your child’s progress, and we hope you will be able to attend.
If you have any questions or concerns, please feel free to reach out.
The teacher
It extracted the following output:
1 sender: Teacher Jane <teacher@school.org>
2 events:
3 - summary: Parent-Teacher Conference
4 description: Discussion about child's progress.
5 start_iso_date_time: '2024-10-04T10:00:00-07:00'
6 end_iso_date_time: '2024-10-04T10:30:00-07:00'
7 location: Classroom
8 todos:
9 - description: Bring flowers for the teacher
10 details: Ensure your child brings flowers to class on Friday morning.
11 deadline_iso_datetime: '2024-10-04T09:00:00-07:00'
12 priority: important
13 summary: Reminder for flowers and parent-teacher conference on Friday.
Conclusions
The result was accurate and, as you can see, not that much code. My imagination goes wild with ways to build up an email assistant from this barebones example. With a few adjustments, I can stop asking questions at parent-teacher conferences that are answered with, “We sent that in an email.”
You may ask, “What’s the big deal?” The big deal is that now we can bridge between software and ChatGPT in a much more consistent way.
I really like that we can use Pydantic to specify the response structure. Great design choice by the OpenAI team.