Metadata-Version: 2.5
Name: functai
Version: 1.2.0
Summary: Typed Python functions whose body is a language model call: the function is the prompt, the body is the program
Project-URL: Homepage, https://github.com/maximerivest/functai
Project-URL: Bug Tracker, https://github.com/maximerivest/functai/issues
Project-URL: Documentation, https://maximerivest.github.io/functai/python.html
Project-URL: Changelog, https://maximerivest.github.io/functai/news.html
Project-URL: Source, https://github.com/maximerivest/functai
Author-email: Maxime Rivest <maxime.rivest@gmail.com>
License: MIT
License-File: LICENSE
Keywords: ai,decorators,function-decorators,llm,lm15,lmcc,prompt-engineering,prompt-optimization,structured-output
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: lm15<2,>=1.0.1
Requires-Dist: lmcc<0.9,>=0.8.5
Provides-Extra: bake
Requires-Dist: accelerate>=1.4; extra == 'bake'
Requires-Dist: bitsandbytes>=0.45; (sys_platform != 'darwin') and extra == 'bake'
Requires-Dist: datasets>=4.7; extra == 'bake'
Requires-Dist: peft>=0.17; extra == 'bake'
Requires-Dist: safetensors>=0.4; extra == 'bake'
Requires-Dist: torch>=2.4; extra == 'bake'
Requires-Dist: transformers>=4.57; extra == 'bake'
Requires-Dist: trl<1.15,>=1.14; extra == 'bake'
Provides-Extra: data
Requires-Dist: dpyr>=1.10; extra == 'data'
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Provides-Extra: fast
Requires-Dist: flash-linear-attention>=0.4; (sys_platform == 'linux') and extra == 'fast'
Requires-Dist: kernels<0.17,>=0.16; (sys_platform == 'linux') and extra == 'fast'
Requires-Dist: liger-kernel>=0.6; (sys_platform == 'linux') and extra == 'fast'
Provides-Extra: prime
Requires-Dist: verifiers==0.3.2.dev136; extra == 'prime'
Provides-Extra: tinker
Requires-Dist: pyarrow>=15; extra == 'tinker'
Requires-Dist: tinker<0.32,>=0.31; extra == 'tinker'
Requires-Dist: transformers>=4.57; extra == 'tinker'
Description-Content-Type: text/markdown

# 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.

```python
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

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

Coming from 1.1? [Upgrading](https://maximerivest.github.io/functai/articles/upgrading.html)
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:

```python
@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):

```python
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:

```python
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:

```python
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):

```python
@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:

```python
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](https://maximerivest.github.io/functai/articles/memory.html),
[Tools](https://maximerivest.github.io/functai/articles/tools.html),
[Plugins](https://maximerivest.github.io/functai/articles/plugins.html),
[Serve it over HTTP](https://maximerivest.github.io/functai/articles/serving.html),
[Big tables](https://maximerivest.github.io/functai/articles/tables.html#long-runs)
and [Watch it being written](https://maximerivest.github.io/functai/articles/streaming.html).

## Documentation

**[maximerivest.github.io/functai](https://maximerivest.github.io/functai/python.html)**, with three ways in:

- **[I have a table of text](https://maximerivest.github.io/functai/get-started.html)**: label, sort or score every row, check it, make it better.
- **[I have notes or documents](https://maximerivest.github.io/functai/articles/notes-to-data.html)**: pull the facts out as columns, following your protocol.
- **[I have a prompt that works](https://maximerivest.github.io/functai/articles/from-a-prompt.html)**: send it exactly as it is, then add types, tables and tests.

Then [eight tutorials](https://maximerivest.github.io/functai/tutorials/index.html),
from a first function to decision models and a model you own; the
[examples](https://maximerivest.github.io/functai/examples/index.html), each
solving one problem end to end; and the
[reference](https://maximerivest.github.io/functai/reference/index.html).

functai also exists in [TypeScript, R and
Julia](https://maximerivest.github.io/functai/#what-each-language-has): the
same function has the same version in each, and a function saved here loads
there.

## Built on

[lm15](https://github.com/lm15-dev/lm15-python) (every provider, no SDKs),
[lmcc](https://github.com/MaximeRivest/lmcc) (how values are written into
prompts and read back) and [dpyr](https://github.com/MaximeRivest/dpyr)
(tables).

## Development

This folder is the Python package; the [repository around
it](https://github.com/MaximeRivest/functai) 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.
