Chega um e-mail com quatro endereços no cabeçalho e uma frase no corpo: “não usem o endereço de faturamento para este, manda o recibo no meu pessoal”. Qual endereço vai no cadastro?
Ou uma fatura com quatro valores: subtotal, imposto, total e um crédito de cortesia. Qual deles é o que o cliente precisa pagar, e qual precisa entrar com sinal invertido?
O dado está no texto. O problema é escolher.
Por que a solução ingênua falha
Regex sozinha não decide. Ela acha os quatro e-mails com precisão e não tem
como saber qual é o do recibo, porque isso depende de uma frase em português no
corpo da mensagem. Regra fixa do tipo “use o Reply-To” acerta neste e-mail e
erra no próximo.
Modelo generativo decide e digita. E digitar é o problema. Pedir “extraia o total da fatura” a um modelo que gera texto significa aceitar um valor reescrito — e um dígito trocado numa fatura é incidente contábil, não bug de formatação. Pior: o erro é invisível, porque o valor sai plausível.
Essa fronteira é declarada. A documentação lista geração entre os modos de falha
do jev-1.13 e recomenda exatamente a inversão desta receita: quando o espaço de
resposta é limitado, transforme extração em escolha entre opções, em vez de pedir
o valor.
O desenho das perguntas
Três passos, e o papel de cada peça fica nítido:
- A regex acha os candidatos, calibrada para achar com folga (melhor achar demais que de menos).
- Uma pergunta Choice escolhe qual candidato responde à pergunta. As opções são os trechos achados, mais um escape.
- O código copia o trecho escolhido e normaliza.
import re
from typesafe_sdk import Choice, TypeSafeClient
EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
NENHUM = "nenhum"
def achar(padrao, texto):
vistos, saida = set(), []
for trecho in padrao.findall(texto):
trecho = trecho.strip()
if trecho and trecho not in vistos:
vistos.add(trecho)
saida.append(trecho)
return saida
def escolher(documento, candidatos, pergunta):
criteria = {c: None for c in candidatos} | {NENHUM: "Nenhum destes é o valor pedido."}
with TypeSafeClient() as client:
resposta = client.system_one(
state=documento,
questions={"escolha": Choice(instructions=pergunta, criteria=criteria)},
)
escolha = resposta.answers["escolha"]
return {"valor": escolha.choice, "confianca": escolha.confidence}
candidatos = achar(EMAIL_RE, documento)
recibo = escolher(documento, candidatos, "Para qual endereço o remetente quer que o recibo seja enviado?")
A propriedade que isso compra: a resposta é um dos trechos, copiado sem alteração. Não existe caminho pelo qual o modelo devolva um e-mail que não estava no documento, ou um valor com dígito diferente. Ele aponta; o código copia.
Depois da escolha, perguntas auxiliares completam o que o código precisa para normalizar — e vão na mesma leva, cada uma com o tipo certo. A moeda é um Choice sobre um conjunto fechado; “isto é crédito ou cobrança?” é um Noul.
O que o experimento oficial mediu
O cookbook mostra três casos. Os números são da TypeSafe, com o jev-1.12:
| Caso | Candidatos achados | Escolha | Confiança |
|---|---|---|---|
| Endereço para o recibo | 4 e-mails | o endereço pessoal, contrariando o campo de faturamento | 0,98 |
| Endereço do remetente | os mesmos 4 | o endereço da linha “De” | 1,00 |
| Celular entre três telefones | 3 números | o celular direto | 1,00 |
| País do escritório | lista fechada | Estados Unidos | 0,90 |
| Total a pagar na fatura | 4 valores | US$ 1.315,50, classificado como cobrança | probabilidade de crédito 0,01 |
| Crédito de cortesia | os mesmos 4 | US$ 50,00, classificado como crédito | probabilidade de crédito 0,99 |
O caso do e-mail é o que melhor mostra o valor da receita: o endereço certo não é o que uma regra fixa escolheria, é o que a frase do corpo pede — e o modelo leu a frase. O caso do telefone mostra a divisão de trabalho completa: o modelo escolhe o número e lê o país no texto, e uma biblioteca de telefone junta os dois para produzir o formato internacional. Nada nos dígitos diz qual número é o celular; as palavras em volta dizem.
No caso do dinheiro, as duas perguntas auxiliares fazem o código saber o sinal de cada valor: 0,01 de probabilidade de crédito no total e 0,99 no crédito.
O cookbook registra dois limites com clareza. O primeiro é o teto de 255 opções de um Choice — acima disso, escolha em duas etapas. O segundo é honesto sobre onde está o trabalho: achar os candidatos é a parte difícil. E-mail, telefone e valor têm expressão regular que os cobre; nome de pessoa não tem, e aí os candidatos precisam vir de um cadastro, de um reconhecedor de entidades ou de um modelo generativo.
O que muda em português
Este é um dos casos em que a adaptação brasileira muda código, não só texto.
Separador decimal. O cookbook avisa que a conversão dele assume vírgula de milhar e ponto decimal, e que em “€1.315,50” é o contrário. No Brasil é sempre o contrário: “R$ 1.315,50”. O próprio cookbook sugere a solução elegante — perguntar com um Noul qual convenção o documento usa e ramificar no código.
Documentos nossos. CPF, CNPJ, chave PIX, número de nota fiscal e código de barras de boleto são todos conjuntos com formato conhecido: regex acha, e a pergunta escolhe qual dos achados é o pedido. É a receita inteira, aplicada ao documento brasileiro mais comum.
Telefone. Nosso celular tem nono dígito e DDD, e a distinção entre fixo e celular está no formato — mas qual deles é “o do financeiro” está nas palavras em volta. Mesma divisão: formato para a regex, papel para o modelo.
Como a TypeSafe declara que o inglês é a língua principal de treino, teste a escolha com os seus documentos antes de confiar em confiança alta.
Onde isso quebra
- Candidato ausente. Se a regex não achou, o modelo não pode escolher. Calibre
para achar demais e deixe o escape
nenhumfazer a limpeza. - Muitos candidatos. Acima de 255 opções, duas etapas.
- Leitura literal. “Qual valor o cliente deve pagar?” e “qual é o total da fatura?” podem apontar trechos diferentes quando existe crédito aplicado. Escreva a condição exata, como discutido em critérios que separam.
- Conta. Somar subtotal com imposto e conferir se fecha com o total é aritmética, e aritmética é código — está entre os limites declarados.
Para datas, que têm partes em conjuntos fechados e a mesma divisão de trabalho, veja extrair datas. O índice está em receitas.
Perguntas frequentes
Por que não pedir o valor direto ao modelo?
Porque o jev-1.13 não é treinado para gerar texto, e a documentação recomenda o contrário: levantar os candidatos com expressão regular ou outro modelo e deixar o Jev escolher o correto entre eles. Assim o valor devolvido é um trecho copiado, não digitado.
Isso impede erro de dígito?
Impede essa classe de erro. Como as opções da pergunta são os próprios trechos achados no documento, a resposta é um daqueles trechos sem alteração. O modelo não tem como inventar um valor nem transpor um número.
E quando nenhum candidato serve?
A receita inclui uma opção de escape, 'nenhum', descrita como 'nenhum destes é o valor pedido'. Sem esse escape, o modelo é forçado a apontar um trecho errado.
Quantos candidatos cabem?
Até 255, que é o limite de opções de uma pergunta Choice. Acima disso, o cookbook sugere duas etapas: escolher a seção primeiro e o trecho dentro dela depois.
Serve para nome de pessoa?
Só se você tiver de onde tirar os candidatos. O cookbook é explícito: e-mail, telefone e valor têm expressão regular que os cobre; nome não tem, então os candidatos precisam vir de um cadastro que você já tem, de um reconhecedor de entidades ou de um modelo generativo.