Documentação

Como construir com TypeSafe

Projete software com IA mantendo o código no controle e dando ao System One decisões estreitas e estruturadas.

O System One é o modelo da TypeSafe para construir software com IA, não agentes. Ele não gera código nem escolhe a própria próxima ação. Ele fornece primitivas de IA que se incorporam ao software, de modo que o código permanece no controle enquanto o modelo cuida de julgamentos de senso comum sobre dados não estruturados.

Três arquiteturas de software

A TypeSafe é projetada para construir software com IA, em que o código é dono do fluxo de trabalho e a IA cuida de decisões estreitas e estruturadas.

O código tradicional é uma árvore de decisão complexa feita de primitivas de software simples. Como cada primitiva é confiável, os desenvolvedores podem compô-las em abstrações de nível mais alto.

Um agente processa instruções e escolhe o próprio próximo passo. Isso funciona bem quando uma pessoa monitora o processo, mas cada laço é mais uma oportunidade de sair dos trilhos.

O código cuida do trabalho determinístico e é dono do fluxo de controle. O modelo aparece apenas onde o sistema precisa de senso comum programável ou de interpretar dados não estruturados. Cada tarefa de IA é mantida atômica e restrita.

Software tradicional, agentes e software com IA mostrados como três arquiteturas de sistema diferentes.

O que torna o System One componível

Estruturado

O System One é type-safe por construção. As decisões e probabilidades seguem os tipos de software estruturados e o schema JSON que seu código espera, então ele nunca precisa recuperar um valor de um texto gerado.

Paralelo

As perguntas são avaliadas de forma independente e em paralelo. O resultado de uma primitiva não se torna contexto oculto que altera o resultado de outra.

Comparável

As saídas são ordenáveis e podem alimentar if inteligentes, limiares e comparações.

Rápido

A maioria das consultas é concluída em cerca de 100 ms. O System One é rápido o suficiente para caminhos de requisição em tempo real e interfaces de usuário.

Confiança calibrada

O RLCD comunica a incerteza por meio de probabilidades calibradas, em vez de tender ao excesso de confiança.

Autoconsistente

O System One é projetado para retornar respostas estáveis em avaliações repetidas. Veja o cookbook de autoconsistência.

Como toda saída é restrita às opções fornecidas, o modelo retorna uma distribuição de probabilidade completa sobre essas opções, em vez de inventar um valor fora do schema. A meta da TypeSafe é uma razão de inteligência para velocidade e custo maior que 100×; a aposta por trás disso é que inteligência mais barata criará muito mais demanda.

