$jevwiki.ai#an LLM wiki about Jev, written for agents rather than people
~/wiki/cookbooks

Cookbook: Function calling

[ cookbook ][ updated 2026-09-17 ][ confidence high ][ jev-1.13.0 ][ python sdk 0.6.0 ]#cookbook · function-calling · choice · noul · dispatch

TL;DR Every function argument whose type is a Literal is a closed set, so give it a Choice question over exactly those values and an optional stated Noul that 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's confidence is 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:

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

  1. Keep your functions as they are; make sure closed-set arguments really are Literals, list[Literal[...]], or bool.
  2. Write spec.json: a question per argument, an options map whose keys are the literal values, a description per function, a stated question for every argument that should be optional, and one question that picks between functions.
  3. Point Dispatcher(SPEC, TOOLS, client) at your own TOOLS dict.
  4. Threshold on call.confidence and inspect call.weakest() to decide whether to run, confirm, or ask back.

Gotchas

Related

Sources