Skills
Une skill est une compétence : on la sollicite pour un besoin, et elle propose des actions parmi lesquelles choisir. Sa déclaration dit comment obtenir ces actions, les classer, les présenter, les réserver et les exécuter.
skills:
- name: refund
cognitive: # QUAND s'en servir — c'est aussi ce que lit le modèle de classement
objective: "proposer le remboursement autorisé pour la facture du ticket"
diagnose: [le motif du client, la ligne de facture]
params: # ce que la skill lit, et QUI peut le fixer
invoice: {type: id, set_by: [read]}
max_amount: {type: price, set_by: [read]}
invoke: # comment obtenir les actions possibles
method: proposeRefund
args: {invoice: "$params.invoice"}
kinds: # comment les classer
partial:
when: "$fields.amount < $fields.invoice_total" # deux champs de present, lus sur la ligne
select: {guards: [{approval: {role: support-manager}}]}
present: # ce qu'on garde, typé
fields:
name: {from: label, type: text, role: title}
amount: {from: amount, type: price, role: billed}
invoice_total: {from: invoice_total, type: price}
select: # comment les réserver
max_items: 1
unique: "$params.invoice"
guards:
- {name: plafond-facture, require: "$item.fields.amount <= $params.max_amount"}
- check: checkRefundPolicy
- {approval: {role: support-manager}, when: "$item.fields.amount > 50"}
submit: # comment les exécuter
- name: pay
tool: createRefund
args: {body.amount: "$item.fields.amount", body.idempotency_key: "$action.key"}
output: {id: {from: "$output.id", type: id}}
render: {component: cards}
output_filters: {drop: [card_number]}
per_profile: {agent-support: {select: {guards: [{policy: remboursements-du-jour}]}}}
cognitive
objective dit à quel besoin la skill répond ; diagnose, ce qu'il faut comprendre avant de
proposer. Ce n'est pas décoratif : c'est la description que lit seek_guidance pour proposer
la bonne skill.
params et set_by — d'où vient chaque valeur
Une skill ne lit que ses propres paramètres. C'est le playbook qui les lie à des lectures précédentes. Ainsi la même skill sert dans plusieurs parcours.
set_by dit qui peut fixer un paramètre :
| source | sens |
|---|---|
llm | le modèle, depuis la conversation |
person | la personne, dans un formulaire ou d'un clic |
read | une lecture de cette room |
result | le résultat d'un submit précédent |
identity | l'identité de celui qui agit |
Absent : [llm, person], comme aujourd'hui. Un montant set_by: [read] ne peut pas être inventé
par le modèle ; un destinataire set_by: [read, identity] ne peut pas être détourné par un
contenu malveillant.
Un paramètre peut avoir un default — pour une skill appelée hors de tout parcours. C'est une
valeur littérale : il est lu avant l'appel, quand aucun item n'existe encore.
Chaque paramètre a un type, pris dans la table des types. Une
valeur qui n'est pas de ce type n'est pas passée à la skill : un montant lu en texte ne devient pas
un prix.
invoke
invoke:
method: getOvertimeReport
args: {team: "$params.team"}
guards: [{permission: {profiles: [manager, rh]}}] # une lecture a aussi des gardes
Une skill dont l'opération est une lecture s'exécute tout de suite et laisse une trace. Ses
gardes se déclarent ici, sous invoke.guards : une lecture ne passe jamais par select.
invoke.method ne nomme qu'une opération des APIs de la station. Une capability de Convoy
lui-même se cite comme une opération, avec invoke.native : search (une recherche sémantique),
routine (le moteur de bundles), set (un ensemble composé à la demande). Une skill nomme l'une ou
l'autre, jamais les deux. search ne nomme pas de source. Sur quoi cherchent search et routine
n'est pas encore fixé : un index que la station remplit à part, pour un tenant qui n'a pas
d'opération de recherche, ou l'opération de recherche du tenant.
Il n'y a pas de catalogue dans la config. Ce que la station propose, c'est ce que rendent les
invoke de ses skills, à chaque appel, chez le tenant. Chercher dans ce que vend le tenant, c'est
une skill dont l'invoke est l'opération de recherche du tenant, la question passée en paramètre :
params:
q: {type: text, set_by: [llm, person]}
invoke:
method: searchProducts
args: {q: "$params.q"}
Un item se résout par ce qui a été montré. Une ref que la room a reçue d'un invoke est
connue ; son nom, son prix, son image et son lien viennent du present de la skill qui l'a
montrée. Une ref que la room n'a jamais vue est refusée : l'agent la montre d'abord, en appelant
la skill. Convoy ne garde pas de copie de ce que rend l'invoke d'un tenant.
kinds
Un kind se reconnaît par une condition sur les champs de l'action. Il peut changer select et
submit, et déclarer ses champs de ligne :
kinds:
pickup:
when: "$fields.type == 'pickup'"
submit: [{name: collect, mode: self}]
delivery:
when: "$fields.type == 'delivery'"
select:
guards:
- form: {address: {type: text, required: true}}
- check: checkDeliverable
- update: {price: getDeliveryPrice}
present:
fields:
name: {from: name, type: text, role: title}
type: {from: type, type: text}
Le when d'un kind se lit sur la ligne, avant qu'elle soit un item : $fields.<nom> y nomme
un champ déclaré dans le present.fields de la skill, et rien d'autre. Partout ailleurs, une
condition lit $item.fields.<nom>.
Les champs de ligne
Un choix qui appartient à une ligne, pas à la sélection — l'accompagnement d'un plat — est un
champ de ligne. Le kind le déclare sous fields, avec un type pris dans la
table des types :
kinds:
dish:
when: "$fields.category != 'nems'"
fields:
variante: {type: id, required: true, label: "Accompagnement"}
- Chaque kind a ses champs : un nem et un plat ne portent pas les mêmes.
- La personne les remplit ligne par ligne, dans un formulaire propre à la ligne. Une valeur hors de son type est refusée.
requiredretient le submit, pas l'ajout : une ligne entre dans la sélection sans réponse, et le dit ; elle ne part pas au tenant sans elle.- Le champ est sur l'item sous son nom (
$item.fields.variante), et l'appel qui envoie la ligne le lit par$line.fields.variante(la forme d'une ligne). Un nom déjà pris parpresent.fieldsest refusé au chargement.
present — des champs typés
Chaque champ a un nom choisi par la station, une source (from, le champ de la ligne du
tenant ; un chemin pointé lit dans un objet), un type, et parfois un rôle et une classe.
Tout ce qui porte une valeur prend son type dans la même table : un champ de present, un
champ de formulaire (une garde form, le formulaire du submit), un paramètre (params), une
sortie d'un appel (output), un champ d'une ligne envoyée au tenant, un champ lu par $read. Un
type hors de la table refuse la station au chargement.
| type | ce que fait Core |
|---|---|
text | l'affiche |
email, phone | l'écrit d'une seule façon avant qu'il identifie une room |
number, integer | vérifie qu'une réponse en est un (integer : un nombre entier) |
boolean | le vérifie ; dans le formulaire du submit, un boolean obligatoire est un consentement, qui n'accepte que oui |
price | dans la devise de la station ; le champ billed est le prix de la ligne |
quantity | un nombre entier |
id | un identifiant : du texte ou un nombre entier |
url | une adresse ; le champ link est la page de l'item chez le tenant |
image | l'adresse d'une image ; la première est l'image de la carte |
datetime | une date, ou une date et une heure, en texte ISO 8601 |
Le rôle dit à la carte quoi afficher où : title (le nom de la ligne, text ou id), billed
(le prix facturé, price), headline (le chiffre d'un rapport), link (la page de l'item chez le
tenant, url). Un champ par rôle, au plus. Un item peut avoir deux champs price ; un seul est
billed. Tous sont dans la devise de global.
- Seul ce qui est déclaré est lu. Sans champ
billed, l'item n'a pas de prix affiché ; sanstitle, la ligne montre sa référence. L'image de la carte est le premier champimage. - Chaque champ est sur l'item sous son nom (
$item.fields.<nom>). - Trois noms sont réservés :
nameest letitle,pricelebilled,image_urlla premièreimage.code,slugetidsont refusés : Core y écrit la référence de l'item. - Un
templatecommence par{tenant_url}ouhttp(s)://: relatif, il se résoudrait sur l'hôte qui dessine la carte.
page: {from: slug, type: url, role: link,
template: "https://maison-indigo.example.fr/article/{value}"}
Le template du lien n'a que deux marques : {value}, la valeur du champ (encodée), et
{tenant_url}, l'adresse du déploiement. Sans {value}, ou avec une autre marque, il est refusé.
Sans template, la valeur est le lien si c'est une adresse absolue http(s). Sans champ link, la
carte n'a pas de lien : Convoy n'invente jamais une adresse sur le domaine du tenant.
La classe (public, internal, personal, confidential, secret) dit où la valeur peut
aller : voir Données.
select
| clé | effet |
|---|---|
max_items | 1 : un seul ; le choix remplace |
guards | la liste ordonnée des gardes : voir Les gardes |
unique | une clé verrouillée sur toute la station tant qu'une action la porte |
valid_for | au-delà (30d), l'action est revérifiée avant le submit |
quantity | {min, max, step} : les quantités admises pour une ligne ; max et step facultatifs |
quantity se règle le plus souvent par kind : au moins cinq nems, un
plat à l'unité.
kinds:
nems:
when: "$fields.category == 'nems'"
select: {quantity: {min: 5}}
Core tient la règle à l'ajout, au changement de quantité et au submit. Une quantité hors de la
règle est refusée, jamais corrigée en silence : la phrase dit le minimum ou le pas, et le tenant
peut la réécrire (Les phrases de la station).
step dit le pas (step: 6 pour des cartons de six) ; max, le plafond. La quantité ne se lit
pas dans une condition ($item.quantity) : une garde ne voit qu'un ajout, et un second ajout ou un
changement de quantité atteindrait la ligne sans elle.
submit — des appels nommés
submit:
- name: open
tool: openFormationDossier
args: {body.capital: "$item.fields.capital", body.idempotency_key: "$action.key"}
output: {company_id: {from: "$output.company_id", type: id}}
- Chaque appel a un nom et déclare ses sorties. Les autres le citent par la tâche et le
nom :
$submitted[societe.open].company_id. - Plusieurs appels s'exécutent dans l'ordre. Un appel qui lit la sortie d'un appel échoué est mis en attente.
mode:request(défaut),handover(avecurl),self.$action.key: une clé d'idempotence par action, stable d'une tentative à l'autre. Plusieurs lignes déposées par un même appel sont une seule action et partagent une clé.- Seules les sorties déclarées se lisent :
output: {company_id: {from: "$output.company_id", type: id}}garde ce chemin de la réponse du tenant, avec son type, et rien d'autre. line:dit, sur l'appel, la forme d'une ligne envoyée au tenant :line: {product_id: "$line.ref", quantity: "$line.quantity"}, ouline: "$line.ref"pour un tenant qui attend une liste d'identifiants. Voir La forme d'une ligne.- Un appel qui dépose une action de cette skill vit ici, ou sur son kind : un remboursement, une livraison, un retrait. Une commande qui envoie en une requête les lignes de plusieurs tâches (les plats, la boisson, les couverts) se déclare une seule fois, sur le playbook (Le submit). Une skill utilisée hors de tout playbook garde tous ses appels.
render et output_filters
render.component : carousel, cards, checklist, report. Le composant demande des rôles
aux champs, jamais des positions.
output_filters agit avant que le modèle voie le résultat :
output_filters:
drop: [iban, card_number] # retiré
reduce: {address: district} # « Plateau, Dakar » au lieu de l'adresse complète
policies: [conseil-juridique] # une politique smart sur la réponse (effect: rewrite)
Politiques possibles
Une skill porte ses règles là où elles s'appliquent (Où une règle s'applique) :
| phase | où on l'écrit | exemples |
|---|---|---|
| lire | invoke.guards | une permission sur une lecture, une politique smart sur ce qui est demandé |
| sélectionner | select.guards, par kind (kinds.<k>.select) ou par profil (per_profile) | form, check, require, permission, approval, {policy: nom}, un budget |
| soumettre | les appels de submit ; les règles du submit vivent dans le submit_guard du playbook, ou de la station hors playbook | l'ordre, les dépendances ; submitted_by dans le submit_guard (Le submit) |
| sortie | output_filters | mask, rewrite sur ce qui sort d'une lecture |
Dans select, unique (une même action une seule fois) et valid_for (une action qui périme) ;
une skill n'est utilisable que par les profils qui ont reçu les opérations qu'elle appelle (Permissions). Voir Données.
per_profile
Les mêmes points, spécialisés pour une catégorie d'acteurs, comme un kind les spécialise pour une
catégorie d'actions (kinds). Voir
Profils pour les acteurs, Politiques pour les points.