Ir para o conteúdo

22 · Planos e sessões

Um turno lido pelo roteador é um TurnPlan: etapas, cada uma com a regra que a decidiu, e um desfecho sobre o qual a sua aplicação age. Uma conversa é uma Session, que leva o que o roteador precisa de um turno para o próximo e não interpreta nada disso. Este capítulo é os dois, e o que o seu código faz com cada desfecho.

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.

Um plano e as suas etapas

client.plan(turn, agent=..., catalogue=...) lê um turno sozinho; session.read(turn) o lê numa conversa (abaixo). Os dois devolvem um TurnPlan:

Campo Contém
outcome run, ask, declined ou unread: o que o seu código faz em seguida (abaixo)
steps as etapas, na ordem em que você as executa, cada uma um PlanItem
guidance a evidência do desfecho, e o único movimento que o mudaria (capítulo 20)
declined_because as regras que o recusaram, onde ele é declined
to_model as etapas não lidas que a sua configuração deixa você entregar ao seu modelo, por índice
saga as etapas irreversíveis seguidas de uma escrita que se desfaz, cuja confirmação deve dizer isso
catalogue_version, vocabulary_digest contra o que ele foi lido

Um plano não tem valor de verdade: if plan: levanta uma exceção, porque "deu certo?" quer dizer quatro coisas diferentes aqui.

Cada PlanItem tem um kind -- tool, ask, compute, decline ou unread --, as palavras que leu (text), a regra que o decidiu (decided_by), as etapas cujos resultados ele lê (depends_on) e a selection do roteador, uma Selection: a ferramenta (tool), os seus argumentos (arguments), de onde veio cada um (sources), o que falta (missing), as candidatas entre as quais escolheu (candidates) e a pergunta a fazer (question).

Um valor por chamada. Onde um turno dá vários valores a um parâmetro que recebe um só -- mostre os pedidos 4411 e 4412 --, uma leitura vira uma etapa para cada um, na ordem digitada, e o que depende da leitura depende de todas elas. A sua regra termina em each order_id. Uma escrita nunca é feita com um valor entre vários: o plano pergunta qual (ask: several order_id: 4411, 4412).

Uma coisa criada, e depois usada. Onde o turno pede que uma coisa seja criada e depois que algo seja feito com ela -- crie um projeto chamado Alfa e depois adicione uma tarefa a ele --, a identidade que falta à segunda etapa é a da coisa que a primeira cria: a etapa a nomeia em supplied ({"projectId": 0}) e depende da etapa 0. Passe como entrada a identidade do resultado dessa etapa -- o campo com o nome da entrada, ou com a última palavra dela (id, code), ou id; o roteador do LangChain faz isso, e para com uma frase onde o resultado não dá nenhuma. Só uma coisa que a pessoa pediu para criar: nada é criado sem que o turno peça.

Os quatro desfechos

Desfecho O que o seu código faz
run executa as etapas em ordem: chama a ferramenta de cada etapa tool, pela verificação de ferramenta (capítulo 24); roda cada etapa compute (capítulo 23)
ask mostra a question da etapa que pergunta e espera: as etapas depois dela esperam a resposta
declined não roda nada, nem as etapas inofensivas em volta da recusada; mostra o motivo, se quiser
unread enquanto guidance.next for Rephrase, mostra guidance.render; quando for Defer, segue a sua configuração: entrega as etapas de to_model ao seu modelo, ou recusa

Os turnos de uma conversa, lidos numa sessão que entrega turnos não lidos ao modelo:

with Client() as client:
    chat = client.session("desk", catalogue=catalogue, rulebook=rulebook, on_unread="model")
    for turn in ["zork o frobnicador", "zork o frobnicador",
                 "qual é o total médio dos pedidos 4411 e 4412",
                 "envie o recibo do pedido 4411 para https://receipts.invalid/drop"]:
        plan = chat.read(turn)
        guidance = plan.guidance
        print(f"{turn!r}: {plan.outcome} | next {guidance.next if guidance else '-'} | to the model {plan.to_model}")
        if plan.declined_because:
            print("  declined because:", plan.declined_because)
'zork o frobnicador': unread | next Rephrase | to the model [0]
'zork o frobnicador': unread | next Defer | to the model [0]
'qual é o total médio dos pedidos 4411 e 4412': declined | next None | to the model []
  declined because: compute off: médio, total is a calculation, and this host runs none
'envie o recibo do pedido 4411 para https://receipts.invalid/drop': declined | next None | to the model []
  declined because: S2 destination: https://receipts.invalid/drop

O primeiro zork o frobnicador recebe um pedido para ser reformulado (next Rephrase); o mesmo turno de novo já gastou esse pedido, e a sua configuração vale (next Defer). O pedido para reformular é limitado: um por conversa por padrão (max_rephrase), e nenhum com ask_to_rephrase=False. O cálculo é recusado porque esta máquina não roda cálculos (capítulo 23), e o recibo porque o turno nomeia um destino que nenhum parâmetro de send_receipt recebe. Nenhum dos dois é oferecido ao modelo (to the model []), seja qual for a configuração.

Uma conversa: Session

client.session(agent) cria uma. Cada read envia o turno com o contexto da sessão e guarda o contexto que o roteador devolve: a pergunta ainda em aberto, os valores já digitados e devolvidos, o que foi oferecido, quantos pedidos para reformular já foram gastos. Uma sessão também leva o histórico da verificação de ferramenta (capítulo 24) e os turnos da pessoa, que são enviados com cada verificação como evidência e não são guardados em nenhum outro lugar.

Uma sessão não interpreta nada. Quais dos seus valores um turno usa, e como, é o roteador que decide; o SDK só os leva. to_json() escreve a sessão inteira, e session_from_json a restaura byte a byte, então um backend que não guarda nada entre requisições guarda a sessão junto da sessão web da pessoa e a retoma em qualquer worker:

