mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 17:17:08 +00:00
292 lines
15 KiB
PHP
292 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* ASPIC — consignes_fonctions.php
|
|
*
|
|
* Bibliothèque des fonctions du catalogue ASP_Fonctions (chantier des
|
|
* consignes, 6 septembre 2026 — voir Réflexions stratégiques). Chaque
|
|
* fonction ci-dessous porte le nom exact enregistré dans
|
|
* ASP_Fonctions.FON_NomPHP, et est appelée dynamiquement par le moteur
|
|
* générique (voir moteur_consignes.php) — plus aucun appel en dur dans
|
|
* contacts.php depuis la version du 6 septembre 2026 (soir).
|
|
*
|
|
* Convention pour écrire une nouvelle fonction ici :
|
|
* - Traitement à UN champ : signature (string $valeur): string — reçoit la
|
|
* valeur actuelle du champ, retourne la valeur transformée.
|
|
* - Test à UN champ : signature (string $valeur): bool — retourne vrai si la
|
|
* valeur respecte la consigne, faux sinon (le message affiché à l'échec
|
|
* vient de CNS_MessageErreur en base, jamais de cette fonction).
|
|
* - Traitement/Test à PLUSIEURS champs (extension du 7 septembre 2026, voir
|
|
* moteur_consignes.php) : quand CNS_Champ contient plusieurs noms de champs
|
|
* séparés par une virgule, la fonction reçoit un tableau associatif
|
|
* [nom_du_champ => valeur] au lieu d'une simple chaîne — un Traitement
|
|
* retourne alors ce même type de tableau (valeurs mises à jour, mêmes clés),
|
|
* un Test retourne toujours un simple bool. Voir
|
|
* consigne_completer_code_postal() ci-dessous pour un exemple.
|
|
* - Jumeau "_seraitDeclenchee" (7 septembre 2026, soir) : toute fonction de
|
|
* Traitement qui interroge un service externe (IA) et porte
|
|
* FON_VisuelAttenteIA = 'O' DOIT avoir, en plus, une fonction nommée
|
|
* "<nom_de_la_fonction>_seraitDeclenchee", signature (array $valeurs): bool
|
|
* — répond "est-ce que j'agirais sur ces valeurs", sans jamais rien faire
|
|
* d'autre (aucun appel réseau). La vraie fonction appelle ce jumeau EN
|
|
* PREMIER (voir consigne_completer_code_postal() ci-dessous) : la condition
|
|
* de déclenchement n'existe ainsi qu'à un seul endroit, jamais recopiée ni
|
|
* devinée ailleurs (pas même dans ce fichier) — c'est ce jumeau
|
|
* qu'interroge consignes_apercu.php pour piloter le badge IA générique côté
|
|
* écran, voir ce fichier et moteur_consignes.php
|
|
* (auMoinsUneConsigneIaSeraitDeclenchee()).
|
|
* Fichier volontairement sans dépendance à la session ni à la base de
|
|
* données : chaque fonction ne fait que transformer ou tester une ou
|
|
* plusieurs valeurs qu'on lui passe. Exception depuis le 7 septembre 2026
|
|
* (soir) : une fonction peut interroger un service externe (API d'IA, voir
|
|
* consigne_completer_code_postal()) quand la consigne elle-même l'exige
|
|
* explicitement — jamais la session ni la base ASPIC, seulement un appel
|
|
* réseau sortant, et toujours avec un repli silencieux (valeurs inchangées)
|
|
* en cas d'échec : un Traitement ne doit jamais faire échouer un
|
|
* enregistrement à cause d'un service tiers indisponible.
|
|
*/
|
|
|
|
// ASP_Fonctions.FON_Nom = 'MAJUSCULE' (Nature=Traitement)
|
|
// Met en majuscules la valeur d'un champ, en respectant les caractères
|
|
// accentués français (mb_strtoupper avec encodage explicite : un simple
|
|
// strtoupper() laisserait "é"/"è"/... inchangés selon la locale du serveur).
|
|
function consigne_majuscule(string $valeur): string
|
|
{
|
|
return mb_strtoupper($valeur, 'UTF-8');
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'NON_VIDE' (Nature=Test)
|
|
// Vrai si la valeur n'est pas vide une fois débarrassée des espaces
|
|
// superflus en début/fin (une chaîne de simples espaces est considérée vide).
|
|
function consigne_non_vide(string $valeur): bool
|
|
{
|
|
return trim($valeur) !== '';
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'PREMIERE_LETTRE_MAJUSCULE' (Nature=Traitement)
|
|
// "georges" -> "Georges", "JOSEPH" -> "Joseph", "jean-marc" -> "Jean-Marc".
|
|
// Tout le reste en minuscules, une majuscule après le début de la chaîne et
|
|
// après chaque tiret ou espace (pour les prénoms composés). Volontairement
|
|
// pas de ucwords() : cette fonction native de PHP n'est pas fiable en UTF-8
|
|
// (elle raisonne en octets, pas en caractères — un prénom commençant par une
|
|
// lettre accentuée type "Éric" serait corrompu).
|
|
function consigne_premiere_lettre_majuscule(string $valeur): string
|
|
{
|
|
$valeur = mb_strtolower($valeur, 'UTF-8');
|
|
return (string)preg_replace_callback(
|
|
'/(^|[\s\-])(\p{L})/u',
|
|
function (array $m): string {
|
|
return $m[1] . mb_strtoupper($m[2], 'UTF-8');
|
|
},
|
|
$valeur
|
|
);
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'MAIL_VALIDE' (Nature=Test), 7 septembre 2026.
|
|
// Vrai si la valeur ressemble à une adresse mail syntaxiquement correcte,
|
|
// faux sinon — une simple cohérence de saisie ("@" présent, domaine
|
|
// plausible), jamais une vérification que la boîte existe réellement.
|
|
// Champ facultatif : une valeur vide est considérée conforme ici (le
|
|
// caractère obligatoire ou non du champ reste la responsabilité d'une
|
|
// consigne NON_VIDE séparée, combinable avec celle-ci sur le même champ).
|
|
//
|
|
// Volontairement PAS de regex écrite à la main du style "@ suivi de deux
|
|
// groupes de lettres séparés par un point" : une adresse valide peut avoir
|
|
// un domaine à plus de deux niveaux (ex. toto@tata.ndd.com), un chiffre, un
|
|
// tiret... filter_var(FILTER_VALIDATE_EMAIL) est le validateur natif de PHP,
|
|
// déjà utilisé ailleurs dans ASPIC pour la même vérification (voir
|
|
// prospects_core.php, contrôle sur PSP_Mail) — le reprendre ici garde une
|
|
// seule et même définition de "adresse mail valide" dans toute l'application
|
|
// plutôt que deux logiques différentes qui pourraient diverger.
|
|
function consigne_mail_valide(string $valeur): bool
|
|
{
|
|
$valeur = trim($valeur);
|
|
if ($valeur === '') return true;
|
|
return filter_var($valeur, FILTER_VALIDATE_EMAIL) !== false;
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'SANS_CHIFFRE' (Nature=Test), 7 septembre 2026.
|
|
// Vrai si la valeur ne contient aucun chiffre, faux sinon — champ facultatif
|
|
// (une valeur vide est conforme, même principe que consigne_mail_valide() :
|
|
// le caractère obligatoire est la responsabilité d'une consigne NON_VIDE
|
|
// séparée, combinable sur le même champ). Générique et réutilisable tel
|
|
// quel sur n'importe quel champ texte : Jean-Marc l'a demandée pour Nom,
|
|
// Prénom et Ville, ce qui ne demande ici AUCUNE fonction par champ — juste
|
|
// trois lignes dans ASP_Consignes (CNT_Nom, CNT_Prenom, CNT_AdrVille)
|
|
// pointant toutes vers cette même fonction, chacune avec son propre
|
|
// CNS_MessageErreur si on veut un message différent par champ.
|
|
function consigne_sans_chiffre(string $valeur): bool
|
|
{
|
|
return trim($valeur) === '' || !preg_match('/\d/', $valeur);
|
|
}
|
|
|
|
// ASP_Fonctions.FON_Nom = 'CP_DEPUIS_VILLE' (Nature=Traitement, CNS_Champ =
|
|
// "CNT_AdrVille,CNT_AdrCP"), 7 septembre 2026 (soir).
|
|
// Première consigne "qui va chercher sa réponse avec l'IA" (demande de
|
|
// Jean-Marc) : complète le code postal à partir de la ville quand celui-ci
|
|
// n'a pas été saisi. Un champ seul ne suffisait pas ici (il faut la ville en
|
|
// entrée pour produire le code postal en sortie) — voir l'extension du
|
|
// moteur à plusieurs champs dans moteur_consignes.php.
|
|
//
|
|
// Ne fait RIEN (retourne les valeurs inchangées) si le code postal est déjà
|
|
// renseigné — jamais d'écrasement d'une saisie existante, même douteuse —
|
|
// ou si la ville est vide (rien à déduire). Sinon interroge le même modèle
|
|
// que contact_ia_interprete.php (OPENAI_API_KEY, voir ce fichier pour le
|
|
// détail de l'intégration) avec une sortie structurée stricte : le modèle
|
|
// renvoie soit un code postal à 5 chiffres qu'il connaît avec une confiance
|
|
// raisonnable, soit null s'il n'est pas sûr — jamais un code inventé, même
|
|
// principe de prudence que consigne_mail_valide() ci-dessus (mieux vaut
|
|
// laisser le champ vide qu'afficher une fausse information). Une ville qui
|
|
// couvre plusieurs codes postaux sans qu'aucun ne soit clairement principal
|
|
// (ex. arrondissements de Paris/Lyon/Marseille) doit renvoyer null plutôt que
|
|
// de deviner — c'est écrit explicitement dans la consigne envoyée au modèle.
|
|
//
|
|
// Toute défaillance (clé absente, réseau, quota, réponse inattendue) se
|
|
// traduit par un retour silencieux des valeurs inchangées : un Traitement ne
|
|
// doit jamais bloquer l'enregistrement d'un contact à cause d'un service
|
|
// externe (voir commentaire d'en-tête de moteur_consignes.php) — contrairement
|
|
// à un Test, dont l'échec est lui affiché à l'utilisateur.
|
|
// Jumeau "_seraitDeclenchee" (voir en-tête de fichier) : réplique à elle
|
|
// seule la condition de déclenchement de consigne_completer_code_postal()
|
|
// ci-dessous, sans jamais faire le moindre appel réseau. C'est la SEULE
|
|
// fonction que consignes_apercu.php interroge pour savoir si le badge IA doit
|
|
// apparaître — la vraie fonction l'appelle elle-même en premier, pour que
|
|
// cette condition n'existe jamais qu'à cet unique endroit.
|
|
function consigne_completer_code_postal_seraitDeclenchee(array $valeurs): bool
|
|
{
|
|
$ville = trim((string)($valeurs['CNT_AdrVille'] ?? ''));
|
|
$codePostal = trim((string)($valeurs['CNT_AdrCP'] ?? ''));
|
|
|
|
return $codePostal === '' && $ville !== '';
|
|
}
|
|
|
|
function consigne_completer_code_postal(array $valeurs): array
|
|
{
|
|
if (!consigne_completer_code_postal_seraitDeclenchee($valeurs)) {
|
|
return $valeurs;
|
|
}
|
|
|
|
$ville = trim((string)($valeurs['CNT_AdrVille'] ?? ''));
|
|
|
|
$apiKey = trim((string) getenv('OPENAI_API_KEY'));
|
|
if (!$apiKey) {
|
|
return $valeurs;
|
|
}
|
|
|
|
$schema = [
|
|
'type' => 'object',
|
|
'properties' => [
|
|
'code_postal' => [
|
|
'type' => ['string', 'null'],
|
|
'description' => "Code postal français à 5 chiffres correspondant à la ville donnée, uniquement si connu avec une confiance raisonnable. null si la ville est ambiguë, inconnue, introuvable en France, ou couvre plusieurs codes postaux sans qu'un seul soit clairement le plus probable.",
|
|
],
|
|
],
|
|
'required' => ['code_postal'],
|
|
'additionalProperties' => false,
|
|
];
|
|
|
|
$corpsRequete = [
|
|
'model' => 'gpt-5.6-luna', // même modèle que contact_ia_interprete.php, voir ce fichier.
|
|
'messages' => [
|
|
[
|
|
'role' => 'system',
|
|
'content' => "Tu donnes le code postal français d'une ville, pour un CRM. "
|
|
. "N'invente jamais un code postal : renvoie null si tu n'es pas "
|
|
. "raisonnablement sûr, si la ville est ambiguë ou inconnue, ou si "
|
|
. "elle est à cheval sur plusieurs codes postaux sans qu'un seul soit "
|
|
. "clairement le principal (ex. grandes villes découpées en "
|
|
. "arrondissements).",
|
|
],
|
|
['role' => 'user', 'content' => $ville],
|
|
],
|
|
'response_format' => [
|
|
'type' => 'json_schema',
|
|
'json_schema' => ['name' => 'code_postal_ville', 'strict' => true, 'schema' => $schema],
|
|
],
|
|
];
|
|
|
|
$ch = curl_init('https://api.openai.com/v1/chat/completions');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode($corpsRequete),
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $apiKey,
|
|
],
|
|
// Appelée en synchrone pendant l'enregistrement d'un contact (choix
|
|
// de Jean-Marc, 7 septembre 2026 : "oui, tel quel" malgré le coût/la
|
|
// latence) : délai volontairement court, on préfère abandonner
|
|
// (valeurs inchangées) plutôt que de faire attendre longtemps sur un
|
|
// enrichissement facultatif.
|
|
CURLOPT_TIMEOUT => 8,
|
|
CURLOPT_CONNECTTIMEOUT => 5,
|
|
]);
|
|
$reponseBrute = curl_exec($ch);
|
|
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($reponseBrute === false || $codeHttp !== 200) {
|
|
return $valeurs;
|
|
}
|
|
|
|
$reponseDecodee = json_decode($reponseBrute, true);
|
|
$contenuJson = $reponseDecodee['choices'][0]['message']['content'] ?? null;
|
|
$extraction = $contenuJson ? json_decode($contenuJson, true) : null;
|
|
$codePostalDeduit = $extraction['code_postal'] ?? null;
|
|
|
|
// Garde-fou défensif (même principe qu'ailleurs dans ASPIC : jamais
|
|
// confiance aveugle en un service externe) : un code postal français est
|
|
// exactement 5 chiffres, sinon la réponse est ignorée.
|
|
if (is_string($codePostalDeduit) && preg_match('/^\d{5}$/', $codePostalDeduit)) {
|
|
$valeurs['CNT_AdrCP'] = $codePostalDeduit;
|
|
// Journalisation (7 septembre 2026, soir, demande de Jean-Marc après
|
|
// son premier essai réel — "indispensable" pour pouvoir relier une
|
|
// ligne de coût IA à sa cause) : clé réservée "_realisation", voir
|
|
// moteur_consignes.php pour le mécanisme générique. Pas de champ
|
|
// 'agent' ici (contrairement au tout premier jet, qui inventait "Mat"
|
|
// en dur) : Jean-Marc a fait remarquer, à raison, qu'ASP_Consignes
|
|
// porte déjà CNS_Agent — c'est le moteur (executerTraitements(), voir
|
|
// moteur_consignes.php) qui l'y lit et l'attribue à la réalisation,
|
|
// jamais cette fonction. ACT_Code 'IA_CP' à créer à la main dans
|
|
// ASP_Actions (table gérée à la main, voir realisations.php) avant le
|
|
// premier passage réel en production.
|
|
$valeurs['_realisation'] = [
|
|
'action' => 'IA_CP',
|
|
'commentaire' => "Code postal $codePostalDeduit déduit de la ville « $ville » par l'IA.",
|
|
];
|
|
} else {
|
|
// Appel IA fait, coût réellement engagé, mais sans réponse
|
|
// exploitable — ex. Paris/Lyon/Marseille, découpées en
|
|
// arrondissements sans code postal unique, pour lesquelles le modèle
|
|
// renvoie sciemment null plutôt que de deviner (voir le prompt
|
|
// système ci-dessus). Repéré par Jean-Marc (7 septembre 2026, nuit,
|
|
// test sur "Paris") : "il y a un appel fait à l'IA et pas de trace
|
|
// dans les réalisations, ce qui n'est pas normal" — un appel qui ne
|
|
// trouve rien coûte quand même des tokens, il doit donc apparaître
|
|
// au même titre qu'un appel réussi. Même ACT_Code 'IA_CP', un
|
|
// commentaire différent suffit à distinguer les deux cas dans
|
|
// l'historique, sans demander à Jean-Marc de créer un deuxième code
|
|
// dans ASP_Actions.
|
|
$valeurs['_realisation'] = [
|
|
'action' => 'IA_CP',
|
|
'commentaire' => "Aucun code postal déduit pour la ville « $ville » (ambiguë ou inconnue pour l'IA).",
|
|
];
|
|
// '_avertissement' (nouvelle clé réservée, voir moteur_consignes.php) :
|
|
// contrairement à '_realisation' (historique consulté a posteriori),
|
|
// ce message est affiché IMMÉDIATEMENT à l'écran — sans lui,
|
|
// l'utilisateur ne voit rien du tout et ne sait pas que l'IA a bien
|
|
// réfléchi puis sciemment renoncé, plutôt qu'un simple silence.
|
|
// Message volontairement impératif (7 septembre 2026, demande de
|
|
// Jean-Marc) : le toast qui l'affiche reste à l'écran tant que
|
|
// l'utilisateur n'a pas cliqué pour le refermer (voir
|
|
// liste_contacts.html/afficherToast()), l'instruction doit donc être
|
|
// sans ambiguïté sur ce qu'il reste à faire.
|
|
$valeurs['_avertissement'] =
|
|
"Code postal non déterminé pour « $ville » (ville ambiguë ou "
|
|
. "avec plusieurs codes postaux possibles). Saisissez le code "
|
|
. "postal vous-même.";
|
|
}
|
|
|
|
return $valeurs;
|
|
}
|