Ir para o conteúdo

23 · Cálculos

Qual é o total médio dos pedidos 4411 e 4412? Nenhuma ferramenta da loja responde isso: get_order devolve um pedido. O roteador lê o turno como duas leituras e um cálculo sobre os resultados delas, e o cálculo roda na sua máquina, escrito pelo seu próprio modelo. Este capítulo é como, e o que cada parte pode ver.

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 plano

with Client() as client:
    plan = client.plan("qual é o total médio dos pedidos 4411 e 4412", agent="desk", catalogue=catalogue)
    print(plan.outcome, "| compute:", plan.compute)
    for step in plan.steps:
        what = (f"over steps {list(step.compute['sources'])}" if step.kind == "compute"
                else f"{step.selection.tool} {dict(step.selection.arguments)}")
        print(f"  {step.index} {step.kind} | {step.decided_by} | {what}")

O plano lê cada pedido que o turno nomeia (each order_id) e depois uma etapa compute que depende das duas leituras:

run | compute: run
  0 tool | R2 source: get_order, each order_id | get_order {'order_id': '4411'}
  1 tool | R2 source: get_order, each order_id | get_order {'order_id': '4412'}
  2 compute | R1+R2: médio, total over get_order | over steps [0, 1]
succeeded 26.25 number
the prompt names the fields, never a value: True True
not_admitted | the function was not admitted: import: socket (line 1)

A etapa de cálculo leva o seu cálculo: a instrução nas palavras da pessoa, o tipo de resultado que ela deve devolver -- number, number[], table ou text -- e as etapas cujos resultados ela lê (sources).

Rodando: client.compute

O seu código executa as etapas de leitura como executa qualquer ferramenta, e depois entrega a etapa e os resultados delas a client.compute, com o seu modelo:

def write(prompt):
    """O seu modelo. Recebe o prompt e devolve o código da função; aqui, uma resposta que ele poderia dar."""
    return ("```python\n"
            "def compute(data):\n"
            "    totals = [float(order['total']) for order in data.values()]\n"
            "    return sum(totals) / len(totals)\n"
            "```")


with Client() as client:
    step = next(step for step in plan.steps if step.kind == "compute")
    sources = [{"tool": "get_order", "result": get_order(plan.steps[i].selection.arguments["order_id"])}
               for i in step.compute["sources"]]
    done = client.compute(step, sources, model=write)
    print(done.state, done.result, done.result_type)
    print("the prompt names the fields, never a value:", "40.00" not in str(done.prompt), "total" in str(done.prompt))

Ele roda em quatro partes, todas na sua máquina:

  1. O prompt. O runtime licenciado o monta a partir do pacote de prompts que a sua licença traz: a instrução, o tipo de resultado e a forma das fontes -- os nomes dos seus campos e os seus tipos JSON, nunca um valor delas. A última linha da saída acima diz isso desta execução.
  2. A função. O seu modelo a escreve. model é qualquer função do prompt -- um ComputePrompt, uma str com a parte de sistema e depois a do usuário, com .system, .user e .schema para um modelo que recebe papéis -- para o código: o SDK do seu provedor, o seu próprio endpoint, um modelo local. O pacote stalwart-agents não guarda nenhuma credencial de modelo e não chama nenhum provedor.
  3. A admissão. O runtime lê a função antes que qualquer coisa a rode. Uma função que abre um socket, lê um arquivo ou importa qualquer coisa fora dos módulos do pacote é recusada, e nada roda.
  4. A execução. Um container novo da imagem do sandbox: sem rede, um sistema de arquivos só de leitura, nenhum arquivo e nenhuma variável da sua máquina. O resultado é verificado contra o tipo pedido, e o resultado verificado é o que você recebe.

client.compute devolve um Computation: state é succeeded (result tem o valor verificado), failed (o sandbox a rodou, ou não conseguiu iniciar, e failure diz qual dos dois) ou not_admitted. result_type é o tipo que o plano pediu -- aqui um number, a média.

Uma função recusada

def careless(prompt):
    return "import socket\n\ndef compute(data):\n    return socket.gethostname()\n"


with Client() as client:
    refused = client.compute(step, sources, model=careless)
    print(refused.state, "|", refused.failure["message"])

A última linha da saída: a admissão nomeia a regra e a linha, o estado é not_admitted, e nada rodou. Uma função recusada não conta nada.

Onde um cálculo roda

Um plano só pede uma etapa de cálculo onde a sua máquina consegue rodar uma: um motor de containers responde, a imagem do sandbox está nele e a sua licença o permite. Em qualquer outro lugar, o plano recusa o turno, nomeando o cálculo (compute off: ...), e nenhuma etapa dele roda (capítulo 22). python -m stalwart_agents.sandbox baixa a imagem uma vez e imprime a sua referência.

Nada de um cálculo sai da sua máquina: nem as fontes, nem o prompt, nem a função, nem o resultado. O que sai são duas contagens no seu relatório de uso: compute_runs, uma execução que chegou ao sandbox, e compute_seconds, o seu tempo de relógio arredondado para cima. A cota de um mês gasta levanta QuotaExceededError antes que o seu modelo seja consultado.

Os limites

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

  • A função é do seu modelo. Um resultado verificado tem o tipo pedido; se ele é o cálculo que a pessoa quis dizer é trabalho do seu modelo, e nada aqui prova isso.
  • A admissão recusa o que sai do sandbox. Ela não lê a função atrás de erros.