Ir para o conteúdo

35 · Pessoas no circuito

Idiomas

O seletor entende turnos escritos em inglês. O português é a sua próxima gramática e ainda não foi lançado; até lá, um turno escrito em português não é lido como português.

Algumas chamadas esperam por uma pessoa: toda escrita, antes de rodar; uma escrita de que ninguém conhece o resultado; um desfazer que continua falhando. Este capítulo mostra o que essa pessoa vê e responde, como uma resposta chega à execução onde quer que ela espere -- em outro worker, vinda de outro canal -- e a interface que mostra tudo isso, @stalwart-agents/agent-ui, que o stalwart serve lhe dá antes que você escreva qualquer parte dela.

Uma confirmação, do lado da pessoa

A cobrança do agente da loja do capítulo 30, enquanto espera (a saída é impressa em inglês):

{"card": "visa-0019", "customer": "c-208", "order_id": "4411", "status": "unpaid", "total": "40.00"}
charge_card | Run charge_card? | ['approve', 'skip', 'cancel_all']
   {'name': 'amount', 'value': 40, 'origin': 'typed'}
   {'name': 'order_id', 'value': '4411', 'origin': 'typed'}
{"amount": "40", "charge_id": "ch-4411", "order_id": "4411"}

Uma confirmação mostra:

  • a chamada: a ferramenta e a sua pergunta -- Run charge_card?, ou o texto que a sua ferramenta dá;
  • cada argumento, com a origem do seu valor: typed (a pessoa o digitou), reply (ela respondeu a uma pergunta), context (a sua aplicação o deu, como o cliente autenticado), computed, result:<passo> (um passo anterior o devolveu), ou model (o modelo o escreveu). A interface diz isso em palavras: you typed it, the assistant wrote it;
  • os motivos por que ela espera, como a verificação os deu: write, argument_origin: amount, tainted_call: untrusted:read_inbox (capítulo 32);
  • o aviso de irreversível, para uma ferramenta marcada irreversible (capítulo 34);
  • as respostas que ela aceita.

As respostas

Toda pausa que uma execução faz, e as respostas que cada uma aceita, geradas a partir do produto:

Tipo Quando uma execução pausa com ele Respostas que aceita
confirm uma chamada que a verificação respondeu com confirm approve, skip, cancel_all
outcome_unknown uma escrita que pode ou não ter tido efeito, e que não é segura para enviar de novo happened, did_not_happen
saga_stuck um desfazer que falhou depois das suas novas tentativas, com o que ainda falta desfazer retry, done_by_hand, abandon
ask uma pergunta que a execução precisa ver respondida para seguir answer
delegate um turno que nada lê, com on_unread: ask: o modelo pode recebê-lo? approve, decline

approve roda a chamada exatamente com os argumentos mostrados, e nada mais. Uma confirmação aprova apenas a chamada mostrada: o seu token cobre aquela ferramenta com aqueles argumentos naquele ponto da execução, então uma chamada cujos argumentos mudaram depois de ser mostrada -- pelo modelo, ou por qualquer outra coisa -- é verificada de novo e pergunta de novo (capítulo 32). skip e cancel_all estão no capítulo 34.

Em Python, reply.waiting lista o que espera, e desk.confirm(user, "approve") responde.

Retomando em qualquer worker

Uma execução que espera é guardada onde o storage: diz -- sqlite:///runs.db para uma máquina, postgres://... para várias. Toda pausa tem um id estável: a mesma chamada, pausada pelo mesmo motivo, tem o mesmo id em todo worker. Assim, a resposta pode chegar em qualquer lugar: o worker que a recebe lê a execução do armazenamento, leva a resposta até ela, e segue. Nada fica guardado no processo entre uma pergunta e a sua resposta.

Várias pendentes ao mesmo tempo

Os agentes que o agent.yaml descreve tratam as chamadas de um turno em ordem, cada uma até o fim, então uma execução deles espera por uma coisa de cada vez. Passos que uma equipe escreve por conta própria podem rodar chamadas lado a lado, e então uma execução espera por várias ao mesmo tempo. Cada uma é respondida pelo seu próprio id -- desk.confirm(user, "approve", waiting=id), ou o id em POST /threads/{id}/resume -- e as outras continuam abertas. Sem id, desk.confirm responde a primeira que espera; GET /threads/{id}/interrupts lista todas.

Uma retomada obsoleta