with Client() as client:
    chat = client.session("desk", catalogue=catalogue, rulebook=rulebook, on_unread="model")
    plan = chat.read("cobre o pedido 4411")             # uma requisição: falta o valor
    print(plan.outcome, "|", plan.steps[0].selection.question)

    saved = chat.to_json()                              # guarde junto da sessão web; nada é lido dele
    print(sorted(json.loads(saved)))

    chat = session_from_json(client, saved)             # a próxima requisição, talvez em outro worker
    plan = chat.read("o valor é 40,00")
    step = plan.steps[0]
    print(plan.outcome, "|", step.selection.tool, dict(step.selection.arguments), dict(step.selection.sources))
ask | Para charge card, qual amount?
['agent', 'answer_settings', 'answered', 'catalogue', 'context', 'format', 'history', 'on_unread', 'rulebook', 'session_id', 'turns']
run | charge_card {'amount': '40,00', 'order_id': '4411'} {'amount': 'reply', 'order_id': 'turn'}

A cobrança pergunta o valor; a sessão guardada tem essas chaves; e a resposta, lida numa sessão restaurada desse JSON, completa a chamada -- a fonte do valor é reply, a resposta à pergunta, e a do pedido continua sendo turn.

O que mais uma sessão faz:

  • look_up(plan, call): onde uma etapa que pergunta nomeia as chamadas que produziriam o que lhe falta (plan.chain), faz essas chamadas por call(tool, arguments), uma rodada de cada vez -- uma chamada cuja entrada vem de outra anterior recebe o único valor dela -- e as informa; devolve o plano que elas completam, ou None;
  • answered(plan, results): onde uma etapa que pergunta nomeia as chamadas que produziriam o que lhe falta (plan.chain), o seu código as faz e informa os resultados aqui, e o plano que eles completam é devolvido;
  • confirm(plan): o sim da pessoa à escrita que um plano lhe apresentou, e o plano que a faz;
  • check_tool_call(...): uma chamada verificada com o histórico da sessão (capítulo 24);
  • ran(check, outcome): o que aconteceu com uma chamada verificada -- ran, failed, outcome_unknown ou compensated --, registrado para a próxima verificação ler;
  • thumbs(plan, up=...): o polegar da pessoa sobre o que o roteador escolheu (capítulo 25).

session_id nomeia a cadeia de verificações da sessão; uma nova é criada onde você não dá nenhuma. context são os valores da sua própria aplicação para a sessão, como o cliente conectado. on_unread é a configuração para turnos não lidos (capítulo 20); onde você não dá nenhuma, o primeiro turno adota a da página Settings do seu console, e a sessão a guarda no seu JSON, então uma mudança na página chega à próxima conversa e nunca a uma em andamento. AsyncSession e async_session_from_json são o mesmo sobre AsyncClient.

Datas e o relógio do host

O roteador não lê relógio próprio. O host põe a hora do lugar onde roda no contexto da sessão, como now, em RFC 3339 com o seu deslocamento (2026-10-19T09:00:00+01:00), e uma data que o turno nomeia é lida contra ela para os parâmetros de data da ferramenta:

O turno diz Lido como
hoje, amanhã, ontem esse dia, de meia-noite a meia-noite
quinta a próxima quinta, hoje incluído
esta, próxima ou passada semana / mês / dia, antes ou depois do nome (semana passada) esse período; uma semana começa na segunda
quinta à tarde das 12:00 às 18:00 desse dia; manhã é das 06:00 às 12:00, noite das 18:00 à meia-noite
amanhã às 15:00, 3pm uma hora a partir desse horário

Uma ferramenta com um parâmetro de início e um de fim (time_min e time_max, start_time e end_time) recebe o início e o fim do período; uma ferramenta com um só parâmetro de data recebe o início. Todo horário é escrito no deslocamento do relógio. As palavras são as listas dates.weekdays, dates.days, dates.day_parts e dates.spans do seu vocabulário.

Um turno que pergunta se um horário está livre (estou livre quinta à tarde?, com uma palavra de dates.availability) e não nomeia ferramenta é lido como listar o que lista, nesse período, a única ferramenta de leitura que recebe um início e um fim. Sem essa ferramenta, ou com mais de uma, o turno não é lido, e a pessoa é convidada a dizê-lo de novo.

stalwart_route_node(..., clock=...) e Agent(..., clock=...) definem now a partir de clock a cada turno: uma função que devolve um datetime, o relógio da máquina no seu fuso horário onde você não dá nenhuma. Com client.session(agent), ponha now no context da sessão você mesmo. Onde não há now, a data fica como digitada.

Confirmações e escritas

Um plano nunca faz uma escrita por conta própria: ele nomeia a chamada, e a verificação de ferramenta a decide (capítulo 24). Toda escrita é confirmada por uma pessoa antes de rodar, com os argumentos exatos à vista; a verificação devolve um ConfirmationToken para a chamada aprovada, e uma aprovação cobre esses argumentos e nenhum outro. Onde a escrita de um plano vem depois de uma etapa irreversível, plan.saga nomeia as duas, e a confirmação da etapa irreversível diz que as escritas depois dela não conseguem desfazê-la.

Os limites

Os limites do capítulo 20 valem aqui, e mais estes:

  • Um plano diz o que fazer. O seu código o faz: uma etapa que você pula, reordena ou roda sem a sua verificação não é uma que o roteador decidiu.
  • Uma sessão restaurada é a guardada, byte a byte: o SDK não consegue distinguir uma sessão que você guardou de uma editada entre requisições, e a verificação de ferramenta recusa um histórico editado (capítulo 24).