Progetto Guidato — Chatbot e Integrazione AI con PHP

Dalla prima chiamata API alla conversazione con memoria, passo dopo passo · Marco Consiglio
Volta Institute · Napoli
A.A. 2026
0

Punto di partenza — cosa costruiamo

🎯 Obiettivo del progetto
Costruiremo un chatbot minimo: un form che invia un messaggio a un'API AI esterna tramite PHP (mai direttamente da JavaScript, per non esporre la chiave API), salva la conversazione nel database, e — nella versione finale — mantiene il contesto tra un messaggio e l'altro.
📁 Struttura delle cartelle del progetto
chatbot/ ├── config.php // connessione DB + chiave API (Passo 2) ├── chat.php // invio messaggio e visualizzazione (Passo 3) ├── login.php // autenticazione (Passo 5) └── ai_client.php // funzioni di comunicazione con l'API (Passo 3-6)
💡 ai_client.php è il file che crescerà di più nel corso della guida: partirà con una singola funzione di chiamata semplice (Passo 3) e arriverà a gestire cronologia ed errori (Passo 6).
1

Il database — conversazioni e messaggi

🗄️ Schema, nell'ordine corretto
conversazioni prima (indipendente), messaggi dopo (referenzia conversazioni).
CREATE TABLE conversazioni ( id INT AUTO_INCREMENT PRIMARY KEY, utente_id INT, iniziata_il TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE messaggi ( id INT AUTO_INCREMENT PRIMARY KEY, conversazione_id INT, ruolo ENUM('user', 'assistant') NOT NULL, contenuto TEXT NOT NULL, creato_il TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (conversazione_id) REFERENCES conversazioni(id) );
💡 Ogni scambio (domanda + risposta) diventa DUE righe in messaggi, distinte dal campo ruolo — questo è ciò che permetterà, al Passo 6, di ricostruire l'ordine esatto della conversazione con un semplice ORDER BY.
✅ Checkpoint — a questo punto hai
2 tabelle collegate, pronte a memorizzare conversazioni — nessuna chiamata API ancora, nessun file PHP.
2

Connessione e chiave API

config.php
🔌 Database + chiave API, mai insieme al codice versionato
<?php $host = 'localhost'; $db = 'chatbot_volta'; $user = 'root'; $password = getenv('DB_PASSWORD'); try { $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8mb4", $user, $password); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { die("Connessione fallita: " . $e->getMessage()); } // caricata da variabile d'ambiente, MAI scritta qui come stringa $apiKey = getenv('ANTHROPIC_API_KEY'); if (!$apiKey) { die('Chiave API non configurata'); } ?>
⚠️ Da qui in avanti, ogni file che chiama l'API includerà config.php per accedere a $apiKey — mai copiata o scritta altrove nel codice.
chat.php (versione di TEST, verrà sostituita al Passo 3)
<?php require 'config.php'; echo "Connessione OK, chiave API caricata."; ?>
✅ Checkpoint — a questo punto hai: apri chat.php. Se vedi il messaggio, database e chiave API sono entrambi pronti per il passo successivo.
3

Funzionalità core — inviare un messaggio e ricevere risposta

🎯 Cosa costruiamo in questo passo
La versione più semplice possibile: un messaggio va all'API, la risposta torna a schermo. Nessun salvataggio nel database ancora, nessuna cronologia — solo il meccanismo base della chiamata.
ai_client.php
🌐 Una funzione per chiamare l'API
<?php function chiamaAI(string $messaggio, string $apiKey): string { $ch = curl_init('https://api.anthropic.com/v1/messages'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', "x-api-key: $apiKey", 'anthropic-version: 2023-06-01' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'model' => 'claude-sonnet-5', 'max_tokens' => 1024, 'messages' => [['role' => 'user', 'content' => $messaggio]] ])); $risposta = curl_exec($ch); curl_close($ch); $dati = json_decode($risposta, true); return $dati['content'][0]['text'] ?? ''; } ?>
chat.php (sostituisce la versione di test del Passo 2)
<?php require 'config.php'; require 'ai_client.php'; $rispostaAI = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $messaggio = $_POST['messaggio'] ?? ''; $rispostaAI = chiamaAI($messaggio, $apiKey); } ?> <form method="post"> <input type="text" name="messaggio" placeholder="Scrivi un messaggio..." required> <button type="submit">Invia</button> </form> <?php if ($rispostaAI): ?> <p><strong>Assistente:</strong> <?= htmlspecialchars($rispostaAI) ?></p> <?php endif; ?>
✅ Checkpoint — a questo punto hai: puoi scrivere un messaggio e vedere la risposta dell'AI a schermo. Ogni ricarica della pagina dimentica tutto: nessuna cronologia, nessun salvataggio ancora.
4

Aggiungere le relazioni — salvare la conversazione

🔗 Ora salviamo davvero i messaggi nel database
Modifichiamo chat.php del Passo 3 per creare una conversazione e salvare sia il messaggio dell'utente sia la risposta AI come righe collegate.
chat.php (MODIFICA rispetto al Passo 3)
<?php require 'config.php'; require 'ai_client.php'; // nuovo: creiamo (o riusiamo) una conversazione $conversazioneId = $_GET['conv'] ?? null; if (!$conversazioneId) { $stmt = $pdo->prepare("INSERT INTO conversazioni () VALUES ()"); $stmt->execute(); $conversazioneId = $pdo->lastInsertId(); } $rispostaAI = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $messaggio = $_POST['messaggio'] ?? ''; $rispostaAI = chiamaAI($messaggio, $apiKey); // nuovo: salviamo entrambi i messaggi $stmt = $pdo->prepare("INSERT INTO messaggi (conversazione_id, ruolo, contenuto) VALUES (:cid, :ruolo, :contenuto)"); $stmt->execute(['cid' => $conversazioneId, 'ruolo' => 'user', 'contenuto' => $messaggio]); $stmt->execute(['cid' => $conversazioneId, 'ruolo' => 'assistant', 'contenuto' => $rispostaAI]); } ?> // il form resta identico al Passo 3
💡 Il form HTML non cambia — solo la logica PHP sopra è cresciuta. Nota execute() chiamato DUE volte sullo stesso $stmt preparato: prepared statement diversi per parametri diversi, riusando la stessa struttura di query.
✅ Checkpoint — a questo punto hai
Ogni scambio viene ora salvato in messaggi, collegato a una conversazione_id. Ricaricando la pagina con lo stesso ?conv=ID nell'URL, la conversazione esiste ancora nel database — ma non viene ancora mostrata (arriva al Passo 6).
5

Permessi e sicurezza — login e protezione della chiave

🎯 Chi può usare il chatbot, e come proteggiamo la chiave
Aggiungiamo login (per collegare le conversazioni a un utente reale) e ribadiamo perché la chiave API non deve mai lasciare il server.
login.php
<?php session_start(); require 'config.php'; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt = $pdo->prepare("SELECT * FROM utenti WHERE email = :email"); $stmt->execute(['email' => $_POST['email']]); $utente = $stmt->fetch(PDO::FETCH_ASSOC); if ($utente && password_verify($_POST['password'], $utente['password_hash'])) { $_SESSION['utente_id'] = $utente['id']; header('Location: chat.php'); exit; } echo "Credenziali non valide"; } ?> <form method="post"> <input type="email" name="email" required> <input type="password" name="password" required> <button type="submit">Accedi</button> </form>
chat.php (MODIFICA rispetto al Passo 4: login + utente_id nella conversazione)
<?php session_start(); if (!isset($_SESSION['utente_id'])) { header('Location: login.php'); exit; } require 'config.php'; require 'ai_client.php'; $conversazioneId = $_GET['conv'] ?? null; if (!$conversazioneId) { $stmt = $pdo->prepare("INSERT INTO conversazioni (utente_id) VALUES (:utente_id)"); $stmt->execute(['utente_id' => $_SESSION['utente_id']]); $conversazioneId = $pdo->lastInsertId(); } // ... resto invariato rispetto al Passo 4 ... ?>
💡 Solo due piccole aggiunte: il controllo sessione in cima (identico nel principio a CMS e CRM), e utente_id ora popolato davvero invece di lasciare la colonna NULL come nel Passo 4.
🔑 Perché la chiave API non deve mai arrivare al browser
Tutta la logica di ai_client.php gira in PHP, lato server — se si tentasse di chiamare l'API direttamente da JavaScript nel browser, la chiave dovrebbe essere inclusa nel codice JS della pagina, visibile a chiunque tramite gli strumenti sviluppatore. Ecco perché fin dal Passo 2 la chiave vive solo in $apiKey, mai trasmessa al client.
✅ Checkpoint — a questo punto hai: accesso al chatbot solo dopo login; ogni conversazione collegata al vero utente; chiave API mai esposta.
6

Funzionalità avanzate — contesto e gestione errori

ai_client.php (ESTENSIONE: nuova funzione per il contesto)
🔄 Recuperare e inviare tutta la cronologia
<?php // aggiunta ad ai_client.php, la funzione chiamaAI() del Passo 3 resta invariata function recuperaCronologia(PDO $pdo, int $conversazioneId): array { $stmt = $pdo->prepare(" SELECT ruolo, contenuto FROM messaggi WHERE conversazione_id = :id ORDER BY creato_il ASC "); $stmt->execute(['id' => $conversazioneId]); $righe = $stmt->fetchAll(PDO::FETCH_ASSOC); return array_map(fn(array $r): array => [ 'role' => $r['ruolo'], 'content' => $r['contenuto'] ], $righe); } function chiamaAIConContesto(array $cronologia, string $apiKey): array { try { $ch = curl_init('https://api.anthropic.com/v1/messages'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', "x-api-key: $apiKey", 'anthropic-version: 2023-06-01' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'model' => 'claude-sonnet-5', 'max_tokens' => 1024, 'messages' => $cronologia ])); $risposta = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { return ['errore' => true, 'messaggio' => "Servizio momentaneamente occupato, riprova"]; } $dati = json_decode($risposta, true); return ['errore' => false, 'testo' => $dati['content'][0]['text'] ?? '']; } catch (Exception $e) { return ['errore' => true, 'messaggio' => 'Servizio AI non disponibile']; } } ?>
💡 chiamaAI() del Passo 3 non viene toccata: chiamaAIConContesto() è una NUOVA funzione più completa, che sostituirà la prima nell'uso ma resta un esempio didattico di come si è partiti dalla versione semplice.
chat.php (MODIFICA rispetto al Passo 5: usare cronologia e mostrare la conversazione)
<?php // ... session_start(), controllo login, connessione, conversazioneId come al Passo 5 ... if ($_SERVER['REQUEST_METHOD'] === 'POST') { $messaggio = $_POST['messaggio'] ?? ''; $stmt = $pdo->prepare("INSERT INTO messaggi (conversazione_id, ruolo, contenuto) VALUES (:cid, 'user', :contenuto)"); $stmt->execute(['cid' => $conversazioneId, 'contenuto' => $messaggio]); $cronologia = recuperaCronologia($pdo, $conversazioneId); $risultato = chiamaAIConContesto($cronologia, $apiKey); if (!$risultato['errore']) { $stmt->execute(['cid' => $conversazioneId, 'contenuto' => $risultato['testo']]); } } // nuovo: mostriamo l'intera conversazione, non solo l'ultimo scambio $storico = recuperaCronologia($pdo, $conversazioneId); ?> <?php foreach ($storico as $m): ?> <p><strong><?= $m['role'] === 'user' ? 'Tu' : 'Assistente' ?>:</strong> <?= htmlspecialchars($m['content']) ?></p> <?php endforeach; ?> <form method="post"> <input type="text" name="messaggio" required> <button type="submit">Invia</button> </form>
✅ Checkpoint — a questo punto hai: la funzione recuperaCronologia() scritta sopra viene riusata DUE volte in questo stesso file — prima per costruire il contesto da inviare all'API, poi per mostrare l'intera conversazione a schermo. L'assistente ora "ricorda" gli scambi precedenti.
7

Integrazione finale — come tutto si tiene insieme

🧩 Il flusso completo, dall'inizio alla fine
PassoCosa abbiamo costruito
12 tabelle: conversazioni → messaggi
2config.php: connessione DB + chiave API da variabile d'ambiente
3ai_client.php con chiamaAI(); chat.php invia un messaggio, mostra la risposta, non salva nulla
4chat.php crea una conversazione e salva ogni scambio in messaggi
5login.php; utente_id reale nella conversazione; chiave API confermata mai esposta al client
6recuperaCronologia() + chiamaAIConContesto(); chat.php mostra l'intera conversazione e la usa come contesto per l'API
Il campo ruolo scritto nello schema al Passo 1 resta senza uno scopo evidente fino al Passo 6, dove diventa essenziale per separare "user" e "assistant" nella cronologia da inviare all'API. La funzione semplice del Passo 3 non viene mai cancellata: resta a dimostrare da dove si è partiti.
🚀 Da qui in avanti
Estensioni naturali non trattate qui: elenco delle conversazioni passate dell'utente, limite di lunghezza sulla cronologia inviata (per non superare i token massimi dell'API), indicatore "sta scrivendo..." lato JavaScript mentre PHP elabora la richiesta.
🎓 Esercitazioni pratiche
1
Elenco delle conversazioni dell'utente
Crea conversazioni.php che elenca (con WHERE utente_id = :id, come il filtro per proprietario visto nel CRM) tutte le conversazioni passate dell'utente loggato, con link a chat.php?conv=ID.
⏱ 35 min📦 1 nuovo file💻 Individuale
2
Limita la cronologia agli ultimi 10 messaggi
Modifica recuperaCronologia() del Passo 6 aggiungendo LIMIT 10 (con relativo ORDER BY ... DESC e successivo riordino in PHP) per evitare di inviare cronologie troppo lunghe all'API.
⏱ 30 min📦 1 funzione modificata💻 Individuale
3
Messaggio di errore comprensibile lato utente
Estendi chat.php del Passo 6 per mostrare $risultato['messaggio'] in un blocco visibile quando $risultato['errore'] è true, invece di ignorare silenziosamente l'errore come nella versione base.
⏱ 20 min📦 1 file modificato💻 Individuale