Documentation

Choice

Un Choice est un type de question System One qui sélectionne une option dans un ensemble défini. La réponse inclut l’option choisie, une probabilité pour chaque option et la confiance.

Utilise un Choice quand la réponse est l’une d’un ensemble fixe d’options. Par exemple, quelle équipe traite un ticket, à quelle catégorie appartient un produit, ou dans quel langage est écrit un extrait de code. Si la réponse est une position sur un spectre, utilise un Score. Si c’est un oui ou un non, utilise un Noul. Choisis un type de question compare les trois.

Une réponse Choice est l’option choisie, dans choice. Le modèle renvoie aussi une probabilité pour chaque option dans probabilities, et une valeur confidence pour l’option choisie.

Exemples de questions :

"What programming language is this code written in"
  → options: python, javascript, typescript, go, rust, other

"What type of meeting is this based on the title and description"
  → options: standup, planning, retrospective, one on one, brainstorm, none of the above

"Which product category does this item belong to"
  → options: electronics, clothing, home garden, food and beverage

Structure de la requête

Le corps de la requête POST vers l’API TypeSafe a une structure précise. Le niveau supérieur a trois champs : state, le contenu à évaluer ; model ; et questions, une map des ids de question que tu choisis vers des objets question. Chaque question Choice a les champs suivants :

  • type : toujours "choice".
  • instructions : la question à laquelle le modèle répond.
  • criteria : les options de réponse, sous forme de map. Chaque clé est un nom d’option et chaque valeur est une description de cette option.

Voici une requête où l’état est un ticket de support d’une boutique de chaussures en ligne et la question est quelle équipe doit le traiter :

request
{
  "state": "My running shoes arrived in the wrong size. Can I swap them for a size 10?",
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle this?",
      "criteria": {
        "returns": "Exchanges, wrong or damaged items",
        "shipping": "Delivery status, delays, lost packages",
        "billing": "Charges, invoices, payment problems"
      }
    }
  }
}

Tu choisis l’id de question, department ici. La réponse est renvoyée sous le même id. Le modèle ne voit jamais l’id de question. Les noms d’option et leurs descriptions sont tous deux envoyés au modèle, alors écris des descriptions qui séparent les options les unes des autres.

Nos SDK clients fournissent des questions typées. En Python, la même question est un Choice :

from typesafe_sdk import Choice, TypeSafeClient

with TypeSafeClient() as client:
    response = client.system_one(
        state="My running shoes arrived in the wrong size. Can I swap them for a size 10?",
        questions={
            "department": Choice(
                instructions="Which team should handle this?",
                criteria={
                    "returns": "Exchanges, wrong or damaged items",
                    "shipping": "Delivery status, delays, lost packages",
                    "billing": "Charges, invoices, payment problems",
                },
            ),
        },
    )

    print(response.answers["department"].choice)

Utilise la méthode system_one ou l’endpoint https://api.typesafe.ai/v1/systemone pour appeler un modèle System One. Le champ model sélectionne quel modèle traite la requête. Construire avec TypeSafe indique où appeler cela dans ton code.

Utilise l’un de nos SDK clients ou appelle directement l’API HTTP. Si un agent de codage écrit l’intégration pour toi, installe d’abord le skill d’agent TypeSafe pour qu’il connaisse les formes de la requête et de la réponse.

Structure de la réponse

La réponse a une entrée dans answers par question, sous les ids de la requête. Voici la réponse à la requête d’exemple ci-dessus :

{
  "model": "jev-1.13.0",
  "answers": {
    "department": {
      "type": "choice",
      "choice": "returns",
      "confidence": 1.0,
      "probabilities": {
        "shipping": 0.0,
        "returns": 1.0,
        "billing": 0.0
      }
    }
  },
  "usage": {
    "input_tokens": 328,
    "output_tokens": 34
  }
}

Outre type, chaque réponse Choice a trois valeurs :

  • choice : l’option avec la probabilité la plus élevée.
  • probabilities : la distribution complète des probabilités sur toutes les options. La somme de toutes les valeurs vaut 1.
  • confidence : un nombre de 0 à 1 calculé à partir de la façon dont probabilities se répartit. Une forme plate, avec la probabilité répartie sur plusieurs options, signifie une confiance faible. Un pic unique sur une option signifie une confiance élevée.

