Skip to main content
When you order a “large iced oat latte, no sweetener,” the barista does not write your sentence down - they mark four options on a cup. This cookbook does the same thing for a trading API: a sentence goes in, and out comes a function name and its arguments as evaluated enums, each with a confidence.
Those calls go to ten ordinary functions in a trading assistant. Their arguments take values from fixed lists, so they are Literals already:
An argument whose values come from a fixed list is a closed set. When it takes one value out of that list, it gets a Choice over exactly those values, so whatever reaches the function is a value the function accepts. You leave the functions alone. What you add is a spec that says in plain words what each argument means. By the end you have a Dispatcher you can point at your own functions.

Setup

Set TYPESAFE_API_KEY. Two modules sit beside this file. trader.py holds the ten functions, plus a TypeSafe client that reads answers from a cache, so re-rendering replays the numbers below without calling the API. dispatch.py holds the code that reads a signature and a spec and makes the call.

Find the closed sets in the signatures

The type hints already say which arguments come from a fixed list, and what is in each list. closed_sets reads a signature and sorts those arguments into three shapes: a choice (a Literal, so one value out of the list), a set (a list[Literal[...]], so any number of them), or a flag (a bool, so on or off). All ten functions are defined in trader.py.
top_movers shows what gets left out. Of its three arguments, two are closed sets. The third, limit, is an int, so it never gets a question and keeps its default of 3. Free text, numbers and dates work the same way: no question, and the function’s default stands.

Write the spec

The Literal gives you the strings "1mo" and "3mo". It does not say that a user typing “this quarter” means the second one. The spec says that. It holds a question per argument, a line per option, a description per function, and one more question that picks between the functions. It lives in spec.json, and an LLM can write it for you from the signatures.
The option keys are the strings the function takes, so nothing has to map a label back to an argument afterwards. stated makes an argument optional. It is a second yes/no question asking whether the command says anything about that argument at all. When the answer is no, the call leaves that argument out and the function’s own default applies. 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. 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.

Turn the spec into questions

Dispatcher builds the questions from the spec once. Each command is then one request carrying the choice of function and every function’s arguments, and the dispatcher reads only the chosen function’s answers.

Run fourteen commands

Each line below is one request. confidence is the least certain judgement behind that call.
Both long commands came out as asked. “plot rolling correlation between nvda and spy for the past month” filled four arguments from one sentence. Two of them, 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. “compare nvda amd and msft over the past three months” put three tickers in the set and left the other three out. Running three of them:
output output output And the ones that answer in text:

Read the confidence

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. Where that number came from, argument by argument:
window and resolution are both omitted here, because “lately” does not say how far back or on what bars, so rolling_correlation runs on its own defaults of one month and hourly bars. That is what the stated question is for. Without it, the choice would have to name some window, and it would have named one confidently.

Open it in the playground

The link below holds one command and the questions for the function it picked: the choice over the ten function descriptions, and rolling_correlation’s four arguments. Edit the command there and the arguments change with it.
Open the command and its questions in the TypeSafe playground →