Coming from another tool¶
What you already know, in functai: the OpenAI SDK, DSPy, pandas and polars, Instructor and Pydantic AI.
import functai
functai.configure(lm="gpt-4.1-mini", temperature=0) # the model behind every output on this page
from functai import ai, _ai
The OpenAI SDK (or Anthropic's, or any chat API)¶
Your messages work as they are: From a prompt you already have shows the same request going out, then the steps from there.
DSPy¶
functai grew from the same idea (the signature is the prompt, and programs are optimized from data), with plain Python functions as the signature.
| DSPy | functai |
|---|---|
class QA(dspy.Signature) with InputField/OutputField |
a function: parameters are inputs, the return type and _ai variables are outputs |
field desc= |
a comment on the parameter or the line |
dspy.Predict(QA) |
@ai |
dspy.ChainOfThought(QA) |
@ai(module="cot"), or a reasoning: str = _ai line |
dspy.ReAct(QA, tools=[...]) |
@ai(tools=[...]) |
a dspy.Module with forward |
a function with @module |
dspy.Example(...).with_inputs(...) |
a row: a dict, or a table (expected= names the answer column) |
metric(example, pred, trace=None) |
metric(row, prediction), a dpyr expression, or an AI judge |
dspy.Evaluate(...) |
functai.evaluate(...): a score with its range, and a table |
BootstrapFewShot, BootstrapFewShotWithRandomSearch |
the same names, used through fn.opt(rows, optimizer=...), which returns an improved copy; or functai.bootstrap_few_shot(fn, rows) |
GEPA |
functai.gepa(fn, rows, teacher=...): written in functai, with changes (design/04-gepa.md) |
MIPROv2 |
InstructionSearch |
dspy.configure(lm=dspy.LM("openai/gpt-4o")) |
functai.configure(lm="openai/gpt-4o") (the same spelling works) |
dspy.inspect_history() |
functai.phistory() |
program.save(path) / load |
fn.save(path) / fn.load(path) for the instruction and examples; functai.save for a whole program with what it depends on |
FunctAI no longer runs on DSPy, so a program cannot be turned back into
one: fn.to_dspy() raises, and says what to use instead (fn.state(),
the instruction and examples an optimizer found).
pandas and polars¶
Your data frame goes in with read, and comes back with .to_pandas()
or .to_polars(). In between, dpyr's
verbs, where AI functions are columns:
import pandas as pd
from dpyr import read, col
from typing import Literal
@ai
def language(text: str) -> Literal["English", "French", "Spanish", "other"]:
"""The language the text is written in."""
...
df = pd.DataFrame({"text": ["Merci beaucoup !", "Thanks a lot!", "¡Muchas gracias!"]})
read(df).mutate(language=language(col.text)).to_pandas()
text language
0 Merci beaucoup ! French
1 Thanks a lot! English
2 ¡Muchas gracias! Spanish
| pandas | dpyr |
|---|---|
df["x"] = df["text"].apply(f) |
.mutate(x=f(col.text)): each distinct value once, in parallel, remembered |
df[df["text"].apply(is_spam)] |
.filter(is_spam(col.text)) |
df.groupby("g").agg(...) |
.group_by(col.g).summarize(...) |
pd.read_csv(...) |
read("file.csv") (also parquet, Excel, JSON, databases, URLs) |
Why not .apply? It runs one row at a time, calls the model again for
repeated values, and loses everything if row 9,000 fails. See
Big tables.
Instructor, Pydantic AI, structured outputs¶
| Instructor / Pydantic AI | functai |
|---|---|
response_model=MyModel / output_type=MyModel |
the return type: -> MyModel (pydantic, dataclass, TypedDict, Literal, lists) |
| field descriptions | Field(description=...), or a comment on the field |
| validators and retries | pydantic validators run on the reply; an unreadable reply is asked again once |
| the prompt, as a string | the docstring, or a chat template |
Agent(tools=[...]) |
@ai(tools=[...]) |
What functai adds: running over tables, measuring, optimizing, and saving a program with everything it needs.