Uma resposta a uma pausa que já não está aberta -- outra pessoa a respondeu, e a execução seguiu -- é recusada como 409 interrupt_stale, e não muda nada. Duas respostas à mesma pausa que chegam juntas, num worker ou em dois, são distinguidas por uma reivindicação guardada no armazenamento de execuções: a primeira a obtém e retoma a execução; a outra é interrupt_stale. Uma resposta que carrega um token diferente do que foi mostrado à pessoa também é interrupt_stale. A interface lê de novo o que espera quando recebe uma.

Aprovação vinda de outro canal

Uma confirmação pode ser respondida de outro lugar que não o chat: uma fila, um e-mail, uma ferramenta de chat que a sua equipe já usa. A aplicação que o stalwart serve roda, make_app em stalwart_agents.serve, recebe dois argumentos para isso:

  • on_interrupt=, avisado de cada nova pausa como (thread, interrupt). O Webhook(url, secret) do pacote é um deles: ele envia a pausa como JSON, assinada num cabeçalho Stalwart-Signature: t=<tempo>,v1=<hmac>, o HMAC-SHA256 do tempo e do corpo sob o seu segredo. Um receptor só aceita uma entrega dentro de cinco minutos do seu próprio relógio, então uma entrega capturada não pode ser reapresentada depois. Um envio que falha nunca faz o turno falhar, pois ele já rodou: a resposta lista o que não pôde enviar em not_notified;
  • secret=: quando definido, uma retomada também precisa carregar um Stalwart-Signature válido, então só o canal que tem o segredo pode responder.

O próprio stalwart serve não recebe nenhum dos dois; uma equipe que quer aprovações vindas de outro canal monta o make_app no seu próprio servidor.

Se você já usa LangGraph

O Webhook fica junto do servidor de retomada, stalwart_agents.langchain.server, que se monta em qualquer aplicação ASGI sobre o seu próprio checkpointer; o capítulo 27 é a porta para ele.

O que o stalwart serve lhe dá

$ stalwart serve agent.yaml

serve os seus agentes por HTTP, com a interface em /, antes que você escreva qualquer interface. As suas rotas, geradas a partir do pacote:

Rota Faz
GET / a interface: chat, confirmações, recusas com a sua regra, rollbacks
POST /threads/{id}/messages um turno: {"text": ...}; a resposta traz a resposta do agente, o que espera por uma pessoa, e qualquer recusa ou rollback como dados
GET /threads/{id}/interrupts o que espera por uma pessoa na conversa, cada coisa pelo seu próprio id
POST /threads/{id}/resume a decisão de uma pessoa sobre uma delas: {"interrupt_id", "decision", "token"}; as outras continuam abertas
GET /threads/{id}/stream os passos da conversa como server-sent events, retomáveis por Last-Event-ID
POST /threads/{id}/rollback um operador desfaz a execução da conversa, com Authorization: Bearer <token de operador>
POST /threads/{id}/feedback o polegar de uma pessoa numa resposta: {"message", "thumb"}, o polegar up ou down, vindo da sessão a que a resposta foi mostrada
GET /healthz 200 enquanto o serviço responde
GET /config.json com o que a interface em / está configurada, o nome do agente e a base sob a qual estão as suas rotas

As suas opções:

Opção O que o produto passa a fazer
--host o endereço em que escutar; 127.0.0.1 quando não informado
--port a porta; 8700 quando não informado
--agent o agente que responde primeiro; o primeiro do arquivo quando não informado
--operator-token o que a rota de rollback precisa carregar como Authorization: Bearer; QLS_OPERATOR_TOKEN quando não informado, e sem nenhum dos dois a rota responde 403

O rollback do operador desfaz a execução de uma conversa de fora dela. Ele recebe o token de operador como Authorization: Bearer <token>, comparado em tempo constante; sem token definido, a rota fica desligada e responde 403, dizendo como definir um.

@stalwart-agents/agent-ui

A interface que o stalwart serve mostra é um pacote próprio, para uma equipe que põe a conversa nas suas próprias páginas:

$ npm install @stalwart-agents/agent-ui

Incorporando

Importar o pacote define os seus elementos; um deles é a conversa inteira:

<stalwart-chat thread="u-17" base="/agent"></stalwart-chat>

base é onde o seu stalwart serve (ou a sua própria montagem dele) responde; thread é o id da conversa. Todos os elementos, gerados a partir do código do pacote:

