Ir para o conteúdo

20 · Roteando uma conversa, e o que o roteador não afirma

A Parte II é para a equipe que coloca o seletor de ferramentas MCP dentro da sua própria aplicação: o seu código recebe o turno de uma pessoa, pede um plano e executa ele mesmo as etapas do plano. O agente da Parte I faz tudo isso por você; aqui você segura cada peça. Este capítulo é o que o seletor decide a cada turno, onde ele roda, e os limites contra os quais cada capítulo desta parte é lido. A superfície de roteamento do seletor -- a parte que lê um turno e faz dele um plano -- passa a se chamar o roteador.

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.

O que o roteador decide

A cada turno, contra o seu catálogo (capítulo 21), o roteador decide quatro coisas:

  • qual ferramenta, com quais argumentos, e de onde veio cada valor: do turno, de uma resposta a uma pergunta, do contexto da sua aplicação ou do resultado de uma chamada anterior;
  • o que perguntar, onde falta um valor ou duas ferramentas servem igualmente: uma pergunta fixa, nunca um palpite;
  • o que calcular, onde o turno pede um cálculo que nenhuma ferramenta responde (capítulo 23);
  • o que recusar: um turno que levaria dados para onde nenhuma ferramenta os leva, e um cálculo que escreveria.

Um turno é lido num plano de etapas, cada uma com a regra que a decidiu. Um turno que o roteador lê é decidido sem chamada a modelo: a regra é uma regra sobre o que as suas ferramentas declaram, não um julgamento, e o mesmo turno sobre o mesmo catálogo e o mesmo vocabulário dá o mesmo plano, byte a byte.

Lendo um turno

A central de atendimento da loja da Parte I, lida turno a turno:

def show(turn, plan):
    print(f"{turn!r}: {plan.outcome}")
    for step in plan.steps:
        print(f"  {step.kind} | {step.decided_by} | {step.selection.tool or '-'} | "
              f"{dict(step.selection.arguments)} | from {dict(step.selection.sources)}")
    if plan.declined_because:
        print("  declined because:", plan.declined_because)
    if plan.guidance and plan.outcome != "run":
        print("  says:", plan.guidance.render)


with Client() as client:
    print("answered by:", client.runtime_status()["side"])
    for turn in ["mostre o pedido 4411", "cobre o pedido 4411 em 40,00", "cobre 40,00 do pedido 4411",
                 "cobre o pedido 4411", "apague o pedido 4411", "zork o frobnicador"]:
        show(turn, client.plan(turn, agent="desk", catalogue=catalogue))
answered by: local
'mostre o pedido 4411': run
  tool | tool: admission | get_order | {'order_id': '4411'} | from {'order_id': 'turn'}
'cobre o pedido 4411 em 40,00': run
  tool | tool: derived | charge_card | {'amount': '40,00', 'order_id': '4411'} | from {'amount': 'turn', 'order_id': 'turn'}
'cobre 40,00 do pedido 4411': run
  tool | tool: derived | charge_card | {'amount': '40,00', 'order_id': '4411'} | from {'amount': 'turn', 'order_id': 'turn'}
'cobre o pedido 4411': ask
  ask | ask: incomplete | charge_card | {'order_id': '4411'} | from {'order_id': 'turn'}
  says: Posso charge card, mas preciso de amount.
'apague o pedido 4411': unread
  unread | unread: no tool | - | {} | from {}
  says: Posso get "pedido", mas nada aqui consegue apagar isso.
'zork o frobnicador': unread
  unread | unread: no tool | - | {} | from {}
  says: Não reconheci nada nisso. Posso charge card, get order, hand off, read inbox ou send receipt. Pode dizer qual ferramenta ou qual item você quer?
  • mostre o pedido 4411 é get_order, com o order_id tirado do turno;
  • cobre o pedido 4411 em 40,00 e cobre 40,00 do pedido 4411 são a mesma chamada: o valor e o pedido se distinguem pelo que cada parâmetro é, qualquer que seja a ordem das palavras;
  • cobre o pedido 4411 é uma pergunta, porque a cobrança não tem valor. O plano é ask, e diz o que falta;
  • apague o pedido 4411 não é de nenhuma ferramenta: nada aqui apaga. O plano diz isso numa frase que você pode mostrar;
  • zork o frobnicador não é lido como coisa nenhuma. O roteador oferece o que pode fazer no lugar.

O decided_by de cada etapa nomeia a regra. Um plano que o surpreenda pode ser lido de volta até a regra que o fez, e contestado nesses termos.

O roteador lê inglês (English) e português do Brasil (pt-BR), no único idioma que você escolhe para ele -- ele nunca adivinha o idioma de um turno. Com o português escolhido, e pedido no seu vocabulário como palavra para order, me mostra o pedido 4411, cancela o pedido 4411 e vc pode cancelar o pedido 4411? são lidos pelas mesmas regras, e respondidos com as mesmas etapas, que o seu equivalente em inglês. Os verbos não pedem nada a você: mostrar é show e cancelar é cancel. Os substantivos de um catálogo com nomes em inglês pedem: pedido é um order numa loja e um request num balcão de atendimento, então as palavras que os seus usuários dizem para os substantivos das suas ferramentas vão num arquivo de vocabulário desse idioma, que você publica ou que o minerador aprende (capítulo 25). Um turno em outra língua não é lido.