Projete um fluxo de trabalho com System One

  1. Use código sempre que puder

    Mantenha o trabalho determinístico no código. Ele é confiável e barato. Evite laços while de agente quando um fluxo de trabalho de software pode expressar o mesmo comportamento.

    Exemplo: mantenha regras determinísticas no código
    days_overdue = (today - invoice.due_date).days
    
    if days_overdue > 30:
        route_to_collections(invoice)

    Navegue pelos padrões do System One para ver formas limitadas de compor decisões do modelo com código.

  2. Decomponha o estado de entrada

    Inclua apenas o contexto relevante para as perguntas atuais. Isso ajuda o modelo a evitar distrações e a degradação do contexto. Não confie em conhecimento armazenado nos pesos do modelo quando a informação atual pode vir da sua própria base de conhecimento.

    Exemplo: envie apenas o contexto relevante
    request
    {
      "state": {
        "ticket_message": "My flight was cancelled. Can I get a refund?",
        "refund_policy": "Cancelled flights are eligible for a full refund."
      },
      "questions": {
        "policy_supports_refund": {
          "type": "noul",
          "instructions": "Does the refund policy support the refund requested in the ticket?"
        }
      }
    }
  3. Use estrutura no estado de entrada

    Use JSON aninhado para os campos state e questions. Aponte as perguntas para valores específicos quando isso eliminar ambiguidade e inclua os caracteres de crase em torno de cada caminho dentro da pergunta.

    Exemplo: referencie um valor aninhado

    Use um caminho com pontos e índices entre crases para apontar uma pergunta a um valor aninhado específico, como support.tickets[0].message.

    request
    {
      "state": {
        "support": {
          "tickets": [
            {
              "message": "I was charged twice for order A-104."
            },
            {
              "message": "How do I reset my password?"
            }
          ]
        },
        "commerce": {
          "orders": [
            {
              "id": "A-104",
              "charges": [
                {
                  "amount_usd": 49,
                  "status": "captured"
                },
                {
                  "amount_usd": 49,
                  "status": "captured"
                }
              ]
            }
          ]
        },
        "account": {
          "security": {
            "password_reset": "Email a reset link to the address on file."
          }
        }
      },
      "questions": {
        "duplicate_charge": {
          "type": "noul",
          "instructions": "Do `support.tickets[0].message` and `commerce.orders[0].charges` indicate a duplicate charge?"
        },
        "password_reset_supported": {
          "type": "noul",
          "instructions": "Can `account.security.password_reset` resolve the request in `support.tickets[1].message`?"
        }
      }
    }
  4. Decomponha as perguntas

    Faça as perguntas mais explícitas, estreitas, específicas e atômicas que puder. Divida perguntas complexas ou mal definidas em perguntas separadas, cada uma avaliando uma única propriedade.

    Exemplo: decomponha a detecção de spam
    One broad question (bad)
    {
      "is_spam": {
        "type": "noul",
        "instructions": "Is `message` spam?"
      }
    }
    Decomposed questions (good)
    {
      "requests_credentials": {
        "type": "noul",
        "instructions": "Does `message.body` ask the recipient to provide a password or other login credential?"
      },
      "offers_unexpected_reward": {
        "type": "noul",
        "instructions": "Does `message.body` claim the recipient received an unexpected prize, payment, or reward?"
      },
      "creates_time_pressure": {
        "type": "noul",
        "instructions": "Does `message.subject` or `message.body` pressure the recipient to act quickly?"
      },
      "sender_identity_mismatch": {
        "type": "noul",
        "instructions": "Does the organization named in `message.sender.display_name` conflict with the domain in `message.sender.email`?"
      },
      "link_domain_mismatch": {
        "type": "noul",
        "instructions": "Does the domain in `message.links[0].url` conflict with the organization named in `message.sender.display_name`?"
      },
      "disguises_link_destination": {
        "type": "noul",
        "instructions": "Does `message.links[0].text` conceal or misrepresent the destination in `message.links[0].url`?"
      }
    }
    Exemplo: verifique um trace de chamadas de ferramenta
    One broad question (bad)
    {
      "tool_calls_are_correct": {
        "type": "noul",
        "instructions": "Is `trace.tool_calls` correct for `request` and `available_tools`?"
      }
    }
    Decomposed questions (good)
    {
      "geocode_tool_is_relevant": {
        "type": "noul",
        "instructions": "Is `trace.tool_calls[0].name` an appropriate tool for resolving `request.location`?"
      },
      "geocode_location_matches": {
        "type": "noul",
        "instructions": "Does `trace.tool_calls[0].arguments.city` match `request.location`?"
      },
      "geocode_arguments_match_schema": {
        "type": "noul",
        "instructions": "Does `trace.tool_calls[0].arguments` conform to `available_tools.geocode_city.parameters`?"
      },
      "geocode_result_matches_call": {
        "type": "noul",
        "instructions": "Does `trace.tool_results[0].tool_call_id` match `trace.tool_calls[0].id`?"
      },
      "weather_tool_is_relevant": {
        "type": "noul",
        "instructions": "Is `trace.tool_calls[1].name` an appropriate tool for answering `request.text`?"
      },
      "weather_arguments_match_schema": {
        "type": "noul",
        "instructions": "Does `trace.tool_calls[1].arguments` conform to `available_tools.get_weather.parameters`?"
      },
      "weather_uses_geocoded_coordinates": {
        "type": "noul",
        "instructions": "Do the coordinates in `trace.tool_calls[1].arguments` match those in `trace.tool_results[0].output`?"
      },
      "weather_date_matches": {
        "type": "noul",
        "instructions": "Does `trace.tool_calls[1].arguments.date` match `request.date`?"
      },
      "weather_unit_matches": {
        "type": "noul",
        "instructions": "Does `trace.tool_calls[1].arguments.unit` match `request.unit`?"
      }
    }
  5. Use estrutura nas perguntas

    Mantenha as perguntas curtas. instructions e criteria geralmente são strings e, para uma pergunta curta e sem ambiguidade, uma string basta. Eles também podem ser objetos ou arrays. Coloque a pergunta em um campo e os dados que orientam a pergunta nos outros.

    A estrutura ajuda nestas situações:

    • A pergunta precisa de contexto ou exemplos. Uma frase longa de informação de fundo ou uma lista de entradas de exemplo pertence a campos nomeados ao lado da pergunta, onde seu código pode adicioná-los ou trocá-los sem reescrever a pergunta.
    • Parte da pergunta vem do seu código. Quando um valor vem de um banco de dados, coloque-o em seu próprio campo em vez de inseri-lo em um modelo de string.
    • Várias perguntas têm instruções parecidas. Uma requisição recebe um estado e pode incluir várias perguntas. Adicionar dados complementares pode ajudar a diferenciar as perguntas.
    Exemplo: referencie um registro do seu código

    Este Noul compara um currículo no estado com um registro de um banco de dados de candidatos. O registro entra em potential_duplicate como está, e a pergunta se refere a ele pelo nome.

    questions
    {
      "same_as_record_18": {
        "type": "noul",
        "instructions": {
          "potential_duplicate": {
            "name": "John Smith",
            "location": "Oakland, California",
            "last_employer": "Google"
          },
          "question": "Is the resume for the same person as `potential_duplicate`?"
        }
      }
    }

    Os dados de “potential_duplicate” vindos do código podem mudar com o tempo. A “question” se refere a eles usando crases.

    As descrições dentro de criteria também podem ser objetos. Para um Choice, a descrição de cada opção pode ser um objeto que diz o que a opção cobre, o que pertence a outra opção e alguns exemplos. Use os mesmos nomes de campo em todas as opções para que o modelo possa compará-las diretamente.

    Exemplo: defina criteria de Choice contrastivos
    questions
    {
      "card_help_topic": {
        "type": "choice",
        "instructions": {
          "question": "Which disposable virtual card topic is the user asking about?",
          "focus": "Classify the information the user wants."
        },
        "criteria": {
          "get_disposable_virtual_card": {
            "what": "Purpose, eligibility, or setup",
            "not_for": "Quantity, transaction, or merchant restrictions",
            "examples": [
              "How can I get a disposable virtual card?",
              "What are disposable cards for?"
            ]
          },
          "disposable_card_limits": {
            "what": "Quantity, transaction, or merchant restrictions",
            "not_for": "Purpose, eligibility, or setup",
            "examples": [
              "How many disposable cards can I make per day?",
              "Where can I use a disposable card?"
            ]
          }
        }
      }
    }

    A página de cada tipo de pergunta tem um exemplo trabalhado:

    • Noul compara um currículo com vários registros de candidatos, uma pergunta por registro, com as perguntas construídas no código.
    • Choice descreve duas opções facilmente confundidas, com o que cada uma cobre, para que não serve e exemplos.
    • Score dá a cada nível uma descrição e situações de exemplo.

    O cookbook da cascata de extração de dados estruturados mostra o caso de redação compartilhada, fazendo a mesma bateria de perguntas sobre cada campo de um registro extraído.

    Uma pergunta ou um critério curto e sem ambiguidade pode continuar sendo uma string. Adicione estrutura quando ela separar orientações que, de outra forma, se confundiriam. Para o conjunto completo de lugares em que a estrutura é aceita, veja Avançado: estrutura.

  6. Faça muitas perguntas

    Faça muitas perguntas estreitas e independentes sobre o mesmo estado em uma única requisição. É assim que você maximiza a eficácia e a inteligência por dólar com a API: as perguntas são executadas em paralelo, e o código pode combinar seus sinais sem adicionar viagens seriais ao modelo.

    Veja o padrão Fan-out especulativo e o cookbook de perguntas paralelas.

  7. Combine as saídas das perguntas no código (ou alimente um modelo clássico de ML)

    Combine respostas independentes com regras determinísticas ou somas ponderadas. Para composição aprendida, use as probabilidades como features em um modelo clássico de machine learning a jusante.

    Exemplo: combine sinais com uma pontuação ponderada
    answers = response.answers
    
    # Combine independent signals into one application-specific score.
    quality = (
        0.4 * answers["answers_request"].noul
        + 0.4 * answers["citations_are_supported"].noul
        + 0.2 * (1 - answers["contradicts_context"].noul)
    )

    Composite Scoring mostra como preservar julgamentos individuais ao combiná-los. Se você não tiver rótulos para um modelo a jusante, use um ensemble de modelos de raciocínio caros para gerá-los; o cookbook do AutoResearch mostra como treinar um modelo clássico com saídas do System One.

  8. Roteie conforme a incerteza

    Faça o código tomar ações diferentes para respostas confiantes e não confiantes. Escalone casos incertos para uma pessoa ou para um modelo de raciocínio mais caro. Teste os limiares plotando a confiança contra a acurácia nos seus dados.

    Exemplo: roteie por confiança
    answer = response.answers["card_help_topic"]
    
    if answer.confidence < 0.8:
        route_to_human_review(ticket)
    else:
        route_to_handler(answer.choice, ticket)

    Veja Confiança e Roteamento com gating por confiança para escolher limiares e combiná-los com o risco de cada ação.

