Documentação

Como construir com a TypeSafe

Concebe software com IA mantendo o código no controlo 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. Não gera código nem escolhe a sua própria ação seguinte. Fornece primitivas de IA que se integram no software, de modo que o código mantém o controlo enquanto o modelo trata dos julgamentos de senso comum sobre dados não estruturados.

Três arquiteturas de software

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

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

Um agente processa instruções e escolhe o seu passo seguinte. Isto funciona bem quando uma pessoa está a monitorizar o processo, mas cada ciclo introduz mais uma oportunidade de descarrilar.

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

Software tradicional, agentes e software com IA representados 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 as probabilidades obedecem aos tipos de software estruturados e ao esquema JSON que o teu código espera, por isso nunca tem de recuperar um valor a partir de prosa gerada.

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 primitiva.

Comparável

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

Rápido

A maioria das consultas conclui-se em cerca de 100 ms. O System One é suficientemente rápido para percursos de pedidos em tempo real e interfaces de utilizador.

Confiança calibrada

O RLCD comunica a incerteza através de probabilidades calibradas em vez de tender para o excesso de confiança.

Autoconsistente

O System One foi concebido para devolver respostas estáveis em avaliações repetidas. Vê o cookbook de autoconsistência.

Como cada saída está restringida às opções fornecidas, o modelo devolve uma distribuição de probabilidade completa sobre essas opções em vez de inventar um valor fora do esquema. O objetivo da TypeSafe é um rácio de inteligência para velocidade e custo superior a 100×; a aposta de fundo é que inteligência mais barata vai criar muito mais procura.

Conceber um fluxo de trabalho com o System One

  1. Usa código quando puderes

    Mantém o trabalho determinístico no código. É fiável e barato. Evita os ciclos while de agentes quando um fluxo de trabalho de software consegue expressar o mesmo comportamento.

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

    Percorre os padrões do System One para veres formas delimitadas de compor decisões do modelo com código.

  2. Decompõe o estado de entrada

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

    Exemplo: enviar 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. Usa estrutura no estado de entrada

    Usa JSON aninhado para os campos state e questions. Aponta as perguntas para valores específicos quando isso elimina ambiguidade, e inclui os carateres de acento grave em torno de cada caminho dentro da pergunta.

    Exemplo: referenciar um valor aninhado

    Usa um caminho com pontos e índices entre acentos graves 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. Decompõe as perguntas

    Faz as perguntas mais explícitas, estreitas, específicas e atómicas que conseguires. Divide perguntas complexas ou mal definidas em perguntas separadas, cada uma das quais avalia uma única propriedade.

    Exemplo: decompor a deteçã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: verificar um traço de chamadas a ferramentas
    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. Usa estrutura nas perguntas

    Mantém as perguntas curtas. instructions e criteria são normalmente strings e, para uma pergunta curta e inequívoca, uma string é tudo o que precisas. Também podem ser objetos ou arrays. Coloca a pergunta num 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 com nome ao lado da pergunta, onde o teu código os pode acrescentar ou trocar sem reescrever a pergunta.
    • Parte da pergunta vem do teu código. Quando um valor vem de uma base de dados, coloca-o no seu próprio campo em vez de o inserir num modelo de string.
    • Várias perguntas têm instruções semelhantes. Um pedido recebe um estado e pode incluir várias perguntas. Acrescentar dados suplementares pode ajudar a tornar as perguntas distintas.
    Exemplo: referenciar um registo do teu código

    Este Noul compara um currículo no estado com um registo de uma base de dados de candidatos. O registo é colocado em potential_duplicate tal como está, e a pergunta refere-se 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” provenientes do código podem mudar ao longo do tempo. A “question” refere-se a eles através de acentos graves.

    As descrições dentro de criteria também podem ser objetos. Numa Choice, a descrição de cada opção pode ser um objeto que diz o que a opção abrange, o que pertence a uma opção diferente e alguns exemplos. Usa os mesmos nomes de campo em todas as opções para que o modelo as possa comparar diretamente.

    Exemplo: definir critérios 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 desenvolvido:

    • Noul compara um currículo com vários registos de candidatos, uma pergunta por registo, com as perguntas construídas no código.
    • Choice descreve duas opções facilmente confundíveis, com o que cada uma abrange, 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 partilhada, em que se faz a mesma bateria de perguntas sobre cada campo de um registo extraído.

    Uma pergunta ou um critério curto e inequívoco pode continuar a ser uma string. Acrescenta estrutura quando esta separa orientações que, de outro modo, se misturariam. Para o conjunto completo de locais onde a estrutura é aceite, vê Avançado: estrutura.

  6. Faz muitas perguntas

    Faz muitas perguntas estreitas e independentes sobre o mesmo estado num único pedido. É assim que maximizas a eficácia e a inteligência por euro com a API: as perguntas são executadas em paralelo e o código pode combinar os seus sinais sem acrescentar idas e voltas em série ao modelo.

    Vê o padrão de Fan-Out especulativo e o cookbook de perguntas em paralelo.

  7. Combina as saídas das perguntas no código (ou alimenta-as para um modelo clássico de ML)

    Combina respostas independentes com regras determinísticas ou somas ponderadas. Para composição aprendida, usa as probabilidades como características num modelo clássico de machine learning a jusante.

    Exemplo: combinar 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 os julgamentos individuais ao combiná-los. Se não tens etiquetas para um modelo a jusante, usa um conjunto de modelos de raciocínio caros para as gerar; o cookbook de AutoResearch mostra como treinar um modelo clássico com saídas do System One.

  8. Encaminha com base na incerteza

    Faz o código tomar ações diferentes para respostas com confiança e sem confiança. Escalona os casos incertos para uma pessoa ou para um modelo de raciocínio mais caro. Testa os limiares traçando a confiança em função da precisão nos teus dados.

    Exemplo: encaminhar 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)

    Vê Confiança e Confidence-Gated Routing para escolheres limiares e os ajustares ao risco de cada ação.

Reunir tudo

Este fluxo de trabalho de tickets de apoio mantém o trabalho determinístico no código, envia apenas o contexto estruturado relevante, avalia muitas perguntas atómicas num único pedido e compõe as respostas com portas de confiança explícitas.

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)