/**
* Fields: authored once, asked in any order, each with its own wording.
*
* Teaches: `ask`, `enum`, `extract: 'asked'`, `maxAsks`, a step's own `ask`,
* confirmation as a collected boolean behind an `if`, `clearOnStart`.
* Read next: docs/concepts/collection.md
*
* Run: GEMINI_API_KEY=... bun run examples/02-fields.ts
*/
import { falai, GeminiProvider, type DataOf } from "@falai/agent";
const f = falai().fields({
nome: { type: "string", ask: "Pergunte o nome de um jeito leve, sem tom de formulário." },
empresa: { type: "string", ask: "Pergunte de qual empresa a pessoa fala." },
tamanho: {
type: "string",
enum: ["1-10", "11-50", "51-200", "200+"],
ask: "Pergunte quantas pessoas trabalham lá e ofereça as faixas.",
},
orcamento: { type: "number", ask: "Pergunte a faixa de investimento, dizendo que é só para orientar." },
// Booleans are harvested only from the reply to the step that asks them,
// so a stray "sim" elsewhere never confirms anything.
confirmado: { type: "boolean", ask: "Resuma em uma frase o que anotou e pergunte se está tudo certo." },
});
type Data = DataOf<typeof f>;
// Data = { nome: string; empresa: string; tamanho: '1-10' | '11-50' | '51-200' | '200+'; orcamento: number; confirmado: boolean }
const triagem = f.flow({
id: "triagem",
name: "Triagem",
description: "Quando alguém chega querendo saber se o produto serve para a empresa dele.",
on: [{ message: ["quer saber como funciona", "pede um orçamento"], repeat: "always" }],
// A second run starts clean; the other fields stay known.
clearOnStart: ["confirmado"],
steps: [
// A step ends when its fields are known. If the first message already
// said "sou a Ana, da Acme", this step is skipped without a model call.
{ id: "quem", prompt: "Descubra quem é e de onde fala.", collect: ["nome", "empresa"] },
// No prompt: the fields' own `ask` texts drive the question.
{ id: "porte", collect: ["tamanho"] },
// Step wording wins over the field's; give up after two tries.
{
id: "grana",
collect: ["orcamento"],
ask: { orcamento: "Pergunte quanto {{data.empresa}} pensa em investir por mês; aceite 'não sei'." },
maxAsks: 2,
},
{ id: "confirma", collect: ["confirmado"] },
// A "no" clears the confirmation and starts over from the first step.
{ id: "ok", if: { equals: { confirmado: true } }, else: { step: "quem", clear: ["confirmado"] } },
{ id: "tchau", prompt: "Agradeça e diga que um vendedor continua daqui." },
],
});
const agent = f.agent({
name: "Ana",
provider: new GeminiProvider({ apiKey: process.env.GEMINI_API_KEY ?? "", model: "gemini-2.5-flash" }),
flows: [triagem],
});
// Two turns. Fields land in whatever order the customer gives them.
const first = await agent.turn({ sessionId: "demo", message: "Oi, sou a Ana da Acme, quero um orçamento" });
console.log(first.messages[0]?.text);
const second = await agent.turn({
sessionId: "demo",
session: first.session,
message: "Somos 30 pessoas",
});
console.log(second.messages[0]?.text);
const data: Partial<Data> = second.session.data;
console.log(data); // { nome: 'Ana', empresa: 'Acme', tamanho: '11-50' }