mirror of
https://github.com/JMP-MS/ASPIC.git
synced 2026-10-27 14:57:09 +00:00
338 lines
15 KiB
PHP
338 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* ASPIC — collaborateur_dialogue.php
|
|
*
|
|
* Endpoint interne (10 septembre 2026, réflexions stratégiques section 20 —
|
|
* "Tester l'identité") — PAS un endpoint destiné aux futurs utilisateurs à ce
|
|
* stade, même esprit que collaborateurs_identite.php/contact_ia_interprete.php :
|
|
* un banc d'essai réservé à Jean-Marc, pour dialoguer en conditions réelles
|
|
* avec un collaborateur et juger si l'identité qu'il vient de lui écrire
|
|
* (AGN_Identite, voir collaborateurs_identite.php) "sonne juste" avant de
|
|
* l'affiner.
|
|
*
|
|
* Reçoit (POST, JSON) :
|
|
* { "AGN_Incr": 3, "messages": [ {"role":"user","content":"..."}, ... ] }
|
|
* — l'historique complet de la discussion en cours, maintenu côté front-end
|
|
* (voir ecran_fond.html) : l'API Anthropic est sans état, chaque appel doit
|
|
* réexpédier tout l'échange précédent. Renvoie { "reponse": "..." }, le
|
|
* prochain message du collaborateur.
|
|
*
|
|
* Ce fichier construit le prompt système à partir d'AGN_Prenom/AGN_Fonction/
|
|
* AGN_Identite/AGN_Relationnel/AGN_Experience/AGN_Competences — et SEULEMENT
|
|
* à partir de ça : jamais ces champs renvoyés eux-mêmes au front-end (voir
|
|
* collaborateurs_identite.php, même principe — l'historique de vie et
|
|
* d'expérience d'un collaborateur reste invisible d'un bout à l'autre de la
|
|
* chaîne, seule sa "voix" en sort, via les réponses du modèle). Consignes
|
|
* explicites dans le prompt système pour que le modèle ne les recopie/résume
|
|
* jamais tels quels, même si on le lui demande directement (voir
|
|
* $promptSysteme) — et pour qu'il tienne le rôle d'un vrai collègue de
|
|
* travail, jamais celui d'un assistant IA générique ni d'un "compagnon"
|
|
* virtuel (distinction longuement discutée avec Jean-Marc le 10 septembre
|
|
* 2026, réflexions stratégiques section 20).
|
|
*
|
|
* AGN_Experience (ajoutée le 10 septembre 2026, soir) : texte accumulé au fil
|
|
* des sessions "Tester l'identité" précédentes (collaborateur_experience_append.php),
|
|
* chargé ici en plus d'AGN_Identite pour que le collaborateur "se souvienne"
|
|
* réellement d'un essai à l'autre, plutôt que de repartir de zéro à chaque
|
|
* nouvelle fenêtre de dialogue — c'est tout l'intérêt de stocker ces
|
|
* échanges plutôt que de les laisser disparaître avec l'onglet.
|
|
*
|
|
* Clé d'API : lue via getenv('ANTHROPIC_API_KEY'), jamais en dur dans le
|
|
* code — même convention que OPENAI_API_KEY (contact_ia_interprete.php) et
|
|
* DB_USER/DB_PASS (db.php). Si absente, l'endpoint répond 500 avec un
|
|
* message explicite. Ajoutée par Jean-Marc le 10 septembre 2026 dans
|
|
* l'environnement des DEUX conteneurs déployés (dev et test).
|
|
*
|
|
* Modèle utilisé : constante ANTHROPIC_MODEL ci-dessous, volontairement
|
|
* isolée (même principe qu'OPENAI_MODEL) — claude-sonnet-5 par défaut,
|
|
* équilibre qualité/coût suffisant pour juger si une identité "sonne juste"
|
|
* en test ; à remonter en claude-opus-5 si le rendu manque de nuance, ou à
|
|
* descendre en Haiku si le coût des essais répétés pèse trop.
|
|
*
|
|
* ⚠️ Point non vérifié en conditions réelles : cette session n'a pas la main
|
|
* pour exécuter un vrai appel à l'API Anthropic (pas de clé disponible ici).
|
|
* L'appel ci-dessous suit la documentation Anthropic (endpoint Messages,
|
|
* en-têtes Content-Type/anthropic-version/X-Api-Key) vérifiée le 10 septembre
|
|
* 2026, mais mérite un premier essai réel pour confirmer que rien n'a divergé
|
|
* entre-temps. Si le premier essai échoue, le message d'erreur renvoyé par
|
|
* Anthropic est transmis tel quel au front-end (voir gestion d'erreur
|
|
* ci-dessous) pour pouvoir corriger vite sans deviner.
|
|
*
|
|
* ⚠️ Session utilisateur : accès réservé aux utilisateurs identifiés
|
|
* (`$_SESSION['USR_Code']`, ouverte par aspic.php) — même principe simplifié
|
|
* que collaborateurs_identite.php (pas de vérification renforcée par
|
|
* USR_Code précis, décision de Jean-Marc du 10 septembre 2026, à revoir dès
|
|
* qu'un second utilisateur réel aura un compte MIRADOR sur la même base).
|
|
*/
|
|
|
|
session_start();
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
require __DIR__ . '/db.php';
|
|
require __DIR__ . '/journal_ia.php';
|
|
require __DIR__ . '/moteur_prompts.php';
|
|
|
|
if (empty($_SESSION['USR_Code'])) {
|
|
http_response_code(401);
|
|
echo json_encode(['error' => 'Non identifié — veuillez vous connecter.']);
|
|
exit;
|
|
}
|
|
// Prénom de l'utilisateur connecté (posé en session par aspic.php au login),
|
|
// jamais "Jean-Marc" en dur dans le prompt (11 septembre 2026, point relevé
|
|
// par Jean-Marc lui-même : ASPIC est censé pouvoir un jour tourner pour un
|
|
// autre patron, Joseph ou Sylvie).
|
|
$patron = trim((string) ($_SESSION['USR_Prenom'] ?? '')) ?: "l'utilisateur";
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
http_response_code(405);
|
|
echo json_encode(['error' => 'Méthode non autorisée']);
|
|
exit;
|
|
}
|
|
|
|
const ANTHROPIC_MODEL = 'claude-sonnet-5';
|
|
const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';
|
|
const ANTHROPIC_VERSION = '2023-06-01';
|
|
const ANTHROPIC_MAX_TOKENS = 1024;
|
|
|
|
$data = json_decode(file_get_contents('php://input'), true) ?? [];
|
|
$agnIncr = (int) ($data['AGN_Incr'] ?? 0);
|
|
$messages = $data['messages'] ?? [];
|
|
|
|
if ($agnIncr <= 0) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'AGN_Incr manquant ou invalide']);
|
|
exit;
|
|
}
|
|
if (!is_array($messages) || !$messages) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Aucun message à transmettre']);
|
|
exit;
|
|
}
|
|
|
|
// trim() : même précaution que pour OPENAI_API_KEY (contact_ia_interprete.php)
|
|
// — un espace resté collé en copiant-collant la clé dans docker-compose/.env
|
|
// suffit à faire échouer l'authentification avec un message qui ne le laisse
|
|
// pas deviner.
|
|
$apiKey = trim((string) getenv('ANTHROPIC_API_KEY'));
|
|
if (!$apiKey) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => "Clé ANTHROPIC_API_KEY absente côté serveur — à ajouter dans l'environnement du conteneur avant de pouvoir tester."]);
|
|
exit;
|
|
}
|
|
|
|
// ANTHROPIC_WORKSPACE_ID (optionnelle) — ajoutée le 10 septembre 2026 :
|
|
// premier essai réel de Jean-Marc en erreur HTTP 400, message Anthropic sans
|
|
// ambiguïté : « This API key is not scoped to a workspace, so this request
|
|
// must include the anthropic-workspace-id header ». Arrive avec une clé
|
|
// personnelle/de type "organisation" (pas liée à un espace de travail en
|
|
// particulier dans la Console) plutôt qu'avec une clé créée directement DANS
|
|
// un espace de travail (celles-ci n'ont pas besoin de cet en-tête). Les deux
|
|
// façons de résoudre, au choix de Jean-Marc :
|
|
// 1. Recréer la clé depuis platform.claude.com/settings/keys en choisissant
|
|
// explicitement un espace de travail précis (pas "Toute l'organisation")
|
|
// au moment de la création — alors cette variable n'est pas nécessaire.
|
|
// 2. Garder la clé actuelle et poser ANTHROPIC_WORKSPACE_ID (format
|
|
// wrkspc_..., visible dans les réglages de l'espace de travail concerné
|
|
// sur platform.claude.com) à côté d'ANTHROPIC_API_KEY dans l'environnement
|
|
// du conteneur.
|
|
// Header ajouté seulement si la variable est posée, pour ne rien changer au
|
|
// comportement le jour où Jean-Marc bascule sur une clé déjà liée à un espace
|
|
// de travail (option 1).
|
|
$workspaceId = trim((string) getenv('ANTHROPIC_WORKSPACE_ID'));
|
|
|
|
try {
|
|
$pdo = getDbConnection();
|
|
} catch (PDOException $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Connexion base de données impossible']);
|
|
exit;
|
|
}
|
|
|
|
try {
|
|
$stmt = $pdo->prepare(
|
|
'SELECT AGN_Prenom, AGN_Fonction, AGN_Identite, AGN_Relationnel, AGN_Experience, AGN_Competences FROM ASP_Agents WHERE AGN_Incr = ?'
|
|
);
|
|
$stmt->execute([$agnIncr]);
|
|
$collaborateur = $stmt->fetch();
|
|
} catch (PDOException $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Erreur base de données : ' . $e->getMessage()]);
|
|
exit;
|
|
}
|
|
|
|
if (!$collaborateur) {
|
|
http_response_code(404);
|
|
echo json_encode(['error' => 'Collaborateur introuvable']);
|
|
exit;
|
|
}
|
|
|
|
$prenom = $collaborateur['AGN_Prenom'] ?: 'ce collaborateur';
|
|
$fonction = $collaborateur['AGN_Fonction'] ?: '';
|
|
$identite = trim((string) ($collaborateur['AGN_Identite'] ?? ''));
|
|
$experience = trim((string) ($collaborateur['AGN_Experience'] ?? ''));
|
|
|
|
$blocIdentite = $identite !== ''
|
|
? $identite
|
|
: "(Aucune identité détaillée n'a encore été écrite pour {$prenom} — tiens-toi en à son rôle : {$fonction}.)";
|
|
|
|
// AGN_Relationnel (11 septembre 2026) : posture relationnelle envers l'équipe
|
|
// et Jean-Marc — statique comme AGN_Identite, volontairement distincte pour
|
|
// que le tri de l'identité individuelle (caractère, goûts) n'ait pas à se
|
|
// mélanger avec le rapport à l'équipe qui donne du sens à l'ensemble (voir
|
|
// réflexions stratégiques). Même statut qu'AGN_Identite dans le prompt : pas
|
|
// de garde anti-récitation dédiée, elle reste sous la consigne générale de
|
|
// l'intro ("aide-mémoire à ton seul usage, jamais un texte à réciter")
|
|
// puisque c'est un texte rédigé par Jean-Marc, pas une liste factuelle comme
|
|
// AGN_Competences (voir plus bas pourquoi celle-ci en a besoin).
|
|
$relationnel = trim((string) ($collaborateur['AGN_Relationnel'] ?? ''));
|
|
$blocRelationnel = $relationnel !== ''
|
|
? "\n\nTa manière d'être avec l'équipe et avec {$patron} :\n\n{$relationnel}"
|
|
: '';
|
|
|
|
// Bloc optionnel : absent du prompt tant qu'aucun échange n'a encore été
|
|
// capturé (collaborateur flambant neuf, ou expérience remise à zéro via
|
|
// "Modifier l'expérience") — pas de bloc vide ni de phrase de remplissage
|
|
// inutile dans ce cas.
|
|
$blocExperience = $experience !== ''
|
|
? "\n\nVoici ce que tu retiens de vos échanges précédents avec {$patron} (essais de dialogue passés) :\n\n{$experience}"
|
|
: '';
|
|
|
|
// Anti-récitation propre à ce bloc plutôt que de compter sur la consigne
|
|
// générale plus haut dans le prompt (voir $promptSysteme) : premier essai
|
|
// réel (11 septembre 2026) montrant qu'une compétence du catalogue formel
|
|
// (ASP_Competences, pensé au départ comme mécanique — voir en-tête de
|
|
// competences.php) était citée telle quelle par le modèle, y compris une
|
|
// compétence posée en test ("savoir faire un bon café") sans intention
|
|
// qu'elle infuse la personnalité — ce catalogue n'est pas écrit pour être
|
|
// lu tel quel dans une conversation, contrairement à AGN_Identite.
|
|
$competencesAgent = trim((string) ($collaborateur['AGN_Competences'] ?? ''));
|
|
$blocCompetences = $competencesAgent !== ''
|
|
? "\n\nTes compétences déclarées (ce que tu sais concrètement faire ou apporter — "
|
|
. "un aide-mémoire pour savoir ce que tu peux proposer, jamais une liste à réciter, "
|
|
. "énumérer ou commenter telle quelle, même si on te le demande directement ; "
|
|
. "une entrée isolée ou insolite ne mérite pas plus d'attention qu'une autre) :\n\n{$competencesAgent}"
|
|
: '';
|
|
|
|
// -------- Prompt système --------
|
|
// Texte sorti du code le 11 septembre 2026 (Jean-Marc veut pouvoir le lire/
|
|
// modifier lui-même) — voir moteur_prompts.php pour le texte par défaut, la
|
|
// table ASP_Prompts et bo_prompts.html pour l'édition. Distinction "collègue
|
|
// de travail" / "assistant IA" / "compagnon virtuel" et règle de
|
|
// non-divulgation d'AGN_Identite : décisions du 10 septembre 2026 désormais
|
|
// actées dans ce texte, plus dans ce fichier PHP.
|
|
try {
|
|
$promptSysteme = chargerPromptSysteme($pdo, 'COLLABORATEUR_DIALOGUE', [
|
|
'{{PRENOM}}' => $prenom,
|
|
'{{FONCTION}}' => $fonction,
|
|
'{{PATRON}}' => $patron,
|
|
'{{BLOC_IDENTITE}}' => $blocIdentite,
|
|
'{{BLOC_RELATIONNEL}}' => $blocRelationnel,
|
|
'{{BLOC_EXPERIENCE}}' => $blocExperience,
|
|
'{{BLOC_COMPETENCES}}' => $blocCompetences,
|
|
]);
|
|
} catch (Throwable $e) {
|
|
http_response_code(500);
|
|
echo json_encode(['error' => 'Prompt système indisponible : ' . $e->getMessage()]);
|
|
exit;
|
|
}
|
|
|
|
// -------- Historique --------
|
|
// Reconstruit et validé plutôt que transmis tel quel : seuls role/content
|
|
// sont conservés, seuls "user"/"assistant" acceptés (même précaution
|
|
// défensive que contacts.php/contact_ia_interprete.php envers toute donnée
|
|
// venue du front-end).
|
|
$historique = [];
|
|
foreach ($messages as $m) {
|
|
$role = $m['role'] ?? '';
|
|
$contenu = trim((string) ($m['content'] ?? ''));
|
|
if (!in_array($role, ['user', 'assistant'], true) || $contenu === '') {
|
|
continue;
|
|
}
|
|
$historique[] = ['role' => $role, 'content' => $contenu];
|
|
}
|
|
if (!$historique) {
|
|
http_response_code(400);
|
|
echo json_encode(['error' => 'Aucun message valide à transmettre']);
|
|
exit;
|
|
}
|
|
|
|
// -------- Appel Anthropic --------
|
|
// system en tableau de blocs + cache_control (11 septembre 2026, voir
|
|
// journal_ia.php) : ce prompt (identité + expérience) ne change pas d'un
|
|
// tour à l'autre d'une même session de test, mais était jusqu'ici refacturé
|
|
// en entier à CHAQUE appel — le cache Anthropic permet de ne payer le tarif
|
|
// plein qu'une fois, puis un tarif réduit (~1/10ᵉ) tant que rien ne change.
|
|
// ⚠️ Comme pour le reste de ce fichier, mécanisme suivant la documentation
|
|
// Anthropic mais jamais éprouvé en conditions réelles avant ce soir.
|
|
$corpsRequete = [
|
|
'model' => ANTHROPIC_MODEL,
|
|
'max_tokens' => ANTHROPIC_MAX_TOKENS,
|
|
'system' => [
|
|
['type' => 'text', 'text' => $promptSysteme, 'cache_control' => ['type' => 'ephemeral']],
|
|
],
|
|
'messages' => $historique,
|
|
];
|
|
|
|
$entetes = [
|
|
'Content-Type: application/json',
|
|
'anthropic-version: ' . ANTHROPIC_VERSION,
|
|
'X-Api-Key: ' . $apiKey,
|
|
];
|
|
// Voir commentaire ANTHROPIC_WORKSPACE_ID plus haut — en-tête ajouté
|
|
// uniquement si la variable est posée.
|
|
if ($workspaceId) {
|
|
$entetes[] = 'anthropic-workspace-id: ' . $workspaceId;
|
|
}
|
|
|
|
$ch = curl_init(ANTHROPIC_API_URL);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => json_encode($corpsRequete),
|
|
CURLOPT_HTTPHEADER => $entetes,
|
|
CURLOPT_TIMEOUT => 60,
|
|
CURLOPT_CONNECTTIMEOUT => 10,
|
|
]);
|
|
$reponseBrute = curl_exec($ch);
|
|
$erreurCurl = curl_error($ch);
|
|
$codeHttp = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($reponseBrute === false) {
|
|
http_response_code(502);
|
|
echo json_encode(['error' => "Appel à l'API Anthropic impossible : " . $erreurCurl]);
|
|
exit;
|
|
}
|
|
|
|
$reponseDecodee = json_decode($reponseBrute, true);
|
|
|
|
if ($codeHttp !== 200) {
|
|
http_response_code(502);
|
|
echo json_encode([
|
|
'error' => "L'API Anthropic a renvoyé une erreur (HTTP $codeHttp) : " .
|
|
($reponseDecodee['error']['message'] ?? $reponseBrute),
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
// Réponse Messages API : content est un tableau de blocs ({"type":"text",
|
|
// "text":"..."} pour une réponse texte simple, seul cas utilisé ici — pas
|
|
// d'appel d'outil configuré).
|
|
$texteReponse = '';
|
|
foreach ($reponseDecodee['content'] ?? [] as $bloc) {
|
|
if (($bloc['type'] ?? '') === 'text') {
|
|
$texteReponse .= $bloc['text'];
|
|
}
|
|
}
|
|
|
|
if ($texteReponse === '') {
|
|
http_response_code(502);
|
|
echo json_encode(['error' => "Réponse Anthropic inattendue (pas de contenu texte) : " . $reponseBrute]);
|
|
exit;
|
|
}
|
|
|
|
// Journalisation du coût réel (11 septembre 2026, voir journal_ia.php) —
|
|
// jamais bloquant, jamais silencieux côté serveur en cas d'échec.
|
|
journaliserAppelIA($pdo, 'collaborateur_dialogue', $agnIncr, null, ANTHROPIC_MODEL, $reponseDecodee['usage'] ?? []);
|
|
|
|
echo json_encode(['reponse' => $texteReponse]);
|