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:
- I have a table of text: label, sort or score every row, check it, make it better.
- I have notes or documents: pull the facts out as columns, following your protocol.
- I have a prompt that works: send it exactly as it is, then add types, tables and tests.
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)
| File | Size | Uploaded | |
|---|---|---|---|
| functai-1.2.0.tar.gz | 424.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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