Um bot feito no console também lê com as suas próprias palavras. O construtor de bots as deriva, em cada língua, do dicionário inteiro para as ferramentas que você escolheu, e as grava ao lado do agent.yaml num arquivo que vocabulary: nomeia (Agent(vocabulary=...) para um bot declarado em Python). Elas só levam a uma ferramenta onde nenhuma das suas palavras decide; uma palavra que duas ferramentas compartilham é perguntada, nunca adivinhada; e cada plano nomeia as palavras com que leu pelo resumo delas, derived_digest. Um arquivo alterado depois que o construtor o gravou é recusado quando o bot carrega. O arquivo também guarda os adjetivos e advérbios das suas ferramentas -- do nome, dos valores de um parâmetro, de uma opção como permanent -- e as palavras de mesmo sentido ou do sentido oposto, que escolhem entre as ferramentas que um turno alcançou e preenchem o parâmetro que nomeiam, e nunca alcançam uma ferramenta sozinhos.

O que um turno pede, antes de qualquer ferramenta

Cada resposta traz form, a forma lógica do turno: o que ele pede, lido da sua gramática antes de qualquer ferramenta ser comparada. É por ela que um plano que surpreende pode ser seguido de volta, para além das ferramentas, até o que o roteador entendeu do turno.

  • act -- o que o turno pede: inform (que lhe digam algo), act (algo feito), change (algo alterado), howto (como se faz algo), meaning (o que uma palavra quer dizer), give (que lhe deem algo, como quero um café), reply (uma resposta à pergunta que o bot fez) ou none;
  • predicate -- o verbo com que pede;
  • roles -- o papel das suas palavras, cada um com o seu text: a coisa de que trata (thing), uma apontada (pointed: este, aqueles), um qualificador (qualifier: artigos que têm um autor em comum), um destino (destination: para ana@example.com), uma quantidade (quantity) e um tempo (time);
  • polarity -- affirmative, ou negated onde o pedido é negado;
  • tense -- present, past ou future;
  • frame -- question, howto, ordering (um pedido posto como tal: quero, me vê) ou none.

Um turno que pergunta o que foi feito -- chegou algum e-mail?, uma question no past -- nunca é uma escrita: o roteador não chama para ele nenhuma ferramenta que altere algo.

Onde ele roda

O roteador roda onde estão os seus turnos, nunca nos nossos servidores: no runtime licenciado que o pacote stalwart-agents traz, na sua máquina, sob a sua licença (capítulo 38). Client() o usa onde houver uma licença ativa.

client.runtime_status() diz se o runtime responde (local, ou none com o que configurar). A API hospedada compila as suas regras, guarda o seu vocabulário e conta o que você usa: ela nunca vê um turno, um argumento ou um resultado. Onde o runtime não consegue responder, um plano levanta SelectorUnavailableError, dizendo o que configurar, e nada é decidido no seu lugar.

O SDK traz a superfície do roteador, não a sua lógica. Client.plan, Session.read e o resto entregam o turno ao runtime e devolvem a resposta dele como dados: nenhum Python do pacote lê uma palavra de um turno. O que você segura é um TurnPlan (capítulo 22).

Um turno que o roteador não consegue ler

Todo plano traz uma orientação: o desfecho, a evidência dele e o único movimento que o mudaria. Os desfechos:

Desfecho O que significa
resolved uma ferramenta, com todo valor de que precisa
incomplete uma ferramenta, faltando um valor: o plano o pede
ambiguous duas ou mais ferramentas servem igualmente: o plano pergunta qual
unmet_object a ação é uma que uma ferramenta faz, sobre nada que este catálogo tem
unmet_action a coisa é uma sobre a qual uma ferramenta age, por nenhuma ação que ela faz
unread nada no turno é lido

E o próximo movimento, next: Supply (dê o valor que falta), Choose (escolha entre as ferramentas nomeadas), Rephrase (diga de novo, uma vez), Defer (a configuração do host decide) ou None (nada o muda: o catálogo não consegue). render é a frase a mostrar à pessoa.

Um turno que nada lê recebe primeiro um pedido para ser reformulado. Depois disso, a sua configuração para turnos não lidos, on_unread, decide: recusá-lo (refuse), entregá-lo ao seu modelo (model), perguntar à pessoa se o modelo pode recebê-lo (ask) ou respondê-lo a partir dos seus documentos (answer, capítulo 44). Onde você não define nada, cada conversa adota no seu primeiro turno a configuração da página Settings do seu console, ou refuse.

O que o roteador não afirma

Estes são os limites contra os quais cada capítulo da Parte II é lido.

  • Um plano recusado nunca é entregue a um modelo, diga o on_unread o que disser. Um turno que pede para enviar dados para onde nenhuma ferramenta os leva é recusado inteiro, e nenhuma etapa dele roda.
  • Um turno devolvido a você está fora do que o roteador decidiu. Onde a sua configuração entrega um turno não lido ao seu modelo, é o modelo que escolhe o que fazer com ele. Toda chamada de ferramenta que ele fizer ainda é verificada (capítulo 24), mas a ferramenta que o modelo escolheu não é uma decisão do roteador.
  • Ele decide pelo que as suas ferramentas declaram. Uma ferramenta cujo nome e descrição dizem outra coisa que não o que ela faz é escolhida pelo que eles dizem. O roteador não lê o código das suas ferramentas.
  • Ele não prova que um plano é o certo. Ele lê o turno por regras fixas, e uma regra pode ler um turno de um jeito diferente de quem o digitou. É por isso que cada etapa nomeia a sua regra, e por isso que o caçador de bugs (capítulo 29) verifica que as suas guardrails fazem toda ferramenta passar por ele, e não que ele escolheu bem.
  • O vocabulário move os planos. Uma palavra que o feedback dos seus usuários ensinou ao roteador (capítulo 25) muda como o próximo turno é lido. Todo plano nomeia o vocabulário com que foi lido (vocabulary_digest), e uma versão pode ser revertida.