Skip to content

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)