Documentation

Décisions pilotées par schéma

Transforme un schéma JSON, ou un modèle pydantic, en questions Laya, et récupère des valeurs typées avec une confiance calibrée. C’est le pont qui fait de Laya un moteur de sortie structurée : tu décris la forme que tu veux, Laya y répond en une seule passe avant.

import laya

schema = {
    "type": "object",
    "properties": {
        "department": {"type": "string", "enum": ["billing", "support", "sales"],
                       "description": "Which team should handle this?"},
        "urgency": {"type": "integer", "minimum": 0, "maximum": 2},
        "needs_human": {"type": "boolean"},
    },
}

agent = laya.load("convaiinnovations/laya")
values = agent.decide("I was charged twice, refund me.", schema=schema)
# {"department": "billing", "urgency": 2, "needs_human": True}

Avec pydantic (installe laya[structured]) :

from typing import Literal
from pydantic import BaseModel

class Ticket(BaseModel):
    department: Literal["billing", "support", "sales"]
    urgency: Literal[0, 1, 2]
    needs_human: bool

ticket = agent.decide("I was charged twice, refund me.", schema=Ticket)

Le sous-ensemble pris en charge

Le niveau supérieur doit être un objet avec properties. Chaque propriété devient une question.

Chaque ligne ci-dessous est un schéma réel : tests/test_structured_docs.py compile la première colonne et asserte la question que le compilateur produit réellement, donc ce tableau ne peut pas dériver du code. Une cellule est soit un schéma de propriété seul, soit un appel à un point d’entrée.

Schéma JSON Question Laya Valeur renvoyée
{"enum": ["billing", "support"]} choice la valeur choisie, avec son type d’origine
{"const": "billing"} choice cette unique valeur
{"type": "boolean"} noul true / false
{"type": "integer", "minimum": 0, "maximum": 5} score le niveau de plus haute probabilité, sous forme d’entier
{"type": "number", "minimum": 0, "maximum": 5} score le niveau, sous forme d’entier
{"type": "string", "enum": ["low", "high"], "description": "How urgent?"} choice How urgent? est la formulation de la question
{"anyOf": [{"enum": ["x", "y"]}, {"type": "null"}]} choice comme la ligne enum simple ; une absence de réponse omet la clé
{"oneOf": [{"type": "boolean"}, {"type": "null"}]} noul comme la ligne boolean simple
{"type": ["integer", "null"], "minimum": 1, "maximum": 3} score comme la ligne entier borné simple

Literal[...] et Optional[...] sont les écritures pydantic des lignes enum et anyOf : questions_from_pydantic les rend sous ces formes et les mêmes lignes s’appliquent.

title n’est pas lu. pydantic en met un sur chaque champ de model_json_schema() que tu l’aies demandé ou non, et un nom par propriété ne peut pas étiqueter les choix par option dont une question est construite, donc le levier de formulation est description — voir Comment ça se mappe en interne ci-dessous.

La projection est exacte : un enum: [1, 2, 3] renvoie 2, pas "2" ; un entier borné renvoie un niveau entre minimum et maximum ; un booléen est noul >= 0.5.

Rejets

Un schéma auquel on ne peut pas répondre depuis un ensemble d’options fixe lève laya.structured.SchemaError (une ValueError) en nommant le chemin exact. Chaque ligne est exécutée aussi, avec le champ nommé name :

