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
Literaldeviennent des questionschoiceavec 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
scoreavec un niveau par valeur ; la valeur renvoyée estminimum + argmax. - Un booléen devient une question
noul; la valeur estnoul >= 0.5. - Une
descriptiondevient 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
nullest supprimée avant que le champ ne soit planifié, doncOptional[X]pose exactement la question que poseX. 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
- Hooks de prédiction : observe, façonne, met en cache ou filtre les décisions que ceci produit.
- Primitives de décision :
choice,scoreetnoulen profondeur. - LangChain et LangGraph :
LayaDecisionest cet appel sous forme de runnable dans une chaîne.