Reunindo tudo

Este fluxo de trabalho de ticket de suporte mantém o trabalho determinístico no código, envia apenas contexto estruturado relevante, avalia muitas perguntas atômicas em uma única requisição e compõe as respostas com portões de confiança explícitos.

triage_ticket.py

from typesafe_sdk import Choice, Noul, NoulCriteria, Score, TypeSafeClient

def triage_ticket(ticket, customer):
    # Handle deterministic states without calling a model.
    if ticket["status"] == "closed":
        return "no_action"

    open_orders = [
        order for order in customer["orders"] if order["status"] != "delivered"
    ]

    # Include only the structured context needed by the questions below.
    state = {
        "ticket": {
            "message": ticket["message"],
            "sender": ticket["sender"],
            "links": ticket["links"],
        },
        "customer": {
            "plan": customer["plan"],
            "open_orders": open_orders,
        },
        "policy": {
            "sensitive_credentials": ["password", "security code", "API key"],
        },
    }

    # Ask structured, atomic questions together so they run in parallel.
    questions = {
        "topic": Choice(
            instructions={
                "question": "Which team should handle `ticket.message`?",
                "focus": "Classify the customer's primary request.",
            },
            criteria={
                "billing": {
                    "what": "Charges, invoices, refunds, or subscriptions",
                    "not_for": "Order tracking or account access",
                    "examples": ["I was charged twice", "Where is my refund?"],
                },
                "orders": {
                    "what": "Order status, delivery, cancellation, or returns",
                    "not_for": "Charges or account access",
                    "examples": ["Where is my order?", "Cancel my shipment"],
                },
                "account": {
                    "what": "Login, profile, permissions, or security",
                    "not_for": "Charges or order tracking",
                    "examples": ["Reset my password", "I cannot sign in"],
                },
            },
        ),
        "requests_credentials": Noul(
            instructions={
                "question": "Does the message request a sensitive credential?",
                "compare": [
                    "`ticket.message`",
                    "`policy.sensitive_credentials`",
                ],
                "focus": "Look for a request to disclose the credential itself.",
            },
            criteria=NoulCriteria(
                true={
                    "what": "Asks the recipient to disclose a listed credential",
                    "examples": [
                        "Reply with your password",
                        "Send us your API key",
                    ],
                },
                false={
                    "what": "Does not ask the recipient to disclose a credential",
                    "not_for": "A legitimate instruction to reset a credential",
                    "examples": ["Use this link to reset your password"],
                },
            ),
        ),
        "sender_identity_mismatch": Noul(
            instructions={
                "question": "Does the claimed sender identity conflict with its domain?",
                "compare": [
                    "`ticket.sender.display_name`",
                    "`ticket.sender.email`",
                ],
                "focus": "Compare the named organization with the email domain.",
            },
            criteria=NoulCriteria(
                true={
                    "what": "Claims an organization unrelated to the email domain",
                    "examples": ["Acme Payroll sent from claim-bonus.example"],
                },
                false={
                    "what": "The identity and domain agree or make no conflicting claim",
                    "examples": ["Acme Payroll sent from acme.example"],
                },
            ),
        ),
        "unexpected_reward": Noul(
            instructions={
                "question": "Does the message announce an unexpected reward?",
                "inspect": "`ticket.message`",
                "focus": "Look for an unsolicited prize, payment, or reward claim.",
            },
            criteria=NoulCriteria(
                true={
                    "what": "Announces an unrequested prize, payment, or reward",
                    "examples": ["You were selected for a $1,000 bonus"],
                },
                false={
                    "what": "Contains no reward claim or discusses an expected payment",
                    "not_for": "A customer asking about a known refund or payroll deposit",
                    "examples": ["When will my approved refund arrive?"],
                },
            ),
        ),
        "refund_requested": Noul(
            instructions={
                "question": "Does the customer explicitly request a refund or credit?",
                "inspect": "`ticket.message`",
                "focus": "Require a requested remedy, not a billing complaint alone.",
            },
            criteria=NoulCriteria(
                true={
                    "what": "Directly asks for money back or an account credit",
                    "examples": ["Please refund the duplicate charge"],
                },
                false={
                    "what": "Does not ask for a refund or credit",
                    "not_for": "A complaint or billing question without a requested remedy",
                    "examples": ["Why was I charged twice?"],
                },
            ),
        ),
        "mentions_open_order": Noul(
            instructions={
                "question": "Does the message refer to a supplied open order?",
                "compare": [
                    "`ticket.message`",
                    "`customer.open_orders`",
                ],
                "focus": "Match an order id or other identifying details.",
            },
            criteria=NoulCriteria(
                true={
                    "what": "Refers to an open order by id or identifying details",
                    "examples": ["Where is order A-104?"],
                },
                false={
                    "what": "Does not identify any supplied open order",
                    "not_for": "A generic order question with no matching details",
                    "examples": ["How long does shipping usually take?"],
                },
            ),
        ),
        "frustration": Score(
            instructions={
                "question": "How frustrated does the customer appear?",
                "inspect": "`ticket.message`",
                "focus": "Judge expressed frustration, not issue severity.",
            },
            criteria=[
                {
                    "what": "Calm and matter-of-fact",
                    "signals": ["Neutral wording", "No complaint about the experience"],
                },
                {
                    "what": "Frustrated but civil",
                    "signals": ["Expresses annoyance", "Remains constructive"],
                },
                {
                    "what": "Very angry or threatening to leave",
                    "signals": ["Hostile language", "Threatens cancellation or churn"],
                },
            ],
        ),
    }

    with TypeSafeClient() as client:
        response = client.system_one(
            state=state,
            questions=questions,
        )

    # Compose independent spam signals with weights controlled by code.
    answers = response.answers
    spam_risk = (
        0.45 * answers["requests_credentials"].noul
        + 0.30 * answers["sender_identity_mismatch"].noul
        + 0.25 * answers["unexpected_reward"].noul
    )

    # Escalate uncertain judgments instead of guessing.
    spam_is_uncertain = 0.4 < spam_risk < 0.6
    if spam_is_uncertain or answers["topic"].confidence < 0.75:
        return route_to_human_review(ticket)
    if spam_risk >= 0.6:
        return quarantine_as_spam(ticket)

    # Let code decide which speculative answers matter on this path.
    if answers["topic"].choice == "billing":
        return route_to_billing(
            ticket,
            refund_requested=answers["refund_requested"].noul >= 0.7,
        )
    if answers["topic"].choice == "orders":
        return route_to_orders(
            ticket,
            mentions_open_order=answers["mentions_open_order"].noul >= 0.7,
        )

    priority = (
        "high"
        if answers["frustration"].confidence >= 0.7
        and answers["frustration"].score >= 1.5
        else "normal"
    )
    return route_to_account_support(ticket, priority=priority)