schéma de propriété message
{"type": "string"} properties.name: a free string cannot be a fixed option set; use 'enum' or a boolean
{"type": "array", "items": {"type": "string"}} properties.name: arrays are not supported; ask one field per element
{"type": "object", "properties": {"inner": {"type": "boolean"}}} properties.name: nested objects are not supported; flatten the schema
{"$ref": "#/definitions/node"} properties.name: $ref/recursion is not supported; flatten the schema
{"enum": [1, "1"]} properties.name: enum values produce duplicate choice labels
{"enum": []} properties.name: 'enum' must not be empty
{"type": "number"} properties.name: a numeric field needs integer 'minimum' and 'maximum' to become a score
{"type": "integer", "minimum": 5, "maximum": 2} properties.name: 'maximum' 2 is below 'minimum' 5
{"type": "integer", "minimum": 0, "maximum": 10} properties.name: 11 levels exceeds MAX_SCORE_LEVELS=10; narrow the range or use an enum
{"anyOf": [{"type": "string"}, {"type": "integer"}]} properties.name: only 'Optional[...]' unions (one non-null branch) are supported, got 2
{"type": ["string", "integer"]} properties.name: 'type' has multiple non-null types; unions are not supported
{"format": "date"} properties.name: unsupported schema {'format': 'date'}
"boolean" properties.name: property must be an object, got str

Les points d’entrée eux-mêmes rejettent ceux-ci :

appel message
plan_from_json_schema("not a schema") expected a JSON schema object, got str
plan_from_json_schema({"type": "object"}) the top level must be an object with 'properties'
plan_from_json_schema({"type": "object", "properties": {}}) 'properties' must be a non-empty object
plan_from_json_schema({"type": "object", "properties": {"p%d" % i: {"type": "boolean"} for i in range(33)}}) 33 properties exceeds MAX_PROPERTIES=32
plan_from_json_schema({"type": "object", "properties": {"name": {"enum": ["v%d" % i for i in range(33)]}}}) properties.name: 33 options exceeds MAX_OPTIONS=32
decide(None, "I was charged twice.", schema=42) expected a JSON schema dict or a pydantic model, got int

Limites : MAX_PROPERTIES = 32, MAX_OPTIONS = 32, MAX_SCORE_LEVELS = 10.

L’API

fonction rôle
laya.decide(runner, state, schema=..., *, questions=..., return_details=..., min_confidence=..., **predict_kwargs) la fonction libre, fonctionne pour Agent et Router
agent.decide(state, schema=..., ...) / router.decide(state, schema=..., ...) méthodes de commodité
laya.decide_batch(runner, states, schema=..., ...) / agent.decide_batch(...) / router.decide_batch(...) la même chose sur de nombreux états, un seul appel en lot
questions_from_json_schema(schema) schéma vers questions Laya
questions_from_pydantic(model) modèle pydantic vers questions (nécessite pydantic)
answers_to_json(answers, schema) projette les réponses brutes sur les valeurs du schéma
answer_to_pydantic(model, answers) projette les réponses brutes dans une instance pydantic
plan_from_json_schema(schema) le plan de champs validé (avancé)

Passe exactement l’un de schema ou questions. Avec questions, decide renvoie les réponses brutes au lieu de projeter. Les arguments nommés supplémentaires sont transmis à predict, donc les hooks, model=, task= et le budget de jetons fonctionnent tous :

router.decide(state, schema=Ticket, model="multilingual", hooks=[Metrics()])

Évaluer de nombreux états

decide_batch est la forme pour le débit : le schéma est planifié une fois et ses questions tournent sur chaque état via predict_batch, donc les états partagent des passes avant au lieu d’une par appel. Les résultats reviennent dans l’ordre d’entrée, projetés exactement comme decide projette, et return_details=True donne un DecisionResult par état :

values = agent.decide_batch(ticket_texts, schema=Ticket)          # values[i] matches ticket_texts[i]
results = router.decide_batch(states, schema=Ticket, return_details=True, batch_size=64)

Sur un Router, chaque état est quand même routé tout seul, donc un appel peut couvrir plusieurs checkpoints. Les arguments nommés atteignent predict_batch, donc batch_size=, model= et les hooks fonctionnent comme pour decide. Agent, ONNXAgent et Router l’ont tous ; un runner sans predict_batch lève TypeError plutôt que de retomber silencieusement sur une boucle — appelle decide par état dans ce cas. Le traitement par lots peut décaler les argmax limites de la même façon que predict_batch ; le README enregistre les accélérations mesurées pour les deux appareils.

Confiance et probabilités

