Files
ASPIC/collaborateur_dialogue.php
T

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]);