Cookbook: Function calling
TL;DR Every function argument whose type is a
Literalis a closed set, so give it aChoicequestion over exactly those values and an optionalstatedNoulthat decides whether the argument was mentioned at all. One request carries the function choice plus every function's arguments; the dispatcher reads only the winner's answers, and the call'sconfidenceis the minimum per-argument probability, not the product.
Goal
Turn a sentence such as "plot rolling correlation between nvda and spy for the past month" into rolling_correlation(symbol='NVDA', benchmark='SPY', window='1mo') with confidence 0.91, calling ten ordinary typed Python functions in a trading assistant. The cookbook's framing: a barista does not write your sentence down, they mark four options on a cup.
Because Jev only ever picks among the options you hand it, whatever reaches the function is a value the function accepts. You do not modify the functions; you add a spec that says in plain words what each argument means.
Inputs / state shape
The state is the user's command string. The functions are the schema. Signature (verbatim):
def plot_price(
symbol: Literal["SPY", "NVDA", "AMD", "AAPL", "MSFT", "TSLA"],
style: Literal["line", "candles"] = "line",
resolution: Literal["1m", "5m", "15m", "1h", "1d"] = "15m",
window: Literal["1d", "1w", "1mo", "3mo"] = "1w",
include_volume: bool = False,
moving_average: Literal["9", "20", "50"] | None = None,
log_scale: bool = False,
): ...
closed_sets(fn) reads a signature and sorts arguments into three shapes: choice (a Literal, one value), set (a list[Literal[...]], any number), flag (a bool). Across the 10 functions there are 28 fillable arguments; list_symbols has 0, plot_price has 7. top_movers's limit is an int, so it gets no question and keeps its default of 3 — free text, numbers and dates behave the same way.
The second input is spec.json, one entry per argument (an LLM can write it from the signatures). Verbatim excerpts:
{
"style": {
"question": "Does the user want a plain line or candles?",
"stated": "Does the user say how the chart should be drawn, such as a line, candles, or OHLC bars?",
"options": {
"line": "a simple line through the closing prices",
"candles": "a candlestick or OHLC chart, showing each bar's open, high, low and close"
}
}
}
{
"moving_average": {
"question": "How many bars should the moving average cover - nine, twenty, or fifty?",
"stated": "Does the user ask for a moving average or a smoothed line over the candles?",
"options": {
"9": "a nine-bar moving average, a fast one",
"20": "a twenty-bar moving average",
"50": "a fifty-bar moving average, a slow one"
}
}
}
The option keys are the strings the function takes, so nothing has to map a label back to an argument afterwards.
Questions asked
Dispatcher builds 54 questions per command from the spec once. Four of them, exactly as the cookbook prints them (qid, type, instructions):
qid |
type |
instructions |
|---|---|---|
__tool__ |
choice |
What is the user asking the trading assistant to do? |
plot_price.style |
choice |
Does the user want a plain line or candles? |
plot_price.style? |
noul |
Does the user say how the chart should be drawn, such as a line, … (truncated at 64 chars in the source output) |
compare_returns.symbols.NVDA |
noul |
Does the user want NVDA in the comparison? |
Structure of the question set:
__tool__— oneChoiceover the ten function descriptions. Its answer picks the function.<fn>.<arg>— aChoicewhose criteria are the argument'soptions(key → description).<fn>.<arg>?— thestatedNoul, which makes the argument optional. When the answer is no, the call leaves the argument out and the function's own default applies.<fn>.<arg>.<MEMBER>— a set argument gets its question once per member, with{}standing in for the member name:"Does the user want {} in the comparison?"becomes one question per ticker.
Writing advice from the cookbook: write each question about the idea rather than the words a user might pick, because the match is on meaning — "is amd tracking nvidia lately" reaches rolling_correlation even though neither tracking nor lately appears anywhere in spec.json. Avoid naming a question after its parameter; "Which resolution?" gives the command nothing to match against.
Combining logic in code
The dispatcher sends one request carrying the function choice and every function's arguments, then reads only the chosen function's answers:
import json
from pathlib import Path
from dispatch import ROUTE, Dispatcher, closed_sets
from trader import TOOLS, client, load
TYPESAFE_MODEL = "jev-1.12"
SPEC = json.loads(Path("spec.json").read_text())
assistant = Dispatcher(SPEC, TOOLS, client)
call = assistant("plot rolling correlation between nvda and spy for the past month")
print(call) # rolling_correlation(symbol='NVDA', benchmark='SPY', window='1mo')
print(f"{call.confidence:.2f}") # 0.91
print(f"{call.tool.probability:.2f}")
call.run() # actually invokes the function
Reading one call apart, argument by argument:
call = CALLS["is amd tracking nvidia lately"]
for name, argument in call.arguments.items():
top = sorted(argument.distribution.items(), key=lambda kv: -kv[1])[:3]
shown = "omitted, default stands" if argument.omitted else repr(argument.value)
print(
f" {name:<12}{shown:<26}p {argument.probability:.2f} "
+ " ".join(f"{k} {v:.2f}" for k, v in top)
)
print(f" weakest argument: {call.weakest().name}")
"is amd tracking nvidia lately" -> rolling_correlation(symbol='AMD', benchmark='NVDA') confidence 0.82
symbol 'AMD' p 0.87 AMD 0.87 NVDA 0.13 AAPL 0.00
benchmark 'NVDA' p 0.78 NVDA 0.92 AMD 0.08 AAPL 0.00
window omitted, default stands p 0.96
resolution omitted, default stands p 0.99
weakest argument: benchmark
confidence reports the least certain judgement in the call, rather than the product of all of them, since one wrong argument is enough to spoil the result. A product answers a different question ("is every part right"), and it falls as a function takes more arguments whether or not any one judgement is shaky.
dispatch.py and trader.py sit beside the notebook and are not reproduced upstream, so the Dispatcher/closed_sets bodies are not available (inferred: you write them yourself from the description above).
Results / what the cookbook reports
Fourteen commands, each one request. Selected rows verbatim:
| command | call | confidence | tool p |
|---|---|---|---|
show nvda 1h |
plot_price(symbol='NVDA', resolution='1h') |
0.78 | 1.00 |
plot rolling correlation between nvda and spy for the past month |
rolling_correlation(symbol='NVDA', benchmark='SPY', window='1mo') |
0.91 | 1.00 |
when during the day does nvda trade the most |
intraday_pattern(symbol='NVDA') |
0.53 | 1.00 |
what tickers do you have |
list_symbols() |
1.00 | 1.00 |
candles for tesla with a 20 period moving average |
plot_price(symbol='TSLA', style='candles', moving_average='20') |
0.69 | 0.97 |
compare nvda amd and msft over the past three months |
compare_returns(symbols=['NVDA', 'AMD', 'MSFT'], window='3mo') |
0.94 | 1.00 |
biggest losers today |
top_movers(window='1d', direction='losers') |
0.98 | 0.98 |
worst drawdown for nvda this quarter, and chart it please |
drawdown(symbol='NVDA', window='3mo', plot=True) |
0.84 | 0.84 |
show me apple daily with volume |
plot_price(symbol='AAPL', resolution='1d', include_volume=True) |
0.75 | 0.85 |
is amd tracking nvidia lately |
rolling_correlation(symbol='AMD', benchmark='NVDA') |
0.82 | 0.82 |
The rolling-correlation command filled four arguments from one sentence; symbol and benchmark draw from the same six tickers and each ticker landed in the right argument because the questions spell out the roles (the one being measured, named first against the second one named, the yardstick). The published run used TYPESAFE_MODEL = "jev-1.12" over 156,780 one-minute bars.
Adapting it to a new domain
- Keep your functions as they are; make sure closed-set arguments really are
Literals,list[Literal[...]], orbool. - Write
spec.json: aquestionper argument, anoptionsmap whose keys are the literal values, adescriptionper function, astatedquestion for every argument that should be optional, and one question that picks between functions. - Point
Dispatcher(SPEC, TOOLS, client)at your ownTOOLSdict. - Threshold on
call.confidenceand inspectcall.weakest()to decide whether to run, confirm, or ask back.
Gotchas
statedis what keeps defaults alive. Without it theChoicewould have to name some window and would have named one confidently; "lately" says nothing about a window, sorolling_correlationruns on its own defaults (one month, hourly bars).- Arguments with open types get no question, so their function defaults silently stand.
top_movers(limit: int)is never filled from the sentence. - Confidence is a minimum, so it is only as good as the weakest argument's question.
intraday_pattern(symbol='NVDA')at 0.53 is the sort of row you route to a confirmation prompt (see Confidence-gated routing). - Install line. The cookbook uses
pip install ipython polars matplotlib numpy "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/.cooksafeis a helper package on TypeSafe's private index, andpypi.typesafe.aireturned 404 publicly as of 2026-09-17 — installpip install typesafe-sdkinstead and reimplement the helpers you need. Here that ismake_playground_link(state, questions, models=[...]), which builds ahttps://console.typesafe.ai/playground#share/...URL, and (intrader.py) a response cache. - The published code pins
TYPESAFE_MODEL = "jev-1.12"; this wiki documentsjev-1.13.0, so re-running live may shift probabilities.
Related
- Choice questions — the primitive behind every argument question
- Noul (yes/no) questions — the
statedquestion type - Confidence vs probability — what
confidencemeans and how to threshold it - Intent routing — the same shape when the target is a handler, not a function
- Speculative fan-out — why all 54 questions ride in one request
- Cookbook: Skill suggestion — ranking a large option set then re-reading the top few
- Cookbooks overview — the full cookbook catalog
Sources
- raw/docs/cookbooks__function_calling.md (https://docs.typesafe.ai/cookbooks/function_calling.md)