Aller au contenu principal
Version: Cible · roadmap

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 :

sourcesens
llmle modèle, depuis la conversation
personla personne, dans un formulaire ou d'un clic
readune lecture de cette room
resultle résultat d'un submit précédent
identityl'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.
  • required retient 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 par present.fields est 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.

typece que fait Core
textl'affiche
email, phonel'écrit d'une seule façon avant qu'il identifie une room
number, integervérifie qu'une réponse en est un (integer : un nombre entier)
booleanle vérifie ; dans le formulaire du submit, un boolean obligatoire est un consentement, qui n'accepte que oui
pricedans la devise de la station ; le champ billed est le prix de la ligne
quantityun nombre entier
idun identifiant : du texte ou un nombre entier
urlune adresse ; le champ link est la page de l'item chez le tenant
imagel'adresse d'une image ; la première est l'image de la carte
datetimeune 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é ; sans title, la ligne montre sa référence. L'image de la carte est le premier champ image.
  • Chaque champ est sur l'item sous son nom ($item.fields.<nom>).
  • Trois noms sont réservés : name est le title, price le billed, image_url la première image. code, slug et id sont refusés : Core y écrit la référence de l'item.
  • Un template commence par {tenant_url} ou http(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_items1 : un seul ; le choix remplace
guardsla liste ordonnée des gardes : voir Les gardes
uniqueune clé verrouillée sur toute la station tant qu'une action la porte
valid_forau-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 (avec url), 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"}, ou line: "$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) :

phaseoù on l'écritexemples
lireinvoke.guardsune permission sur une lecture, une politique smart sur ce qui est demandé
sélectionnerselect.guards, par kind (kinds.<k>.select) ou par profil (per_profile)form, check, require, permission, approval, {policy: nom}, un budget
soumettreles appels de submit ; les règles du submit vivent dans le submit_guard du playbook, ou de la station hors playbookl'ordre, les dépendances ; submitted_by dans le submit_guard (Le submit)
sortieoutput_filtersmask, 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.