Aller au contenu

8. L'intelligence artificielle

Oui, votre module peut appeler un modèle de langage. Non, il ne peut pas appeler un fournisseur directement.

from kewos.ai import AiMessage, AiRequest, complete

La distinction n’est pas une formalité : tout ce qui rend un appel d’IA acceptable en entreprise vit dans la façade, et vous en héritez uniquement en passant par elle.

Ce que la façade fait pour vous

L’activationSi le client n’a pas activé l’IA, l’appel est refusé. Pas de contournement possible.
La liste blancheSeuls les fournisseurs que le client a autorisés sont joignables.
La résidence des donnéesUn fournisseur hors Union européenne exige un consentement explicite du client. Sans lui, l’appel est refusé.
Le quotaUn budget d’appels par fenêtre de temps, par client.
L’auditChaque appel est tracé.
Le cacheDeux fois la même question ne se paie pas deux fois.
Le choix du modèleLe client choisit son fournisseur ; votre code n’en connaît aucun.

Un module qui appellerait une API de fournisseur en direct perdrait les sept. C’est pourquoi c’est interdit, pas parce qu’on aime les règles.

Un appel

from kewos.ai import AiMessage, AiRequest, complete
from kewos.ai.exceptions import AiDisabledError, AiQuotaExceededError, AiResidencyError
from kewos.secrets import build_tenant_secrets_backend
def summarise_blurb(engine, blurb: str) -> str | None:
"""Turn a publisher blurb into one sentence for the catalogue."""
request = AiRequest(
messages=(AiMessage(role="user", content=f"Summarise in one sentence:\n\n{blurb}"),),
system="Answer in a single sentence. No preamble, no commentary.",
max_tokens=200,
)
try:
response = complete(
engine,
request,
secrets=build_tenant_secrets_backend(engine),
)
except AiDisabledError:
return None # AI is off for this tenant: not a failure
except (AiQuotaExceededError, AiResidencyError):
return None # a legitimate refusal: degrade, do not break
return response.text

Où faire cet appel

Pas dans une requête HTTP. Un appel de modèle prend des secondes et part sur le réseau. Mettez-le dans un traitement de fond.

from kewos.jobs import job
@job(queue="default")
def summarise_title(title_id: int) -> None:
"""Fill in the summary of one catalogue entry, out of band."""
...

Le déclencheur naturel est un événement du module, ou une tâche planifiée qui traite un lot.

Un modèle qui voit

Pour une image, un bloc dédié remplace le texte :

from kewos.ai.provider import AiImageBlock, AiTextBlock
message = AiMessage(
role="user",
content=(
AiImageBlock(media_type="image/png", data=base64_de_l_image),
AiTextBlock(text="Which book cover is this? Give the title and author."),
),
)

Pour transcrire un document plutôt que le commenter, ne le faites pas vous-même : passez par kewos/ocr, qui gère le choix du moteur et le repli.

Faire écrire des données par un modèle

Un modèle de langage ne doit jamais écrire directement dans votre base. Le schéma est : proposer, puis confirmer.

  1. Le modèle produit une proposition structurée, contrainte par un schéma.
  2. Vous la validez comme n’importe quelle entrée utilisateur.
  3. Vous la présentez à l’utilisateur.
  4. L’utilisateur confirme, et c’est votre code qui écrit.
request = AiRequest(
messages=(...),
response_format="json", # constrain the reply to a valid JSON object
max_tokens=800,
)

Une proposition non confirmée n’a produit aucun effet de bord. C’est ce qui rend la fonctionnalité rattrapable le jour où le modèle se trompe, et il se trompera.

Un cas concret : proposer un genre

La bibliothécaire colle le texte de l’éditeur. Le module propose un genre et une phrase de catalogue ; elle accepte ou refuse. Rien n’est écrit avant.

_SYSTEM = (
"You classify library books. Answer with a JSON object holding exactly two "
"keys: 'genre', which MUST be one of the allowed values given to you, and "
"'summary', a single sentence of at most 25 words. No other key, no prose."
)
def suggest_for_title(engine, *, title, author, blurb, allowed_genres, secrets):
"""Propose a genre and a one-line summary, or None."""
request = AiRequest(
messages=(AiMessage(role="user", content=_prompt(title, author, blurb, allowed_genres)),),
system=_SYSTEM,
response_format="json", # constrain the reply to a JSON object
max_tokens=300,
)
try:
response = complete(engine, request, secrets=secrets)
except (AiDisabledError, AiQuotaExceededError, AiResidencyError):
return None # legitimate refusals: degrade, never break
return parse_suggestion(response.text, allowed=allowed_genres)

La validation est le vrai travail

Contraindre la réponse ne suffit pas : il faut la vérifier contre la liste du modèle.

def parse_suggestion(text: str, *, allowed: Sequence[str]) -> Suggestion | None:
"""Read the model's answer, or return None when it is unusable."""
try:
payload = json.loads(text)
except (ValueError, TypeError):
return None
if not isinstance(payload, dict):
return None
genre, summary = payload.get("genre"), payload.get("summary")
if not isinstance(genre, str) or not isinstance(summary, str):
return None
# The list comes from the model's own selection field, so a value outside it
# could never be stored anyway. Refused HERE, where it can still be explained.
if genre not in allowed:
return None
if len(summary.split()) > MAX_SUMMARY_WORDS:
return None
return Suggestion(genre=genre, summary=summary.strip())

Ce que ça donne, cas par cas

Exécuté avec un modèle de substitution qui répond ce qu’on lui dit :

une réponse valide -> Suggestion(genre='fiction', summary='A desert planet, a rare
spice, and the fall and rise of a noble house.')
un genre inventé -> None
une réponse bavarde -> None
une moitié de réponse -> None
IA éteinte chez le client -> None

Quatre None sur cinq, et c’est le but. Un modèle ne peut pas inventer une catégorie : un genre hors liste est écarté, pas corrigé, parce qu’une valeur plausible et fausse est pire qu’une valeur absente. Et une demi-réponse est refusée entière, parce qu’une demi-suggestion montrée à quelqu’un est une suggestion qu’il accepte sans voir la moitié manquante.

Ce qui n’existe pas encore

Déclarer un prompt dans une vue ou dans une action YAML. Le format déclaratif ne connaît aujourd’hui que deux opérations, affecter une valeur et émettre un événement. Un module en YAML ne peut donc pas appeler un modèle sans passer par du code.

C’est une limite identifiée, et une décision d’architecture est en cours de rédaction pour la lever. En attendant, un module YAML qui a besoin d’IA s’accompagne d’un petit module en code qui expose l’appel.

Et ensuite

Lire un document.