Ce ticket est facile, donc toute la probabilité est sur returns et la confiance vaut 1.0. Un ticket qui mentionne une mauvaise taille et un remboursement manquant répartirait la probabilité entre returns et billing, et la confiance baisserait.

Bonne pratique : pose plusieurs questions par appel

Pose toutes les questions Choice dont ton code pourrait avoir besoin en une seule requête plutôt qu’une requête par question. Les questions sont évaluées en parallèle. Ajouter des questions ne change presque pas le temps de réponse, et le code peut ignorer les réponses dont il n’a pas besoin. Les questions en plus coûtent quand même des jetons. Poser plusieurs questions ensemble l’explique en détail ; la section suivante montre cinq questions Choice en un seul appel.

La même logique vaut pour les options à l’intérieur d’une seule question Choice. Une question Choice accepte jusqu’à 255 options, et ajouter des options coûte quelques jetons chacune, alors donne au modèle la liste complète des équipes, catégories ou produits plutôt qu’une présélection. Ajoute une option other ou none of the above quand la liste risque de ne pas couvrir toutes les entrées, pour que le modèle puisse dire qu’aucune des autres ne convient.

Pour classer des documents dans une hiérarchie profonde ou une grande taxonomie, enchaîne les questions Choice niveau par niveau. Le cookbook Classification hiérarchique montre comment exécuter une recherche en faisceau sur les probabilités de Choice, en gardant les K meilleurs chemins candidats à chaque niveau au lieu de s’engager sur un seul chemin glouton.

Un exemple plus complexe

L’exemple de base ci-dessus route un ticket vers une équipe. Un système de support plus grand pourrait aussi avoir besoin du motif de retour, du problème de livraison, de ce que veut le client et du ton du client.

La requête ci-dessous pose cinq questions Choice sur un ticket plus ambigu que le premier : il concerne trois équipes et ne dit pas ce que veut le client.

request
{
  "state": "Shoes arrived two weeks late and in the wrong size. Also I see two charges of $120 on my card. What are you going to do about this?",
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle this?",
      "criteria": {
        "returns": "Exchanges, wrong or damaged items",
        "shipping": "Delivery status, delays, lost packages",
        "billing": "Charges, invoices, payment problems"
      }
    },
    "return_reason": {
      "type": "choice",
      "instructions": "If the customer wants to return something, why?",
      "criteria": {
        "wrong_size": "The item doesn't fit",
        "wrong_item": "A different product was delivered",
        "damaged": "The item arrived broken or faulty",
        "changed_mind": "The item is fine, the customer no longer wants it",
        "other": "A return reason that fits none of the above"
      }
    },
    "shipping_issue": {
      "type": "choice",
      "instructions": "If this is a shipping problem, which kind is it?",
      "criteria": {
        "not_delivered": "The package never arrived",
        "delayed": "The package is late but still on its way",
        "wrong_address": "The package went to the wrong place",
        "damaged_in_transit": "The package arrived damaged",
        "other": "A shipping problem that fits none of the above"
      }
    },
    "requested_resolution": {
      "type": "choice",
      "instructions": "What does the customer want to happen?",
      "criteria": {
        "exchange": "Swap the item for a different one",
        "refund": "Money back",
        "replacement": "The same item sent again",
        "information": "Just an answer, no action needed"
      }
    },
    "tone": {
      "type": "choice",
      "instructions": "What is the customer's tone?",
      "criteria": {
        "calm": null,
        "frustrated": null,
        "angry": null
      }
    }
  }
}

Deux de ces questions Choice sont spéculatives : return_reason ne compte que si le department est returns, et shipping_issue ne compte que si c’est shipping. La question tone utilise des descriptions null parce que les noms des options sont clairs en eux-mêmes.

La réponse de TypeSafe :

