Skip to main content

functai

Write a Python function. A language model does the work. You measure how well.

functai turns a typed Python function into a call to a language model. The function's name, docstring and types say what you want; the answer comes back as the type you asked for. Then you run it on a whole table, find out how often it is right, and make it better.

from typing import Literal
from dpyr import col
import functai
from functai import ai

@ai
def team(message: str) -> Literal["shipping", "billing", "product", "account"]:
    """Which team should answer this customer message?"""
    ...

team("I was charged twice for order B-2210, please fix this.")    # 'billing'

tickets = functai.datasets.tickets()                              # 80 labelled support messages
tickets.mutate(team=team(col.message))                            # a new column, one call per message
functai.evaluate(team, tickets, expected="category")              # how often it's right, with a range

Installation

pip install "functai[data]"      # Python 3.11+

Coming from 1.1? Upgrading says what changed.

With an API key in your environment (OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, …) or a Claude, ChatGPT or Copilot subscription, there's nothing to set up: functai picks a small model you can use and tells you which. To choose: functai.configure(lm="claude-haiku-4-5").

Programs you can describe, logs you can keep

Every program says what it takes and gives, as data, and a @module checks every call against it:

@functai.module
def support(message: str, tone: str = "kind") -> str:
    ...

support.interface          # {"description", "inputs": [...], "outputs": [...]}: served, saved, described
support(3)                 # 3 is bound to "3": a value is converted when its meaning is clear
support(None)              # InterfaceError (interface-input): null does not bind to {"type":"string"}
functai.describe("saved/") # what a saved program takes and gives, without loading it

The call log keeps what you allow, field by field, and a host's rule holds for every program it runs (log_content only ever removes):

with functai.configure(log_calls=True, log_content={"transcript": False}):
    summarize(transcript)  # no transcript in the record, nor anything that could quote it:
                           # the reasoning, the requests and replies, error messages

A call tree's events can be watched and kept while it runs, and read again elsewhere. An observer gets the kept form of every event (a list is complete when the call returns; a function runs in a thread of its own: functai.flush() waits for it). A required journal makes the call wait until its start, each tool call and its end are kept:

seen = []
store = functai.MemoryStore()      # or your own store: see functai.Store
functai.configure(observers=[seen], journal=functai.Journal(store, required=True, timeout=30))

try:
    answer = summarize(transcript)
except functai.JournalError as err:
    if err.code == "journal-end":             # the call ended; the journal did not confirm its end
        answer = err.outcome.get()            # what it did (its value, or it raises its error)
        if err.journal == "unknown":          # no answer came: it may have been kept
            err.settle(claim=True)            # "kept", "not-kept" or "another-end", for good
    else:
        raise                                 # journal-barrier: the code or a tool did not run

A store answers each append "kept" or "duplicate", or raises functai.EventRefused; any other answer is a refusal, and any other exception means no answer came (the event is sent again, with a growing pause). A barrier waits at most the journal's timeout; a store should time out its own I/O too. A store with extend(events) is sent every event through it, what waits as one batch; a subclass that overrides only append is sent every event through its append. At exit, FunctAI waits at most two seconds for observers and journals to catch up. In a process forked inside a call, calls start a tree of their own.

Conversations, plugins, tools that ask first, serving

A conversation is a program's calls that remember each other. The function is unchanged; the memory is the conversation's, kept where you say, and nothing in it is ever deleted:

chat = tutor.conversation("alex", store="tutoring/")   # the same line tomorrow opens it again
chat("Hi, I'm Alex.")
chat("What is 1/2 + 1/3?")
chat.turns[-1].saw                                      # what that answer was based on
chat.render("Why can't I add the bottoms?")             # the next request, nothing sent
other = chat.continue_from(chat.turns[0])               # a branch

A tool says what it does to the world, and a person can be asked before it runs: at once, or later, from any process (the turn waits, saved, and goes on without paying for a model answer twice):

@functai.tool(effects="changes")
def refund(order: str, amount: float) -> str: ...

chat = assistant.conversation(customer, store=STORE, approve="changes")
try:
    chat("Refund my late parcel, please.")
except functai.Waiting as w:
    ...                                                 # later, anywhere: w.turn.approve(w.approvals[0])

Plugins change what programs do through a few hooks, and every change is recorded as data, so a rated answer is still asked again as it was:

review = functai.Plugin("review-mode", version="1.0.0")

@review.before_call
def careful(call):
    return functai.Change(sections=["Only point out problems."], tools=["read_file"])

chat = assistant.conversation("work", plugins=[review, functai.compaction(keep=20)])

Approval is one of them; long conversations are summarized by functai.compaction; functai.delegate(program) hands work to another program in a conversation of its own.

A program is served with functai serve saved/ --keys keys.txt (or functai.serve(program)), to callers who see only its boundary; on their side, functai.remote(url, key=...) is a program again. Replies can be kept on disk (configure(cache_replies="disk")), so a long fn.map(rows, threads=8) resumes by being run again. Rated turns become rows that keep their earlier turns (functai.rated), for evaluate and the optimizers.

Each has its page: Memory, Tools, Plugins, Serve it over HTTP, Big tables and Watch it being written.

Documentation

maximerivest.github.io/functai, with three ways in:

Then eight tutorials, from a first function to decision models and a model you own; the examples, each solving one problem end to end; and the reference.

functai also exists in TypeScript, R and Julia: the same function has the same version in each, and a function saved here loads there.

Built on

lm15 (every provider, no SDKs), lmcc (how values are written into prompts and read back) and dpyr (tables).

Development

This folder is the Python package; the repository around it holds the contract every language's FunctAI follows, the other languages, and the website. In this folder: uv sync --all-extras --all-groups, then uv run pytest (offline, a fake provider). Against real models (costs cents, needs model keys): .venv/bin/python tests/docs_live.py runs this README's code, and --render also every page of the website.

Metadata

Release files for functai 1.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for functai 1.2.0
File Size Uploaded
functai-1.2.0.tar.gz 424.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for functai 1.2.0
File Interpreter ABI Platform
functai-1.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 894.2 kB

Release files / functai-1.2.0.tar.gz

Download URL functai-1.2.0.tar.gz
Size 424.3 kB
Tags Source
SHA-256 checksum
How to use checksums
234dfbd40122ca1cb0e00e081b0d09382a455d3e14423687d7a933fed34fc977
BLAKE2b-256 checksum
How to use checksums
a974cff497beb3a03aaaef2fa7d7654ead720d4289d4bb84f36cb06ac688b723
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release files / functai-1.2.0-py3-none-any.whl

Download URL functai-1.2.0-py3-none-any.whl
Size 470.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
febb675f2b8b8a7458d542b98e2f12603a13a00f40cd3e8bca7b362a6760a5a8
BLAKE2b-256 checksum
How to use checksums
8a68e98b6aca1379d4695a0f22951d7bdc20a87c02203b50cca6523ca53b6dc3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.2.0 This release

2 release files

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.9.0

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page