Types in, types out: extraction with FunctAI¶
Types in, types out: lists, dicts, enums, literals, dataclasses, pydantic models.
The return type of an AI function is a contract: the model is shown what shape to produce, and the reply is read back into that type (or the model is asked again). This page walks through the types you can use, from a list of strings to nested pydantic models.
Every output below is a real reply. This page is a notebook: open it in
Chattering and run it, or run it all with python/.venv/bin/python tools/docs.py run python/examples/typing_and_extraction/README.md.
import functai
functai.configure(lm="gpt-4.1-mini", temperature=0)
from functai import ai, _ai
Containers¶
@ai
def fruits(text: str) -> list[str]:
"""The fruits mentioned, in order."""
...
fruits("I'll go shopping for 2 apples, one orange and a dozen bananas.")
['apples', 'orange', 'bananas']
@ai
def quantities(text: str) -> dict[str, int]:
"""How many of each fruit, by fruit name."""
...
quantities("I'll go shopping for 2 apples, one orange and a dozen bananas.")
{'apples': 2, 'orange': 1, 'bananas': 12}
Post-processing with plain Python¶
_ai stands for the model’s answer, typed as the variable it is
assigned to. The rest of the body is ordinary Python:
@ai
def total_items(text: str) -> int:
"""The total number of items to buy."""
counts: dict[str, int] = _ai["How many of each item."]
return sum(counts.values())
total_items("I'll go shopping for 2 apples, one orange and a dozen bananas.")
15
The model returned counts; your code computed the total. predict
returns what the model produced instead of the function’s return value:
total_items.predict("2 apples, one orange and a dozen bananas")
Prediction(counts={'apples': 2, 'oranges': 1, 'bananas': 12})
Several outputs¶
Each _ai assignment is one output; return them however you like:
@ai
def critique_and_improve(text: str) -> tuple[str, str]:
"""Criticize the text constructively, then improve it."""
critique: str = _ai["What is unclear or impolite, in one sentence."]
improved: str = _ai["The improved text."]
return critique, improved
critique, improved = critique_and_improve("U should fix this asap, it's broken.")
print(critique)
print(improved)
The text is too informal and lacks politeness, which may come across as rude or demanding.
Could you please fix this as soon as possible? It appears to be broken. Thank you!
Choices: Enum and Literal¶
from enum import Enum
from typing import Literal
class Priority(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
@ai
def priority(issue: str) -> Priority:
"""How urgent the issue is."""
...
@ai
def sentiment(review: str) -> Literal["positive", "negative", "mixed"]:
"""The review's overall sentiment."""
...
priority("The production database is down."), sentiment("Great food, rude waiter.")
(<Priority.HIGH: 'high'>, 'mixed')
Dataclasses and nesting¶
from dataclasses import dataclass
@dataclass
class Address:
street: str
city: str
country: Literal["US", "CA"]
@dataclass
class Patient:
name: str # full name
age: int
address: Address | None # None when the note gives no address
@ai
def extract_patient(clinical_note: str) -> Patient:
"""Extract the patient's details from the clinical note."""
...
extract_patient("John Doe, 45, lives at 123 Main St, Anytown. US resident.")
Patient(name='John Doe', age=45, address=Address(street='123 Main St', city='Anytown', country='US'))
extract_patient("Seen today: Marie Tremblay, 62 years old. No address on file.")
Patient(name='Marie Tremblay', age=62, address=None)
Pydantic models, as outputs and as inputs¶
from pydantic import BaseModel, Field
class LineItem(BaseModel):
description: str
quantity: int
unit_price: float
class Invoice(BaseModel):
number: str
vendor: str
items: list[LineItem]
currency: str = Field(description="ISO 4217 code, e.g. USD")
@ai
def extract_invoice(document: str) -> Invoice:
"""Extract the invoice."""
...
invoice = extract_invoice("""
INVOICE INV-2025-101 from TechCorp Inc.
5 x Laptop @ 1000.00
2 x Monitor @ 300.00
All amounts in US dollars.
""")
invoice
Invoice(number='INV-2025-101', vendor='TechCorp Inc.', items=[LineItem(description='Laptop', quantity=5, unit_price=1000.0), LineItem(description='Monitor', quantity=2, unit_price=300.0)], currency='USD')
An input can be a model too; it is shown to the model as JSON:
@ai
def invoice_total(invoice: Invoice) -> float:
"""The invoice's total amount."""
...
invoice_total(invoice)
5600.0
(For arithmetic you’d use Python:
sum(i.quantity * i.unit_price for i in invoice.items) is exact and
free. Let the model do what code can’t.)
When the reply doesn’t fit¶
A reply that can’t be read into the type is sent back to the model once,
with what was wrong (retries=1 by default). Small misspellings of the
layout are forgiven and recorded:
p = extract_patient.predict("Jane Roe, 30, 5 Queen St, Toronto, Canada.")
p.result, p.repairs, p.attempts
(Patient(name='Jane Roe', age=30, address=Address(street='5 Queen St', city='Toronto', country='CA')),
[],
1)