{
  "model": "jev-1.13.0",
  "answers": {
    "department": {
      "type": "choice",
      "choice": "returns",
      "confidence": 0.42,
      "probabilities": {
        "shipping": 0.04,
        "billing": 0.35,
        "returns": 0.61
      }
    },
    "return_reason": {
      "type": "choice",
      "choice": "wrong_size",
      "confidence": 1.0,
      "probabilities": {
        "other": 0.0,
        "wrong_size": 1.0,
        "changed_mind": 0.0,
        "damaged": 0.0,
        "wrong_item": 0.0
      }
    },
    "shipping_issue": {
      "type": "choice",
      "choice": "delayed",
      "confidence": 0.67,
      "probabilities": {
        "wrong_address": 0.0,
        "other": 0.26,
        "not_delivered": 0.0,
        "damaged_in_transit": 0.0,
        "delayed": 0.74
      }
    },
    "requested_resolution": {
      "type": "choice",
      "choice": "refund",
      "confidence": 0.2,
      "probabilities": {
        "replacement": 0.34,
        "refund": 0.4,
        "information": 0.02,
        "exchange": 0.24
      }
    },
    "tone": {
      "type": "choice",
      "choice": "frustrated",
      "confidence": 0.76,
      "probabilities": {
        "frustrated": 0.84,
        "angry": 0.16,
        "calm": 0.0
      }
    }
  },
  "usage": {
    "input_tokens": 589,
    "output_tokens": 212
  }
}

Chaque question est répondue pour elle-même face au ticket :

  • La réponse department est returns avec une probabilité de 0.61, mais billing a 0.35 à cause du double débit. Le ticket appartient à deux équipes, et la confiance partagée de 0.42 le reflète.
  • Le return_reason est wrong_size avec une confiance de 1.0, ce qui est attendu car le ticket le dit clairement.
  • La réponse shipping_issue est partagée entre delayed et other. C’est une question spéculative et department n’est pas ressorti comme shipping, donc le code peut l’ignorer, comme le montre l’extrait de code d’exemple ci-dessous.
  • La réponse requested_resolution penche vers refund à 0.40, replacement et exchange se partageant l’essentiel du reste, et la confiance est de 0.20. Le double débit suggère un remboursement, la mauvaise taille suggère un échange, et le client ne dit jamais ce qu’il veut.
  • La réponse tone est frustrated avec une probabilité de 0.84 et une confiance de 0.76.

Le code d’exemple ci-dessous lit les réponses dont il a besoin, ignore le reste, et traite une réponse à faible confiance comme une raison de demander plutôt que d’agir :

from typesafe_sdk import Choice, TypeSafeClient

TRIAGE_QUESTIONS = {
    "department": Choice(
        instructions="Which team should handle this?",
        criteria={
            "returns": "Exchanges, wrong or damaged items",
            "shipping": "Delivery status, delays, lost packages",
            "billing": "Charges, invoices, payment problems",
        },
    ),
    "return_reason": Choice(
        instructions="If the customer wants to return something, why?",
        criteria={
            "wrong_size": "The item doesn't fit",
            "wrong_item": "A different product was delivered",
            "damaged": "The item arrived broken or faulty",
            "changed_mind": "The item is fine, the customer no longer wants it",
            "other": "A return reason that fits none of the above",
        },
    ),
    "shipping_issue": Choice(
        instructions="If this is a shipping problem, which kind is it?",
        criteria={
            "not_delivered": "The package never arrived",
            "delayed": "The package is late but still on its way",
            "wrong_address": "The package went to the wrong place",
            "damaged_in_transit": "The package arrived damaged",
            "other": "A shipping problem that fits none of the above",
        },
    ),
    "requested_resolution": Choice(
        instructions="What does the customer want to happen?",
        criteria={
            "exchange": "Swap the item for a different one",
            "refund": "Money back",
            "replacement": "The same item sent again",
            "information": "Just an answer, no action needed",
        },
    ),
    "tone": Choice(
        instructions="What is the customer's tone?",
        criteria={"calm": None, "frustrated": None, "angry": None},
    ),
}

