Skip to main content
Faro is a language companion on the phone. It knows learners by their number, signs new ones up, and teaches short lessons in English or Spanish. It explains in the learner’s own language, puts each exercise, judges the answer, and carries on from where they stopped, across calls. The code is in examples/faro.

The model grades, code teaches

The lesson needs no task. Five main-agent tools carry it, and each result says its own line:
faro/agent.py
The learner, the language and the exercise waiting are locked. The model sends only the answer, a grade of four (correct, partly, wrong, unclear) and one line of feedback. Code turns the grade into a result, records the attempt in SQLite, moves the learner on or not, and says the next line. Why it works this way. The model cannot finish a lesson by saying so. Only graded answers move progress, and an answer to an exercise that is not waiting is refused. Grading is the one judgement only a model can make, so it is the one thing the model decides.

Two agents, one per language

The library says some lines itself: a read-back, a step’s first question. Those lines belong to an AgentSpec, so Faro is two agents built from one template, faro_en and faro_es. They share the prompt and the tools.
faro/agent.py
A learner who asks to switch is handed to the other agent. The handoff saves the new explanation, so every line code says from then on is in that language. See Handoffs. A known caller is greeted by the agent of their own language. A Lookup in the prefetch reads the language from their number before the greeting. Nothing about them is said until they say the number is theirs.

Read back before it is used

Two tasks hold what must be read back first:
  1. other_number: a number the caller says. It takes the number as a task argument and opens on check_number, so code reads it back with no model request.
  2. sign_up: a new profile. Code reads back the name, the language, the goal and the level, and creates the profile on a yes.
faro/agent.py
learn, goal and level are Literals. A number from outside Spain is refused by the Phone type.

What a call costs

The parts

Best practice.
  1. Keep the lines code says in a table per language, and build one agent per language from it.
  2. Save the language in the State on the handoff. Every line code says can then read it.
  3. Let the model make only the judgement a model must make. Give it a closed set of answers.

Try it

The store has three learners. Call as Emma with UNMUTE_CALL_START={"call": {"direction": "inbound", "calling_number": "+34600555666"}}.
  1. “Yes.” Code greets Emma and says where she stopped.
  2. “Let’s carry on.” Code says the first exercise of “shopping”.
  3. “Un kilo de manzanas, por favor.” Then “¿Cuánto cuesta?” Then “Quiero pan y leche. ¿Cuánto es en total?”
  4. “Can you explain in Spanish?” The other agent takes over and puts the exercise again.
Lucía (+34 600 111 222) explains in Spanish. Any other Spanish number is a new caller, who gets a profile read back and then the first lesson.

Troubleshooting

After a switch, the model says the exercise in its own words

The model wrote the first reply after the switch. Fix: give each Handoff opens_with=OpeningTool(tool="continue_lesson"). Code then puts the exercise again in the new language, with no model request.

A hint is said twice

The model said the hint and passed it to give_hint. Fix: tell it to call give_hint “with no words of your own”. Code says the hint from the result.

Where to go next

Example agents

Back to the overview, and how to run and check each example.

Handoffs

Every argument of a handoff, on both frameworks.