24 · Verificando uma chamada de ferramenta¶
Nem toda chamada vem de um plano. O seu modelo pode escolher uma ferramenta sozinho -- num turno que a sua configuração lhe entregou, ou num loop próprio --, e essa chamada é verificada antes de rodar exatamente como uma planejada. Este capítulo é a verificação do lado do seu código: como pedir, o que volta e o que fazer com cada resposta. O capítulo 32 percorre cada passo da verificação em ordem.
Idiomas
O seletor lê turnos escritos em inglês e em português do Brasil (pt-BR). O idioma é escolhido, nunca adivinhado a partir do turno: na página Settings do console (Language the selector reads), em language numa requisição de plano, ou no settings.language do seu arquivo de vocabulário; sem nenhum deles, inglês. Um turno em português pode ser digitado com os acentos ou sem eles. O português europeu não é uma gramática que o seletor tenha, e um turno em qualquer outra língua não é lido.
Pedindo¶
client.check_tool_call(tool, arguments, agent=...) verifica uma chamada; session.check_tool_call(...) a
verifica com o histórico da sessão (capítulo 22). A verificação roda onde o
roteador roda: no runtime licenciado na sua máquina. Nada da chamada chega à Stalwart.
turns = ["cobre 40,00 do pedido 4411"] # as palavras da pessoa, enviadas como evidência e nunca guardadas
facts = {"card_on_file": True} # o que é o $card_on_file do rulebook, como o seu código sabe
with Client() as client:
history = History() # levado por você entre as verificações, assinado pelo runtime
def check(tool, arguments, **given):
global history
answer = client.check_tool_call(tool, arguments, agent="desk", catalogue=catalogue, rulebook=rulebook,
turns=turns, history=history,
session_id="chat-17", **given)
history = answer.history
print(f"{tool} {arguments}: {answer.decision} | {answer.reason} | {answer.reasons}"
+ (f" | at {answer.pointer}" if answer.pointer else ""))
return answer
check("get_order", {"order_id": "4411"}, origins={"order_id": "typed"})
asked = check("charge_card", {"order_id": "4411", "amount": 40.00},
origins={"order_id": "typed", "amount": "typed"}, context=facts)
# A pessoa disse sim: a mesma chamada, com o token da confirmação.
check("charge_card", {"order_id": "4411", "amount": 40.00},
origins={"order_id": "typed", "amount": "typed"}, context=facts, confirmation=asked.confirmation)
check("charge_card", {"order_id": "4411", "amount": 400.00}, origins={"order_id": "typed", "amount": "model"},
context=facts)
check("delete_order", {"order_id": "4411"}, origins={"order_id": "typed"})
check("get_order", {"order_id": "4411", "note": "urgente"}, origins={"order_id": "typed", "note": "model"})
print(len(history.entries), "checks in the history")
get_order {'order_id': '4411'}: allow | allow | ['allow']
charge_card {'order_id': '4411', 'amount': 40.0}: confirm | write | ['write']
charge_card {'order_id': '4411', 'amount': 40.0}: allow | confirmed | ['confirmed']
charge_card {'order_id': '4411', 'amount': 400.0}: confirm | argument_origin: amount | ['argument_origin: amount']
delete_order {'order_id': '4411'}: refuse | unknown_tool | ['unknown_tool']
get_order {'order_id': '4411', 'note': 'urgente'}: refuse | argument_schema | ['argument_schema'] | at /note
6 checks in the history
O que cada chamada envia além da ferramenta e dos seus argumentos:
origins: de onde veio cada argumento --typedpela pessoa,modelescrito pelo seu modelo, uma resposta (reply), o seucontextouresult:<step>de uma chamada anterior --, o que decide quais valores uma escrita aceita sem perguntar;turns: as próprias palavras da pessoa, como evidência de que um valor dito digitado foi digitado. Enviadas com a verificação e guardadas em lugar nenhum;history: as verificações feitas até agora na conversa (abaixo);context: os fatos que o seu rulebook lê, como$card_on_file, como o seu código os conhece:trueoufalsepor nome. Um fato não enviado é falso, então uma regra que só permite uma cobrançaif $card_on_filea recusa (o capítulo 45 traz os dois jeitos de informá-los);confirmation: o token que a aprovação de uma pessoa devolveu, para a chamada que ela aprovou.
As respostas¶
Um ToolCheck tem uma decisão (decision) -- allow, confirm ou refuse --, o motivo que a decidiu
(reason), todos os motivos de uma vez em reasons e, para um argumento, onde nele (pointer). Como um
plano, ele não tem valor de verdade: confirm não é nem sim nem não.
| Resposta | O que o seu código faz |
|---|---|
allow |
roda a chamada uma vez, com exatamente estes argumentos, e depois informa o que aconteceu com ela: session.ran(check, "ran"), ou failed, outcome_unknown ou compensated |
confirm |
mostra à pessoa a chamada, os seus argumentos e de onde veio cada um, e os motivos; diante de um sim, verifica a mesma chamada de novo com confirmation=check.confirmation. Uma aprovação cobre estes argumentos e nenhum outro |
refuse |
não roda nada. Dá ao seu modelo o motivo como resultado da ferramenta, para ele avisar a pessoa ou tentar algo que a verificação permita |
Na saída: a leitura é permitida; a cobrança que a pessoa digitou pede confirmação, porque toda escrita é
confirmada; a mesma cobrança com o token da aprovação é permitida (confirmed); um valor que o modelo
escreveu pede confirmação de novo, nomeando o argumento; delete_order não está no catálogo; e note não é
argumento de get_order, recusado em /note.
O histórico¶
Toda verificação acrescenta uma entrada ao histórico da conversa: a etapa, a ferramenta, o agente, digests dos
argumentos e das suas origens, as marcas que eles traziam e a resposta -- nomes e digests, nunca um valor.
Cada resposta traz um token que assina essa entrada com todas as anteriores, e a próxima verificação confere a
cadeia. Quem leva o histórico é você: numa Session ele avança sozinho, e sem estado ele é o History de
cada resposta, que to_dict() guarda e History.from_dict restaura. Um histórico editado, reordenado ou
tirado de outra conversa faz a próxima verificação falhar com history_invalid.
Recibos: verificar uma vez, impor duas¶
Um ponto de imposição na frente das suas ferramentas -- uma rota de gateway, um proxy de API -- vê cada chamada
e não deveria decidi-la uma segunda vez. Um agente que verificou a sua própria chamada entrega ao ponto de
imposição o seu histórico, e a rota de recibos do seu próprio stalwart gateway
(capítulo 37) diz se esse histórico é o recibo exatamente desta chamada. A última entrada do histórico é o recibo: allow com o motivo receipt onde ela é
a verificação que permitiu esta ferramenta com estes argumentos, para o agente que a entrada nomeia; senão
refuse, com receipt_missing (o histórico não tem entrada), receipt_not_allowed (a última verificação não
permitiu a sua chamada), receipt_mismatch (outra ferramenta, ou outros argumentos, foi verificada),
receipt_spent (já foi informado um resultado para essa chamada) ou history_invalid. Um recibo admite uma
chamada; a mesma chamada enviada de novo só é admitida onde reenviá-la é seguro -- uma leitura, uma ferramenta
declarada idempotente, ou uma escrita cuja chave de idempotência vai num argumento. O formato da resposta é
qls-tool-receipt/1, e o de uma verificação é qls-tool-check/1.
Os limites¶
Os limites do capítulo 20 valem aqui, e mais estes:
- A verificação decide pelo seu catálogo, pelo seu rulebook e pelo histórico que lhe enviam. Uma
chamada que o seu código roda sem perguntar, ou roda depois de um
refuse, não é uma que ela decidiu. - O que aconteceu com uma chamada -- se ela rodou -- é o que o seu código informa. A próxima verificação lê isso como informado.
- Ela verifica a chamada, não o motivo dela: uma chamada permitida é uma que as suas regras permitem, não necessariamente uma que a pessoa queria.