Aller au contenu

Prompts

Déclarer un prompt du module IA, décrire la forme de sa réponse en JSON Schema, construire la demande avec texte et images, et ajuster les prompts des modules dans ai.js.

Un prompt est déclaré une fois, par un module ou par le ai.js du projet, puis appelé par son nom : depuis un bouton de l'admin, une tâche, ou le code du projet. Cette page décrit sa déclaration, la forme de sa réponse, et ce qu'un projet peut changer aux prompts des modules.

Déclarer un prompt

// ai.js
import { definePrompt } from '@fraza/module-ai/server';

definePrompt({
  name: 'site.summary',
  label: 'Résumer une demande',
  system: 'Tu tries les demandes reçues par une clinique vétérinaire.',
  instructions: 'Résume la demande en une phrase, puis classe-la.',
  output: {
    type: 'object',
    properties: {
      summary: { type: 'string', maxLength: 300, description: 'Une phrase.' },
      category: { type: 'string', enum: ['rendez-vous', 'urgence', 'question', 'autre'] },
      urgent: { type: 'boolean' },
    },
    required: ['summary', 'category', 'urgent'],
  },
  build: ({ context, instructions }) => `${instructions}\n\nDemande :\n${context.message}`,
});
ChampRequisRôle
nameouiNom unique, en mots minuscules séparés par des points : posts.write, site.summary. Chaque mot commence par une lettre et peut contenir chiffres et tirets. Un module préfixe les siens de son nom court.
outputouiSchéma de la réponse, ou une fonction (scope) => schéma, asynchrone permise, qui peut rendre null quand le prompt ne s'applique pas. Voir Décrire la réponse.
buildoui({ input, context, scope, instructions }) => demande, asynchrone permise. Rend la demande envoyée au modèle. Voir Construire la demande.
systemnonLe rôle du modèle : qui il est, pour qui il écrit.
instructionsnonLa demande elle-même, que build() reçoit et place où il veut.
targetnonL'écran qui montre le bouton, posts.post ou medias.media. Sans target, le prompt ne s'appelle que depuis le code. Voir Boutons.
labelavec targetLe texte du bouton. Requis dès qu'il y a un target. La page IA l'affiche pour tous les prompts.
descriptionnonLe texte sous le bouton, dans sa fenêtre.
modenonfill, le défaut, remplit les champs de l'écran. choose propose des choix. Voir Boutons.
permissionnonUne permission demandée en plus de ai.use pour lancer le bouton, posts.manage par exemple.
inputnonSchéma du court formulaire que le bouton ouvre avant l'appel. Voir Boutons.
maxTokensnonPlafond de jetons de la réponse, de 1 à 128 000. 16000 par défaut.

definePrompt vérifie la déclaration et le schéma tout de suite, et lève une TypeError qui liste chaque problème : une erreur de schéma se voit au démarrage, pas au premier clic. Un output en fonction n'est vérifié qu'à l'appel, puisqu'il dépend du scope.

Un nom appartient à une source. Un prompt déclaré par le projet a la source projet, celui d'un module le nom de son paquet. Redéclarer le même nom depuis la même source le remplace, ce que fait un rechargement à chaud. Le déclarer depuis une autre source lève : un projet ne prend pas le nom d'un prompt de module, il l'ajuste avec customizePrompt. Le nom ai.test est pris par le bouton « Tester » de la page IA.

maxTokens borne la réponse, pas la demande. Une réponse coupée à ce plafond n'est pas du JSON complet : l'appel échoue avec truncated au lieu de rendre une réponse partielle. Le défaut est large parce qu'un modèle qui raisonne avant de répondre dépense une part de ce plafond avant d'écrire un mot.

Décrire la réponse

Le schéma est un sous-ensemble de JSON Schema, celui que tous les fournisseurs comprennent.

TypeMots admis, en plus de type, title et description
objectproperties, required, additionalProperties (absent ou false).
stringenum (une liste non vide de chaînes), maxLength.
arrayitems, minItems, maxItems.
number, integer, boolean, nullAucun.

Trois règles en plus :

  • La racine est un objet. Une réponse est toujours un objet à propriétés nommées, que l'écran ou le code lit par leur nom.
  • Chaque propriété est requise. required liste toutes les clés de properties, ni plus ni moins. Une valeur facultative est un type accompagné de null : type: ['string', 'null']. Les modes stricts des fournisseurs l'exigent, et le code qui lit la réponse trouve toujours chaque clé.
  • Huit niveaux d'imbrication au plus.

Tout autre mot, $ref, anyOf, pattern, minimum, format, est refusé à la déclaration. title nomme un champ quand le schéma sert de formulaire, ou un groupe de choix en mode choose.

Le module ne fait pas confiance au fournisseur. Tous ne tiennent pas les mêmes contraintes : certains refusent maxLength, d'autres l'ignorent. Le module retire du schéma envoyé les contraintes qu'un fournisseur ne prend pas, et les écrit dans la description pour que le modèle les lise quand même. Puis il vérifie chaque réponse contre le schéma complet, contraintes comprises, quel que soit le fournisseur :

  1. La réponse est lue comme du JSON. Un bloc de code autour, que certains services ajoutent, est retiré.
  2. Elle est confrontée au schéma : types, valeurs d'enum, longueurs, nombre d'éléments, clés manquantes ou en trop.
  3. Hors schéma, la réponse et la liste des écarts sont renvoyées au modèle, qui répond une seconde fois.
  4. Un second échec rend l'erreur invalid_answer, que l'appelant peut retenter plus tard.

Les jetons des deux appels sont additionnés, et l'historique note 2 tentatives.

Un schéma qui dépend de l'écran