Elemento Mostra Atributos Propriedades Eventos Partes CSS Slots
<stalwart-chat> a conversa, e o que estiver esperando uma decisão base, theme, thread, thumbs eventSource, fetcher — thumbs, thumbs-notice, log, status, steps, composer, citations, sources header, footer
<stalwart-confirm> uma chamada que espera, os seus argumentos com as suas origens, os motivos, o aviso de irreversível e as suas respostas theme interrupt stalwart-decision confirm, turn, arguments, reasons, irreversible, choices —
<stalwart-decision> uma recusa numa frase, a sua regra ao expandir rule, sentence, theme — — decision, rule —
<stalwart-rollback> o que uma execução desfez, e o que não pôde desfazer theme report — rollback —
<stalwart-trace> os passos de uma conversa theme events — trace —

<stalwart-confirm> dispara stalwart-decision com a pausa e a resposta escolhida; <stalwart-chat> a envia ao backend por você.

O backend com que ela fala

A interface fala com base e com nada mais: as rotas acima -- um turno, o que espera, uma decisão, o fluxo de passos, um polegar. Nenhuma telemetria, nenhuma fonte, nenhum script de outro host. Cada requisição nomeia a sessão da página, depois que o servidor entrega uma, então um polegar só é aceito da sessão a que a resposta foi mostrada.

Aplicando um tema

Toda cor e todo tamanho é uma propriedade personalizada de CSS, com um padrão claro e um escuro; theme="light" ou theme="dark" fixa um deles, e sem nenhum dos dois a configuração da própria pessoa escolhe:

Propriedade personalizada Claro Escuro
--stalwart-bg #ffffff #121212
--stalwart-fg #1a1a1a #f1f1f1
--stalwart-muted #595959 #bdbdbd
--stalwart-line #d0d0d0 #3c3c3c
--stalwart-accent #0b57d0 #8ab4f8
--stalwart-accent-fg #ffffff #0b0b0b
--stalwart-warn #8a4b00 #ffb74d
--stalwart-bad #b3261e #f28b82
--stalwart-radius 8px 8px
--stalwart-gap 12px 12px
--stalwart-font system-ui, sans-serif system-ui, sans-serif

Cada região é uma ::part (listadas acima), e <stalwart-chat> tem os slots header e footer para o seu próprio conteúdo. Para as cores da loja, por exemplo: stalwart-chat { --stalwart-accent: #2e7d32; --stalwart-radius: 4px; --stalwart-font: Georgia, serif; }.

Trocando as suas palavras

Toda palavra que a interface mostra é uma string com uma chave, e setStrings({ approve: "Aprovar", skip: "Pular este passo" }) troca qualquer uma delas; um {name} numa string é preenchido onde ela é mostrada. As strings de fábrica:

Chave Texto de fábrica
conversation Conversation
placeholder Write a message
send Send
sending Sending…
waiting Waiting for your decision
confirmTitle Confirm {tool}?
argumentsTitle What it will use
origin_typed you typed it
origin_reply you answered it
origin_context from your account
origin_computed calculated
origin_model the assistant wrote it
origin_result from an earlier step
origin_agent from another assistant
reasonsTitle Why this needs you
irreversible Nothing can undo this step once it runs.
approve Approve
skip Skip this step
cancelAll Cancel everything
refused Not done
rule Rule
showRule Which rule?
rollbackTitle Undoing what this run did
undone Undone
notUndoable Could not be undone
stuck Undoing {tool} failed. What should happen?
retry Try again
doneByHand I undid it myself
abandon Leave it
unknownTitle Did {tool} take effect?
happened Yes, it happened
didNotHappen No, it did not
delegateTitle I couldn't work that out. Shall I pass it to the assistant?
delegateApprove Yes, pass it on
decline No
reconnecting Reconnecting…
traceTitle What happened
step Step {step}
error Something went wrong: {message}
thumbUp Helpful
thumbDown Not helpful
thumbsNotice A rated message is kept to improve the assistant, and nothing else of the conversation.
sources Sources

Os limites

  • Uma confirmação aprova apenas a chamada mostrada, com os seus argumentos; um argumento mudado pergunta de novo. Ela prova que uma pessoa aprovou, não que ela leu.
  • Uma resposta chega a uma execução que espera; ela nunca muda uma que já seguiu (interrupt_stale).
  • O receptor de um webhook é seu: a assinatura prova que a entrega veio do seu servidor dentro de cinco minutos, e nada sobre quem a respondeu do outro lado.
  • A interface mostra o que a execução relata -- a regra de uma recusa, os desfazeres de um rollback -- nunca uma leitura das palavras da resposta.