Par défaut, decide ne renvoie que les valeurs. Passe return_details=True pour un DecisionResult avec la confiance par champ, les probabilités, les réponses brutes, et l’usage et le routage de l’appel :

result = agent.decide(state, schema=Ticket, return_details=True)
result.values["department"]            # "billing"
result.answer_confidence["department"] # 0.94  max(p): the quantity min_confidence gates on
result.confidence["department"]        # 0.71  normalized entropy, which depends on label count
result.probabilities["department"]     # {"billing": 0.94, "support": 0.06, "sales": 0.0}
result.usage                           # {"input_tokens": ..., "output_tokens": 0,
                                       #  "state_tokens": ..., "state_tokens_dropped": ...,
                                       #  "truncated": ..., "truncated_questions": [...]}
result.routing                         # the Router decision, when a Router answered

usage est le bloc que predict() a construit, transmis tel quel : input_tokens additionne l’état sur une ligne par question et output_tokens vaut toujours 0 parce que rien n’est généré, tandis que les quatre autres clés sont le rapport de troncature (#174) – state_tokens est ce dont tout l’état sérialisé a besoin, state_tokens_dropped la part la plus importante que le head d’une question donnée a abandonnée, et truncated / truncated_questions disent laquelle. Une septième clé, options, n’est présente que lorsque le budget du head a laissé les options d’une question partager un même intervalle de tokens (#538) ; docs/http-api.md documente à la fois ce bloc et ce champ comme clés de réponse, et tests/test_structured_docs.py rattache la liste de cette page au code qui la construit.

confidence et answer_confidence sont des quantités différentes, et les noms suivent les définitions. answer_confidence est max(p), la masse de probabilité sur la réponse rapportée. C’est ce que l’échelonnage de température ajuste, ce sur quoi chaque chiffre de calibration de ce dépôt est calculé, et ce à quoi min_confidence est comparé — ce qui est la raison de gater sur elle plutôt que sur confidence. confidence est l’entropie normalisée, qui dépend du nombre d’options de la question : tests/test_confidence.py épingle qu’une distribution à deux options revient à 0.90 sur un noul et 0.53 sur un choice équivalent, donc elle ne se compare pas à un seuil. Un champ qui n’a rapporté aucune answer_confidence utilisable se mappe sur None, ce qui n’est pas la même chose qu’un 0.0 rapporté.

Gate sur la même quantité que la porte utilise :

if result.answer_confidence["department"] < 0.6:
    result.values["department"] = "human-review"

Que answer_confidence soit le bon nombre sur lequel filtrer n’est pas la même chose que son caractère fiable comme probabilité. La lire comme « environ c des réponses renvoyées à c sont correctes » ne tient qu’après avoir ajusté et validé les températures sur des données mises de côté pour ce checkpoint et cette forme de question. Les checkpoints livrés sont trop confiants tels quels et laya-multilingual est livré sans aucune température ajustée — voir les sections Calibration et limites honnêtes du README, et le notebook d’ajustement fin pour la boucle d’ajustement. Ajuste avant de te fier au niveau ; rapporte-le parce que c’est la quantité que la porte et le harnais d’évaluation utilisent tous les deux.

Comment ça se mappe en interne

  • Enum et Literal deviennent des questions choice avec les valeurs comme libellés de chaîne ; le libellé est remappé vers la valeur d’origine en sortie, donc les entiers restent des entiers.
  • Un entier borné devient une question score avec un niveau par valeur ; la valeur renvoyée est minimum + argmax.
  • Un booléen devient une question noul ; la valeur est noul >= 0.5.
  • Une description devient les instructions de la question, donc une bonne description est ce qui rend la décision exacte. Cela suit la même règle que le guide des hooks : sois explicite sur ce que signifie chaque option.
  • Une branche null est supprimée avant que le champ ne soit planifié, donc Optional[X] pose exactement la question que pose X. La clé du champ est simplement absente des valeurs quand il n’y a pas de réponse pour lui, ce qui rend sûr le fait de déclarer un champ optionnel sans changer ce que le modèle voit.

Voir aussi