output peut être une fonction du scope que l'écran passe à son bouton. Posts en tire un schéma par type de contenu, avec un champ de réponse par champ du type :

definePrompt({
  name: 'site.describe',
  // ...
  output: async ({ postType } = {}) => {
    const fields = await fieldsOf(postType);
    return { type: 'object', properties: fields, required: Object.keys(fields) };
  },
  build: ({ input, instructions }) => `${instructions}\n\n${input.subject}`,
});

null : le prompt ne s'applique pas. La fonction rend null quand le prompt n'a pas de sens pour ce scope : l'écran ne montre pas son bouton, et runPrompt rend not_applicable sans appeler le fournisseur. Médias s'en sert pour ne montrer son bouton que sur une image qu'un fournisseur sait lire. Seule la fonction peut rendre null : un output: null écrit tel quel est refusé à la déclaration.

Une fonction qui lève, ou rend un schéma invalide, est une erreur, pas un refus : le bouton disparaît aussi, l'erreur est écrite dans le journal du serveur, et un appel depuis le code rend prompt_error.

Construire la demande

build() reçoit quatre valeurs :

ValeurContenu
inputLes réponses du formulaire du bouton, déjà vérifiées contre input. null sans formulaire.
contextCe que l'appelant passe : depuis un bouton, ce que l'écran lui donne au clic, depuis le code, le context de runPrompt. null sinon.
scopeCe qui a choisi le schéma de sortie. {} par défaut.
instructionsLes instructions du prompt, ajustées par le projet.

Elle rend une chaîne, ou une liste de parties pour joindre des images :

build: async ({ context, instructions }) => {
  const image = await readImage(context.mediaId);
  return [
    { type: 'text', text: instructions },
    { type: 'image', mimeType: 'image/jpeg', data: image.toString('base64') },
  ];
},
PartieForme
Texte{ type: 'text', text }
Image{ type: 'image', mimeType, data }, data en base64, 20 Mo de base64 au plus, soit 15 Mo de fichier environ. mimeType : image/png, image/jpeg, image/webp ou image/gif.

Une partie d'une autre forme, une image d'un autre type ou trop lourde, ou une erreur levée dans build(), rend prompt_error. Le détail est écrit dans le journal du serveur, pas dans la réponse : il peut citer le contenu.

build() place les instructions lui-même. Le module ne les ajoute pas à la demande : c'est build() qui décide où elles vont, avant ou après le contenu. Un build() qui ne les utilise pas rend sans effet l'ajustement de instructions par le projet.

context et scope d'un bouton viennent du navigateur. Ils voyagent dans la requête : build() les traite comme une saisie. Relire l'objet par son identifiant, avec ses propres règles d'accès, plutôt que de croire le texte qu'un écran prétend envoyer.

Ajuster un prompt dans ai.js

Le projet change les textes d'un prompt de module sans le réécrire, dans son ai.js. Voir Installation pour son chargement.

// ai.js
import { configureAi, customizePrompt } from '@fraza/module-ai/server';

// Ajouté au rôle de chaque prompt, ceux des modules compris.
configureAi({ instructions: 'Le site s\'adresse à des Québécois. Écris en français du Québec.' });

customizePrompt('posts.write', {
  system: 'Tu écris pour une clinique vétérinaire, sur un ton chaleureux.',
  instructions: (text) => `${text}\nTutoie le lecteur.`,
  label: 'Rédiger un article',
});

// Retire un prompt : plus de bouton, et un appel rend unknown_prompt.
customizePrompt('medias.describe', { enabled: false });
Champ de customizePromptTypeEffet
systemchaîne ou fonctionUne chaîne remplace le texte du module. Une fonction reçoit ce texte, '' s'il n'y en a pas, et rend le nouveau.
instructionschaîne ou fonctionMême règle.
labelchaîneLe texte du bouton.
descriptionchaîneLe texte sous le bouton.
enabledbooléenfalse retire le prompt de ce projet.

Une chaîne remplace, une fonction complète : (text) => ${text}\n…`` garde le texte du module, et suit donc ses mises à jour. Une fonction qui ne rend pas une chaîne laisse le texte du module. Deux appels pour le même prompt se fusionnent, champ par champ.

La forme de la réponse ne se change pas. output, input, build et target sont refusés : l'écran qui remplit ses champs à partir de la réponse compte sur sa forme. Pour une autre réponse, le projet déclare son propre prompt.

configureAi({ instructions }) ajoute un texte, séparé par une ligne vide, à la fin du rôle de chaque prompt : la voix du site, sa langue, son public. Un nouvel appel remplace le précédent, et configureAi({}) le retire.

Un ajustement attend son prompt. ai.js peut tourner avant les modules : l'ajustement est gardé, et s'applique dès que le prompt est déclaré. Un ajustement dont le prompt n'arrive jamais, faute de frappe ou module absent, apparaît en avertissement sur la page IA, avec son nom. La liste des prompts de la page porte « Ajusté par ai.js » sur chaque prompt touché.

Les prompts des modules officiels

PromptModuleÉcranModePermission
posts.writePostsposts.post, l'éditeur d'un contenu.fillposts.manage
medias.describeMédiasmedias.media, la fenêtre d'un média image.choosemedia.upload
medias.altMédiasAucun : appelé au téléversement d'une image, quand le module Médias l'active.

Leurs textes exacts sont dans le code de chaque module. Les ajuster passe par customizePrompt, avec ces noms.

Notes

  • Les prompts vivent en mémoire, dans le processus du serveur, sur globalThis : un rechargement à chaud ne perd pas ceux des modules. Chaque instance d'un site à plusieurs instances les déclare au démarrage.
  • listPrompts() de @fraza/module-ai/server rend la liste que montre la page IA. Voir API.

Sur cette page