Upgrading¶
From 1.1 to 1.2¶
What changes when you move from FunctAI 1.1 to 1.2.
import functai
from functai import ai, module
Most of what is new adds to the API: conversations, tools that ask first, serving, plugins, a new bake. These are the changes that need you to change code, then the ones that change what your code does without any change.
Code to change¶
| 1.1 | now |
|---|---|
@ai(stateful=True), state_window=n, fn.history, fn.reset(), module.history |
a conversation: chat = fn.conversation("alex"), called like the function; context=functai.last_turns(n) for a window; chat.turns for the history; a new conversation (another id) to start over. The function itself remembers nothing (Memory) |
fn(x, all=True) |
fn.predict(x) (an input may now be called all) |
fn.opt(trainset=rows) changes fn; fn.undo_opt() reverts |
better = fn.opt(rows) is an improved copy; fn is unchanged, so evaluate(fn, ...) and evaluate(better, ...) compare side by side |
fn.programs(), fn.latest_program() |
fn.optimization_runs() says how a copy was made; fn.trials holds a search's candidates |
fn.opt(trainset=rows, optimizer=LabeledFewShot(k=8)) |
also by name: functai.labeled_few_shot(fn, rows, k=8), functai.bootstrap_few_shot(fn, rows, teacher=...), functai.gepa(fn, rows, teacher=...) |
module.opt(...) changes the AI functions it calls |
an improved copy of the module; the AI functions themselves are unchanged (better.state() shows what the copy runs with) |
from functai import * brings helpers (flexiclass, sig2str, settings...) |
it brings the API only; helpers are functai.flexiclass, functai.settings, ... |
| a body that is only a docstring | still works; write ... after the docstring so Pyright (VS Code) accepts the function, or return _ai, which mypy accepts too |
def f(x, /) (positional-only) in @ai or @module |
refused when defined: a program's inputs are given by name (a row of data, another language). Remove the / |
a @module annotated with a type defined further down |
refused when defined: define the type first |
fn.map(rows, num_threads=8) |
threads=8 (num_threads still works) |
| sync calls only | await fn.acall(x), await fn.apredict(x), and @ai async def for a function whose body is the model call |
Settings that no longer exist are refused by name (TypeError: ... unknown
setting(s) ['stateful']), so nothing is silently ignored.
Baked models. Bake was rebuilt (Bake it into a small model):
- A model baked with 1.1 is refused, a head included (
baked.jsonformat 1): bake it again. A saved program that names one cannot load until then. fn.bake(rows)now decides what to train (method="auto"): a head when every output has a fixed set of answers, as before; for any other function, a generative student, where 1.1 refused. That needspip install "functai[bake]"and a GPU here, or a training service you have set up ("functai[tinker]");plan_only=Truesays what it would do and what it would cost, and spends nothing.- The teacher is no longer run again on the test rows by default
(
compare_teacher=Trueto compare).
Inputs are checked, and converted when the meaning is clear¶
Every call, of an AI function or a module, now binds each input to its
declared type before anything else runs. A value whose meaning is clear
is converted; anything else is refused with functai.InterfaceError
(code interface-input), and nothing is sent:
@module
def double(n: int) -> int:
return n * 2
double("5"), double(5.0)
(10, 10)
try:
double("five")
except functai.InterfaceError as error:
print(error.code, error.field, "·", error)
interface-input n · double: input 'n': "five" does not bind to {"type":"integer"}
- Text takes a number (
42is"42"),True/False, a list or a record (as JSON), or a value with a text of its own (a date, a data frame);<object at 0x…>is refused. An integer takes5.0and"5"; a number takes"2.5"; a boolean onlyTrueorFalse. - A record input keeps only the members it declares.
- A missing value (
None,NaN, pandas'NA) for an optional input is that input left out: it gets its default. That is what a table's empty cell now means. - Outputs are checked, never converted, and a record holds only its fields: a model's reply with another member does not fit, and is asked again.
InterfaceErroris aTypeErrorand aValueError, so anexcept TypeErroryou already have still catches it.
What changes without a code change¶
- Versions. Every function with a default, and every module, gets a
new
versiononce (a default now counts by what is written, a module's version includes its interface). The call log's Versions shows a new one. Ratings are matched by signature, which defaults do not change, so they still apply. - Ratings made with 1.1 in a notebook.
rated(fn)now tells two notebooks' functions of the same name apart by the notebook's file. A call logged by 1.1 from a notebook names another file (the kernel's cell file), so some of your earlier ratings may be missing fromrated(fn):functai.rated(fn, any_file=True)takes them from every file. - Who rated. A rating made without
by=is recorded under the computer's account, and kept on its own: on a shared account, one person's rating no longer replaces another's (a disagreement is markeddisputed). - A reply cut off at the token limit is sent again with twice
max_tokensonly when you setmax_tokens. Without it, the reply already had the model's whole limit, so it fails at once, and the error says how many tokens went to thinking and what to change. - Models that only run at temperature 1 (GPT-6; Claude Opus, Sonnet,
Fable and Mythos 5) no longer fail when you set
temperature=0: the setting is left out of their requests, with one warning. - The reply cache never keeps a reply that could not be read, and the optimizers' teacher is never answered from it.
- A tool that returns a record (a dataclass, a pydantic model) is
shown to the model as JSON, not as Python's
str(). - Error messages quote the value at fault (cut after 80
characters), unless
log_contentkeeps that field out of the log. - The call log is format 2. FunctAI reads both formats; if you parse
the JSON lines yourself, records have new fields (
program.interface,saw,request_hash...):contract/calls.mdsays each.
From 0.x¶
What changed in 1.0, and how to move code over.
1.0 kept the way you write functions (@ai, _ai, configure,
all=True, stateful, tools, module="cot", .opt, undo_opt,
@module, phistory); some of these have changed since (see From 1.1
above). What runs underneath is new: prompts are laid out
by lmcc and sent by
lm15. Data, metrics and
optimizers are new too.
| 0.x | 1.0 |
|---|---|
configure(lm=<an LM object>) |
configure(lm="gpt-4.1"): a model name (litellm spellings work) |
training data as Example(...).with_inputs(...) |
a dict per row, or a table; columns named like the parameters are the inputs |
metric(example, pred, trace=None) |
metric(row, prediction), or a dpyr expression |
| optimizer classes from other libraries | functai.BootstrapFewShot, functai.InstructionSearch, … |
| an evaluator returning a percentage | functai.evaluate(program, data, metric): .score (0 to 1, with an interval), .table |
adapter="json", adapter="chat" |
same names, now lmcc layouts |
| custom adapter classes | template=[system(...), turns(), user(...)], or an lmcc.Adapter |
| tools switched the program to an agent module | tools run in a tool loop; the prompt does not change |
fn.signature |
an lmcc SignatureCore |
| exporting the program to another framework | removed; fn.state(), fn.save(path), functai.save |
stateful history in a separate object |
lmcc turns in fn.history (since replaced by conversations) |
Behaviour changes in 1.0¶
- Automatic instruction writing is opt-in. In 0.x every new function
asked the model to rewrite its own instruction (
autoinstruct), and the first calls refined it again. That spent money at import time and made prompts change by themselves. Now@ai(autoinstruct=True)or@ai(instruction_autorefine_calls=2)turns them on; they run at the first call, not at definition. - Prompts are laid out by lmcc, so their text differs from 0.x. Re-run your evaluations after upgrading.
- Unknown settings raise instead of being ignored.