def triage(ticket: str) -> None:
    with TypeSafeClient() as client:
        response = client.system_one(
            state=ticket,
            questions=TRIAGE_QUESTIONS,
        )
    answers = response.answers

    department = answers["department"]
    if department.confidence < 0.3:
        # Not clear which team to send to. Let a person decide.
        send_to_manual_triage(ticket)
        return

    if department.choice == "returns":
        # return_reason answer is only used here
        assign(ticket, team="returns", issue=answers["return_reason"].choice)
    elif department.choice == "shipping":
        # shipping_issue answer is only used here
        assign(ticket, team="shipping", issue=answers["shipping_issue"].choice)
    else:
        assign(ticket, team="billing")

    # A second team with a real share of the probability gets a copy
    for team, probability in department.probabilities.items():
        if team != department.choice and probability > 0.25:
            notify(ticket, team=team)

    resolution = answers["requested_resolution"]
    if resolution.confidence < 0.5:
        # The customer hasn't said what they want. Ask, don't guess.
        ask_customer_what_they_want(ticket)
    elif resolution.choice == "refund":
        flag_for_refund_approval(ticket)

    if answers["tone"].choice == "angry":
        flag_for_senior_agent(ticket)

Pour le ticket ci-dessus, cela assigne le ticket à l’équipe returns avec l’issue wrong_size, envoie une copie à l’équipe billing parce que sa part de 0.35 dépasse le seuil de 0.25, et demande au client ce qu’il veut parce que la confiance de 0.20 sur la résolution est sous 0.5. Le code n’utilise pas la réponse shipping_issue.

Une requête, cinq réponses, et la logique de routage est faite d’instructions if ordinaires. Si tu as ensuite besoin de connaître la langue du client, ou le produit concerné par le ticket, ajoute une autre question Choice à TRIAGE_QUESTIONS ; le nombre de requêtes reste à un.

La démo de l’assistant domotique évalue chaque requête utilisateur contre une longue liste de questions Choice en un seul appel : la catégorie de requête, la pièce, l’appareil et l’action. La plupart de ces questions sont sans rapport avec une requête donnée et le code les ignore.

Instructions et criteria structurés

Commence par une description d’une ligne par option. Quand deux options se ressemblent et que le modèle les confond sans cesse, décris chacune avec un objet au lieu d’une chaîne. Donne-lui des champs pour ce que l’option couvre, ce qui appartient plutôt à une option voisine, et quelques entrées d’exemple.

Les deux options de réponse ci-dessous, return_policy et return_status, sont faciles à confondre. Un ticket sur l’une ou l’autre peut mentionner des retours et des remboursements, alors chaque option dit pour quoi elle n’est pas faite.

request
{
  "state": "I sent the shoes back a week ago. When do I get my money?",
  "questions": {
    "return_topic": {
      "type": "choice",
      "instructions": {
        "question": "Which returns topic is the customer asking about?",
        "focus": "Classify the information the customer wants."
      },
      "criteria": {
        "return_policy": {
          "what": "Whether and how an item can be returned",
          "not_for": "Progress of a return already sent",
          "examples": [
            "Can I return shoes I've worn once?",
            "How long do I have to return an order?"
          ]
        },
        "return_status": {
          "what": "Progress of a return already sent",
          "not_for": "Whether and how an item can be returned",
          "examples": [
            "Has my return arrived yet?",
            "When will my refund be paid?"
          ]
        }
      }
    }
  }
}

La réponse est return_status avec une confiance de 1.0 :

{
  "model": "jev-1.13.0",
  "answers": {
    "return_topic": {
      "type": "choice",
      "choice": "return_status",
      "confidence": 1.0,
      "probabilities": {
        "return_policy": 0.0,
        "return_status": 1.0
      }
    }
  },
  "usage": {
    "input_tokens": 407,
    "output_tokens": 32
  }
}

Les noms de champ question, focus, what, not_for et examples ne font pas partie de l’API, et aucun n’est réservé. Tu les choisis, de la même façon que tu choisis les noms d’option. Le modèle voit les noms avec les valeurs, alors utilise des noms courts qui étiquettent ce qui suit.